#
tokens: 49736/50000 43/64 files (page 1/4)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 4. Use http://codebase.md/mhmzdev/figma-flutter-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .changeset
│   └── config.json
├── .github
│   └── workflows
│       └── release.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── docs
│   ├── cursor_rules_example.md
│   ├── figma-flutter-mcp.md
│   ├── figma-framework-mcp.md
│   ├── getting-started.md
│   └── images
│       ├── button.png
│       ├── figma-flutter-mcp.png
│       ├── screen.png
│       ├── svg.gif
│       ├── svgs_clean.gif
│       ├── text-style-frame.png
│       └── theme-frame.png
├── LICENSE.md
├── package.json
├── README.ja.md
├── README.ko.md
├── README.md
├── README.zh-cn.md
├── README.zh-tw.md
├── src
│   ├── cli.ts
│   ├── config.ts
│   ├── extractors
│   │   ├── colors
│   │   │   ├── core.ts
│   │   │   ├── extractor.ts
│   │   │   ├── index.ts
│   │   │   └── types.ts
│   │   ├── components
│   │   │   ├── core.ts
│   │   │   ├── deduplicated-extractor.ts
│   │   │   ├── extractor.ts
│   │   │   ├── index.ts
│   │   │   ├── types.ts
│   │   │   └── variant-analyzer.ts
│   │   ├── flutter
│   │   │   ├── global-vars.ts
│   │   │   ├── index.ts
│   │   │   ├── style-library.ts
│   │   │   └── style-merger.ts
│   │   ├── screens
│   │   │   ├── core.ts
│   │   │   ├── extractor.ts
│   │   │   ├── index.ts
│   │   │   └── types.ts
│   │   └── typography
│   │       ├── core.ts
│   │       ├── extractor.ts
│   │       ├── index.ts
│   │       └── types.ts
│   ├── figma-config.ts
│   ├── server.ts
│   ├── services
│   │   └── figma.ts
│   ├── tools
│   │   ├── flutter
│   │   │   ├── assets
│   │   │   │   ├── asset-manager.ts
│   │   │   │   ├── assets.ts
│   │   │   │   └── svg-assets.ts
│   │   │   ├── components
│   │   │   │   ├── component-tool.ts
│   │   │   │   ├── deduplicated-helpers.ts
│   │   │   │   └── helpers.ts
│   │   │   ├── index.ts
│   │   │   ├── screens
│   │   │   │   ├── helpers.ts
│   │   │   │   └── screen-tool.ts
│   │   │   ├── semantic-detection.ts
│   │   │   ├── theme
│   │   │   │   ├── colors
│   │   │   │   │   ├── theme-generator.ts
│   │   │   │   │   └── theme-tool.ts
│   │   │   │   └── typography
│   │   │   │       ├── typography-generator.ts
│   │   │   │       └── typography-tool.ts
│   │   │   └── visual-context.ts
│   │   └── index.ts
│   ├── types
│   │   ├── errors.ts
│   │   ├── figma.ts
│   │   └── flutter.ts
│   └── utils
│       ├── figma-url-parser.ts
│       ├── helpers.ts
│       ├── logger.ts
│       ├── retry.ts
│       └── validation.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | node_modules/
2 | .env
3 | package-lock.json
4 | dist/
5 | output/
6 | reports/
7 | .cursor/
8 | prompts/
9 | *.DS_STORE
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | <div align="center">
  2 |   <img src="docs/images/figma-flutter-mcp.png" alt="Theme Setup Example" style="max-width: 100%; height: auto;">
  3 |   <br>
  4 | 
  5 |   <h1>Figma to Flutter MCP Server</h1>
  6 |    <p>
  7 |     🌐 Available in:
  8 |     <a href="README.ko.md">한국어 (Korean)</a> |
  9 |     <a href="README.ja.md">日本語 (Japanese)</a> |
 10 |     <a href="README.zh-cn.md">简体中文 (Simplified Chinese)</a> |
 11 |     <a href="README.zh-tw.md">繁體中文 (Traditional Chinese)</a>
 12 |   </p>
 13 |   <h3>Utilize Figma's rich data in your coding agent.<br/>Implement designs in Flutter way!</h3>
 14 |   <a href="https://npmcharts.com/compare/figma-flutter-mcp?interval=30">
 15 |     <img alt="weekly downloads" src="https://img.shields.io/npm/dm/figma-flutter-mcp.svg">
 16 |   </a>
 17 |   <a href="https://github.com/mhmzdev/figma-flutter-mcp/blob/main/LICENSE">
 18 |     <img alt="MIT License" src="https://img.shields.io/github/license/mhmzdev/figma-flutter-mcp" />
 19 |   </a>
 20 |   <a href="https://twitter.com/mhmzdev">
 21 |     <img alt="Twitter" src="https://img.shields.io/twitter/url?url=https%3A%2F%2Fx.com%2Fmhmzdev&label=%40mhmzdev" />
 22 |   </a>
 23 | </div>
 24 | <br>
 25 | 
 26 | Use [Cursor](https://cursor.sh) or other AI-powered tools to access Figma's rich files, data, components and much more using [MCP server](https://modelcontextprotocol.io/).
 27 | 
 28 | ## 📋 Table of Contents
 29 | 
 30 | - [🦋 Observable Flutter #70](#-observable-flutter-70)
 31 | - [🎥 Short Video Demo](#-short-video-demo)
 32 | - [📝 Getting Started](#-getting-started)
 33 | - [📚 How it works](#-how-it-works--details-here)
 34 | - [🛠️ Usage](#-usage)
 35 |   - [🔑 Figma API Key](#-figma-api-key)
 36 |   - [🏹 MCP in Cursor](#-mcp-in-cursor)
 37 |   - [🚀 Quick Start for Local Testing](#-quick-start-for-local-testing)
 38 | - [🧱 Basic Workflow](#-basic-workflow)
 39 |   - [🤖 AI Coding Agent Assistance](#-ai-coding-agent-assistance)
 40 |   - [⚠️ If SVG assets don’t work with screen generation](#-if-svg-assets-dont-work-with-screen-generation)
 41 | - [⚠️ Disclaimers](#-disclaimers)
 42 | - [🙌🏼 Acknowledgments](#-acknowledgments)
 43 | - [🧱 Other framworks](#-other-framworks)
 44 | - [🔑 License](#-license)
 45 | - [🙋‍♂️ Author](#-author)
 46 |   - [Muhammad Hamza](#muhammad-hamza)
 47 | 
 48 | ## 🦋 Observable Flutter #70
 49 | Featured on Observable Flutter with enhanced explanation and demo:
 50 | 
 51 | <a href="https://www.youtube.com/live/d7qrvytOxSA?si=ESY8hPJpQm_OY4Ye">
 52 |   <img src="https://i.ytimg.com/vi/d7qrvytOxSA/hq720.jpg?sqp=-oaymwEnCNAFEJQDSFryq4qpAxkIARUAAIhCGAHYAQHiAQoIGBACGAY4AUAB&rs=AOn4CLAtjlrlbNDcV_MQ-_MHJN3KAgwpKw" alt="Observable Flutter Figma to Flutter MCP" style="max-width: 100%; height: 300px;">
 53 | </a>
 54 | 
 55 | ## 🎥 Short Video Demo
 56 | Showcased almost all the features of Figma Flutter MCP with real figma design.
 57 | - English: https://youtu.be/lJlfOfpl2sI
 58 | - Urdu/Hindi: https://youtu.be/mepPWpIZ61M
 59 | 
 60 | ## 📝 [Getting Started](docs/getting-started.md)
 61 | You may explore the detailed [getting started](docs/getting-started.md) docs or the [demo video](https://youtu.be/lJlfOfpl2sI) as quick-start. As its a First Release hence there's a lot of room for improvements so you can checkout the [issues](https://github.com/mhmzdev/figma-flutter-mcp/issues) to see what else there's to work or to improve.
 62 | 
 63 | ## 📚 How it works | [Details Here](docs/figma-flutter-mcp.md)
 64 | 1. [Components/Widgets](src/extractors/components/)
 65 | - ✅ Extract Figma node data: Layout, styling, dimensions, colors, text content, etc.
 66 | - ✅ Analyze structure: Child elements, nested components, visual importance
 67 | - ✅ Provide guidance: Suggest Flutter widgets and implementation patterns
 68 | - ❌ NOT generating actual Flutter code files
 69 | 
 70 | 2. [Screens](src/extractors/screens/)
 71 | - ✅ Extract screen metadata: Device type, orientation, dimensions
 72 | - ✅ Identify sections: Header, footer, navigation, content areas
 73 | - ✅ Analyze navigation: Tab bars, app bars, drawers, navigation elements
 74 | - ✅ Provide Scaffold guidance: Suggest Flutter screen structure
 75 | - ❌ NOT generating actual Flutter screen
 76 | 
 77 | Since its just helping AI write Flutter code so it means the better your prompt will be the better results you'll get.
 78 | 
 79 | ## 🛠️ Usage
 80 | Following steps shows a minimal usage and setup instructions:
 81 | 
 82 | ### 🔑 Figma API Key
 83 | You will need to create a Figma access token to use this server. Instructions on how to create a Figma API access token can be found [here](https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens).
 84 | 
 85 | ### 🏹 MCP in Cursor
 86 | Once you've the FIGMA API KEY, you can setup the MCP in cursor as follows:
 87 | 1. Press CMD + Shift + P (Ctrl on Windows)
 88 | 2. Type "Open MCP Settings"
 89 | 3. Click on "Add new MCP"
 90 | 4. Paste the below json object
 91 | 
 92 | #### MacOS/Linux
 93 | ```
 94 | {
 95 |   "mcpServers": {
 96 |     "Figma Flutter MCP": {
 97 |       "command": "npx",
 98 |       "args": ["-y", "figma-flutter-mcp", "--figma-api-key=YOUR-API-KEY", "--stdio"]
 99 |     }
100 |   }
101 | }
102 | ```
103 | #### Windows
104 | ```
105 | {
106 |   "mcpServers": {
107 |     "Figma Flutter MCP": {
108 |       "command": "cmd",
109 |       "args": ["/c", "npx", "-y", "figma-flutter-mcp", "--figma-api-key=YOUR-API-KEY", "--stdio"]
110 |     }
111 |   }
112 | }
113 | ```
114 | 
115 | > NOTE: If you've installed this MCP as `npm` package make sure to keep it updated to latest version. Sometimes, it caches the old version and keep showing you error like "Not being able to use tool call" or "Figma API key setup is not working" etc.
116 | 
117 | 
118 | ### 🚀 Quick Start for Local Testing
119 | 
120 | #### Prerequisites
121 | - Node.js 18+
122 | - Figma API Key (Access Token)
123 | - Cursor AI IDE with MCP support
124 | - Flutter SDK
125 | 
126 | 
127 | For quick local testing, you can run the server via HTTP instead of stdio:
128 | 
129 | ```bash
130 | # Clone and setup
131 | git clone <your-repo-url> figma-flutter-mcp
132 | cd figma-flutter-mcp
133 | npm install
134 | 
135 | # Create .env file with your Figma API key
136 | echo "FIGMA_API_KEY=your-figma-api-key-here" > .env
137 | 
138 | # Start HTTP server for local testing
139 | npm run dev
140 | ```
141 | 
142 | Then add this to your MCP client configuration:
143 | 
144 | ```json
145 | {
146 |   "mcpServers": {
147 |     "local-figma-flutter": {
148 |       "url": "http://localhost:3333/mcp"
149 |     }
150 |   }
151 | }
152 | ```
153 | 
154 | See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed instructions.
155 | 
156 | ## 🧱 Basic Workflow
157 | ### 🤖 AI Coding Agent Assistance
158 | For better results you can setup some instructions in following files as per your AI Coding Agent:
159 | - Cursor: `.cursor/rules/fluttering.mdc`
160 | - Claude: `CLAUDE.md`
161 | - Gemini CLI: `GEMINI.md`
162 | 
163 | This way your AI agent will use the MCP's output and ensure the flutter code is as per your project requirements and structure. You can checkout an example of [cursor rules](docs/cursor_rules_example.md) that I used for testing this out.
164 | 
165 | 1. **Setup Theme & Typography**: The most efficient way, put two frames in Figma with Theme colors and Typography samples on it. For instance:
166 | 
167 | ![Theme Setup Example](docs/images/theme-frame.png)
168 | ![Typography Setup Example](docs/images/text-style-frame.png)
169 | 
170 | - Figma Desktop: Select the frame and press CMD + L or Ctrl + L
171 | - Figma Web: Select the frame and copy the URL
172 | 
173 | > 💡 HINT: The valid URL will contain a FILE ID and NODE ID params
174 | 
175 | ```
176 | "Setup flutter theme from <figma_link> including Colors and Typography.
177 | ```
178 | 
179 | 2. **Widget Generation**: The most efficient way, use COMPONENTS in figma. For example:
180 | 
181 | ![Button](docs/images/button.png)
182 | 
183 | This one has 2 variants with enabled and disabled states.
184 | ```
185 | "Create this widget in flutter from figma COMPONENT link: <figma_link>, use named constructors for variants and break the files in smaller parts for code readability."
186 | ```
187 | If you **do not** have COMPONENTS in figma, you can use FRAME just prompt the AI that you want this to be a widget and it will handle the rest.
188 | 
189 | 3. **Full Screen Generation**: If there are any IMAGE ASSETS (.png, .jpeg, .jpg etc.) available, it will export them and put them in `assets/` along with `pubspec.yaml`
190 | 
191 | <img src="docs/images/screen.png" alt="Screen" height="500" width="auto">
192 | 
193 | ```
194 | "Design this intro screen from the figma link <figma_link>, ensure the code is readable by having smaller files."
195 | ```
196 | 4. **Assets Export**:
197 | - Image Assets: Will work automatically when generating screens
198 | ```
199 | "Export this image asset from figma link: <figma_link>
200 | ```
201 | - SVG Assets: Will NOT work automatically if they are scrambled or are ungrouped, explained below.
202 | ```
203 | "Export this as an SVG asset from Figma link: <figma_link>"
204 | ```
205 | #### ⚠️ If SVG assets don’t work with screen generation
206 | * In Figma vectors include icons and pen-tool shapes, so bulk exports may grab unintended nodes;
207 |   *  Recommend exporting SVGs **separately** i.e. to take them out an an independent FRAME or GROUP
208 |   *  Here's how the separation of SVGs looks like:
209 | 
210 | <img src="docs/images/svgs_clean.gif" alt="Screen" height="500" width="auto">
211 | 
212 | <br>
213 | 
214 | * Here's an example of identifying a GOOD vs BAD svg while exporting them:
215 | 
216 | <br>
217 | 
218 | <img src="docs/images/svg.gif" alt="Screen" height="500" width="auto">
219 | 
220 | ## ⚠️ Disclaimers
221 | 
222 | - **Use Case**: At this stage, its highly recommend to NOT use it to develop scalable apps rather try and play it with MVPs, smaller and explanatory tasks.
223 | - **Figma Design**: Since we're using Figma's API to fetch the node and its details, so the better design you have the more better it will interpret for the AI to consume i.e. auto layouts, frame usage over group usage, consistently aligned across the board.
224 | - **Rate limiting**: Heavy usage may trigger Figma rate limits (e.g., HTTP 429). The server includes retry with backoff, but it does not bypass Figma limits. If you encounter rate limits, wait a few minutes and reduce the request volume.
225 | 
226 | ## 🙌🏼 Acknowledgments
227 | I came across [Figma Context MCP](https://github.com/GLips/Figma-Context-MCP) by [Graham Lipsman](https://x.com/glipsman) that sparks this motivation for me to develop Figma to Flutter explicitly having features like:
228 | - Assets exports
229 | - Colors and Theme setups
230 | - Widget tree and full screen building
231 | 
232 | Others coming soon...
233 | 
234 | ## 🧱 Other framworks
235 | If you want to develop this for React, Angular, React Native, Vue or any other framework. I've added a detailed doc [Figma Framework MCP](docs/figma-framework-mcp.md) that you can explore and get started. Meanwhile I'll maintain a list here if someone's already doing this for framework specific Figma's MCP servers.
236 | - ...
237 | - ...
238 | 
239 | ## 🔑 License
240 | This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details
241 | 
242 | ## 🙋‍♂️ Author
243 | #### Muhammad Hamza
244 | [![LinkedIn Link](https://img.shields.io/badge/Connect-Hamza-blue.svg?logo=linkedin&longCache=true&style=social&label=Connect
245 | )](https://www.linkedin.com/in/mhmzdev)
246 | 
247 | You can also follow my GitHub Profile to stay updated about my latest projects:
248 | 
249 | [![GitHub Follow](https://img.shields.io/badge/Connect-Hamza-blue.svg?logo=Github&longCache=true&style=social&label=Follow)](https://github.com/mhmzdev)
250 | 
251 | If you liked the repo then kindly support it by giving it a star ⭐!
252 | 
253 | Copyright (c) 2025 MUHAMMAD HAMZA
254 | 
255 | ---
256 | 
257 | **Built with ❤️ for designers and developers who want to bridge the gap between design and code.**
```

--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------

```markdown
1 | MIT License
2 | 
3 | Copyright (c) 2025 Muhammad Hamza (https://mhmz.dev)
4 | 
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 | 
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 | 
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```

--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Contributing to Figma Flutter MCP
  2 | 
  3 | Thank you for your interest in contributing to Figma Flutter MCP! This guide will help you get started with development and testing.
  4 | 
  5 | ## 🚀 Quick Start for Contributors
  6 | 
  7 | ### Prerequisites
  8 | - Node.js 18+
  9 | - npm or yarn
 10 | - Figma API Key (for testing)
 11 | - Git
 12 | 
 13 | ### Setup Development Environment
 14 | 
 15 | 1. **Fork and Clone**
 16 |    ```bash
 17 |    git clone https://github.com/your-username/figma-flutter-mcp.git
 18 |    cd figma-flutter-mcp
 19 |    npm install
 20 |    ```
 21 | 
 22 | 2. **Create .env file**
 23 |    ```bash
 24 |    # Create .env file with your Figma API key
 25 |    echo "FIGMA_API_KEY=your-figma-api-key-here" > .env
 26 |    ```
 27 | 
 28 | 3. **Start Development Server**
 29 |    ```bash
 30 |    npm run dev
 31 |    ```
 32 | 
 33 | ## 📋 Development Guidelines
 34 | 
 35 | ### Code Style
 36 | - Use TypeScript for all new code
 37 | - Follow existing code patterns and conventions
 38 | - Use meaningful variable and function names
 39 | - Add docs in `docs/` if you think its neccessary
 40 | 
 41 | ### Project Structure
 42 | ```
 43 | src/
 44 | ├── cli.mts              # CLI entry point
 45 | ├── server.mts           # MCP server implementation
 46 | ├── config.mts           # Configuration handling
 47 | ├── extractors/          # Figma data extractors
 48 | │   ├── colors/
 49 | │   ├── components/
 50 | │   ├── screens/
 51 | │   └── typography/
 52 | ├── tools/               # Flutter code generators
 53 | ├── services/            # External service integrations
 54 | ├── types/               # TypeScript type definitions
 55 | └── utils/               # Utility functions
 56 | ```
 57 | 
 58 | ### Making Changes
 59 | 
 60 | 1. **Create a Branch**
 61 |    ```bash
 62 |    git checkout -b feature/your-feature-name
 63 |    # or
 64 |    git checkout -b fix/issue-description
 65 |    ```
 66 | 
 67 | 2. **Make Your Changes**
 68 |    - Write clean, documented code
 69 |    - Add tests for new features
 70 |    - Update documentation as needed
 71 | 
 72 | 3. **Test Your Changes**
 73 |    ```bash
 74 |    # Build and check for errors
 75 |    npm run build
 76 |    
 77 |    # Test locally
 78 |    npm run dev
 79 |    ```
 80 | 
 81 | 4. **Commit and Push**
 82 |    ```bash
 83 |    git add .
 84 |    git commit -m "feat: add new feature description"
 85 |    git push origin feature/your-feature-name
 86 |    ```
 87 | 
 88 | 5. **Create Pull Request**
 89 |    - Use descriptive titles and descriptions
 90 |    - Reference any related issues
 91 |    - Include screenshots/examples if applicable
 92 | 
 93 | ## 🧪 Local Testing & Development
 94 | 
 95 | The project supports HTTP server mode for easier development and testing. This allows you to test MCP tools without setting up a full MCP client.
 96 | 
 97 | ### Setting Up Your Environment
 98 | 
 99 | If you haven't already set up your Figma API key:
100 | ```bash
101 | # Create .env file
102 | echo "FIGMA_API_KEY=your-figma-api-key-here" > .env
103 | ```
104 | 
105 | **Get your Figma API Key:**
106 | 1. Go to [Figma Settings > Personal Access Tokens](https://www.figma.com/developers/api#access-tokens)
107 | 2. Generate a new personal access token
108 | 3. Copy the token and add it to your `.env` file
109 | 
110 | ⚠️ **Important**: Never commit your `.env` file to version control. It's already included in `.gitignore`.
111 | 
112 | ### Development Server Options
113 | 
114 | #### Using npm scripts (recommended)
115 | ```bash
116 | # Start HTTP server on default port 3333
117 | npm run dev
118 | 
119 | # Start HTTP server on a specific port
120 | npm run dev:port 4000
121 | 
122 | # Start in stdio mode (for MCP clients)
123 | npm run dev:stdio
124 | ```
125 | 
126 | #### Using direct commands
127 | ```bash
128 | # Start HTTP server
129 | npx tsx src/cli.mts --http
130 | 
131 | # Start HTTP server on specific port
132 | npx tsx src/cli.mts --http --port 4000
133 | 
134 | # Start in stdio mode
135 | npx tsx src/cli.mts --stdio
136 | ```
137 | 
138 | #### Using built version
139 | ```bash
140 | # Build first
141 | npm run build
142 | 
143 | # Start HTTP server
144 | node dist/cli.mjs --http
145 | 
146 | # Start HTTP server on specific port
147 | node dist/cli.mjs --http --port 4000
148 | ```
149 | 
150 | ## Connecting to the Server
151 | 
152 | ### MCP Client Configuration
153 | 
154 | To connect an MCP client to the local HTTP server, add this configuration to your MCP JSON config file:
155 | 
156 | ```json
157 | {
158 |   "mcpServers": {
159 |     "local-figma-flutter-mcp": {
160 |       "url": "http://localhost:3333/mcp"
161 |     }
162 |   }
163 | }
164 | ```
165 | 
166 | ## Available Endpoints
167 | 
168 | When the HTTP server is running, the following endpoints are available:
169 | 
170 | - **POST /mcp** - Main Streamable HTTP endpoint for MCP communication
171 | - **GET /mcp** - Session management for StreamableHTTP
172 | - **DELETE /mcp** - Session termination for StreamableHTTP  
173 | - **GET /sse** - Server-Sent Events endpoint (alternative transport)
174 | - **POST /messages** - Message endpoint for SSE transport
175 | 
176 | ## Environment Variables
177 | 
178 | You can configure the server using environment variables. The recommended approach is to use a `.env` file:
179 | 
180 | ### Using .env file (Recommended)
181 | ```env
182 | # Required: Your Figma API key
183 | FIGMA_API_KEY=your-figma-api-key-here
184 | 
185 | # Optional: Enable HTTP mode by default
186 | HTTP_MODE=true
187 | 
188 | # Optional: Set default HTTP port
189 | HTTP_PORT=3333
190 | ```
191 | 
192 | ## 📋 Pull Request Checklist
193 | 
194 | Before submitting a PR:
195 | - [ ] Code builds without errors (`npm run build`)
196 | - [ ] Tests pass (if applicable)
197 | - [ ] Documentation updated
198 | - [ ] PR description explains changes
199 | - [ ] Related issues referenced
200 | - [ ] Follows existing code style
201 | 
202 | Thank you for contributing to Figma Flutter MCP! 🚀
203 | 
```

--------------------------------------------------------------------------------
/src/utils/helpers.ts:
--------------------------------------------------------------------------------

```typescript
1 | 
```

--------------------------------------------------------------------------------
/src/types/flutter.ts:
--------------------------------------------------------------------------------

```typescript
1 | // flutter related types
```

--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
 3 |   "changelog": [
 4 |     "@changesets/changelog-github",
 5 |     {
 6 |       "repo": "mhmzdev/figma-flutter-mcp"
 7 |     }
 8 |   ],
 9 |   "commit": true,
10 |   "fixed": [],
11 |   "linked": [],
12 |   "access": "public",
13 |   "baseBranch": "main",
14 |   "updateInternalDependencies": "patch",
15 |   "ignore": []
16 | }
```

--------------------------------------------------------------------------------
/src/extractors/flutter/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // src/extractors/flutter/index.mts
 2 | 
 3 | export { 
 4 |   FlutterStyleLibrary, 
 5 |   FlutterCodeGenerator,
 6 |   type FlutterStyleDefinition,
 7 |   type StyleRelationship,
 8 |   type OptimizationReport
 9 | } from './style-library.js';
10 | 
11 | export {
12 |   GlobalStyleManager,
13 |   type GlobalVars
14 | } from './global-vars.js';
15 | 
16 | export {
17 |   StyleMerger,
18 |   type MergeCandidate
19 | } from './style-merger.js';
20 | 
```

--------------------------------------------------------------------------------
/src/extractors/colors/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // src/extractors/colors/index.mts
 2 | 
 3 | // Core functionality
 4 | export { ColorExtractor, extractThemeColors } from './core.js';
 5 | 
 6 | // Extractor functions
 7 | export {
 8 |     extractColorsFromThemeFrame,
 9 |     isTypographyNode
10 | } from './extractor.js';
11 | 
12 | // Types
13 | export type {
14 |     ThemeColor,
15 |     ColorDefinition,
16 |     ColorExtractionContext,
17 |     ColorExtractorFn,
18 |     ThemeGenerationOptions
19 | } from './types.js';
20 | 
```

--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export class Logger {
 2 |     static log(...args: any[]): void {
 3 |         console.error('[MCP Server]', ...args);
 4 |     }
 5 | 
 6 |     static error(...args: any[]): void {
 7 |         console.error('[MCP Server ERROR]', ...args);
 8 |     }
 9 | 
10 |     static warn(...args: any[]): void {
11 |         console.error('[MCP Server WARN]', ...args);
12 |     }
13 | 
14 |     static info(...args: any[]): void {
15 |         console.error('[MCP Server INFO]', ...args);
16 |     }
17 | }
18 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "ESNext",
 5 |     "moduleResolution": "node",
 6 |     "outDir": "./dist",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true,
12 |     "declaration": true,
13 |     "declarationMap": true,
14 |     "sourceMap": true,
15 |     "allowSyntheticDefaultImports": true
16 |   },
17 |   "include": [
18 |     "src/**/*"
19 |   ],
20 |   "exclude": [
21 |     "node_modules",
22 |     "dist"
23 |   ]
24 | }
```

--------------------------------------------------------------------------------
/src/extractors/typography/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // src/extractors/typography/index.mts
 2 | 
 3 | // Core functionality
 4 | export { TypographyExtractor, extractThemeTypography } from './core.js';
 5 | 
 6 | // Extractor functions
 7 | export {
 8 |     extractTypographyFromThemeFrame,
 9 |     isTypographyDemoNode
10 | } from './extractor.js';
11 | 
12 | // Types
13 | export type {
14 |     TypographyStyle,
15 |     TypographyDefinition,
16 |     TypographyExtractionContext,
17 |     TypographyExtractorFn,
18 |     TypographyExtractionOptions,
19 |     TypographyGenerationOptions,
20 |     FontWeightMapping,
21 |     TextStyleHash
22 | } from './types.js';
23 | 
```

--------------------------------------------------------------------------------
/src/tools/flutter/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // tools/flutter/index.mts
 2 | import type {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
 3 | import {registerFlutterAssetTools} from "./assets/assets.js";
 4 | import {registerSvgAssetTools} from "./assets/svg-assets.js";
 5 | import {registerComponentTools} from "./components/component-tool.js";
 6 | import {registerScreenTools} from "./screens/screen-tool.js";
 7 | 
 8 | export function registerFlutterTools(server: McpServer, figmaApiKey: string) {
 9 |     // Register Flutter asset management tools
10 |     registerFlutterAssetTools(server, figmaApiKey);
11 | 
12 |     // Register SVG asset management tools
13 |     registerSvgAssetTools(server, figmaApiKey);
14 | 
15 |     // Register component analysis tools
16 |     registerComponentTools(server, figmaApiKey);
17 | 
18 |     // Register screen analysis tools
19 |     registerScreenTools(server, figmaApiKey);
20 | }
21 | 
22 | 
```

--------------------------------------------------------------------------------
/src/extractors/screens/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // src/extractors/screens/index.mts
 2 | 
 3 | // Core functionality
 4 | export {
 5 |     ScreenExtractor,
 6 |     analyzeScreen
 7 | } from './core.js';
 8 | 
 9 | export {
10 |     extractScreenMetadata,
11 |     extractScreenLayoutInfo,
12 |     analyzeScreenSections,
13 |     extractNavigationInfo,
14 |     extractScreenAssets
15 | } from './extractor.js';
16 | 
17 | // Types
18 | export type {
19 |     ScreenAnalysis,
20 |     ScreenMetadata,
21 |     ScreenLayoutInfo,
22 |     ScreenSection,
23 |     NavigationInfo,
24 |     NavigationElement,
25 |     ScreenAssetInfo,
26 |     SkippedNodeInfo,
27 |     ScreenExtractionOptions
28 | } from './types.js';
29 | 
30 | // Convenience functions
31 | export {
32 |     parseComponentInput,
33 |     isValidNodeIdFormat,
34 |     extractIds,
35 |     generateFigmaUrl,
36 |     isFigmaUrl
37 | } from '../../utils/figma-url-parser.js';
38 | 
39 | // Re-export commonly used types from figma types
40 | export type {FigmaNode, FigmaColor, FigmaEffect} from '../../types/figma.js';
41 | 
```

--------------------------------------------------------------------------------
/src/extractors/colors/types.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // src/extractors/colors/types.mts
 2 | 
 3 | import type {FigmaNode} from '../../types/figma.js';
 4 | 
 5 | /**
 6 |  * Color definition from theme extraction
 7 |  */
 8 | export interface ThemeColor {
 9 |     name: string;
10 |     hex: string;
11 |     nodeId: string;
12 | }
13 | 
14 | /**
15 |  * Deduplicated color definition for design system
16 |  */
17 | export interface ColorDefinition {
18 |     id: string;
19 |     name: string;
20 |     value: string; // hex color
21 |     usage: 'primary' | 'secondary' | 'background' | 'text' | 'accent' | 'other';
22 |     usageCount: number;
23 | }
24 | 
25 | /**
26 |  * Color extraction context
27 |  */
28 | export interface ColorExtractionContext {
29 |     colorMap: Map<string, string>; // color value -> color ID
30 |     currentDepth: number;
31 |     maxDepth: number;
32 | }
33 | 
34 | /**
35 |  * Color extractor function type
36 |  */
37 | export type ColorExtractorFn = (
38 |     node: FigmaNode,
39 |     context: ColorExtractionContext,
40 |     colorLibrary: ColorDefinition[]
41 | ) => string[] | null; // Returns color IDs
42 | 
43 | /**
44 |  * Flutter theme generation options
45 |  */
46 | export interface ThemeGenerationOptions {
47 |     generateThemeData?: boolean;
48 |     includeColorScheme?: boolean;
49 |     includeMaterialColors?: boolean;
50 | }
51 | 
```

--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // src/tools/index.mts
 2 | import type {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
 3 | import {registerFlutterTools} from "./flutter/index.js";
 4 | import {registerThemeTools} from "./flutter/theme/colors/theme-tool.js";
 5 | import {registerTypographyTools} from "./flutter/theme/typography/typography-tool.js";
 6 | 
 7 | export function registerAllTools(server: McpServer, figmaApiKey: string) {
 8 |     console.error('🛠️ Tools Debug - Starting tool registration...');
 9 | 
10 |     // Register all tool categories
11 |     registerFlutterTools(server, figmaApiKey);
12 |     console.error('🛠️ Tools Debug - Flutter tools registered');
13 | 
14 |     registerThemeTools(server, figmaApiKey);
15 |     console.error('🛠️ Tools Debug - Theme tools registered');
16 | 
17 |     registerTypographyTools(server, figmaApiKey);
18 |     console.error('🛠️ Tools Debug - Typography tools registered');
19 | 
20 |     console.log("📋 Registered tool categories:");
21 |     console.log("  🚀 Flutter tools - Widgets, Screens");
22 |     console.log("  🏞️ Export assets - Images, SVGs");
23 |     console.log("  🎨 Theme tools - Colors, Typography");
24 |     console.log("  📝 Typography tools - Fonts, Sizes");
25 | 
26 |     console.error('🛠️ Tools Debug - All tools registration complete');
27 | }
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "figma-flutter-mcp",
 3 |   "version": "0.3.3",
 4 |   "description": "MCP server for Figma to Flutter conversion",
 5 |   "type": "module",
 6 |   "main": "dist/cli.js",
 7 |   "bin": {
 8 |     "figma-flutter-mcp": "dist/cli.js"
 9 |   },
10 |   "files": [
11 |     "dist/**/*",
12 |     "README.md",
13 |     "LICENSE.md"
14 |   ],
15 |   "scripts": {
16 |     "build": "tsc",
17 |     "start": "node dist/cli.js",
18 |     "dev": "tsx src/cli.ts --http",
19 |     "dev:stdio": "tsx src/cli.ts --stdio",
20 |     "dev:port": "tsx src/cli.ts --http --port",
21 |     "dev:remote": "tsx src/cli.ts --remote --port 3333",
22 |     "changeset": "changeset add",
23 |     "version": "changeset version && npm install --lockfile-only",
24 |     "release": "changeset publish"
25 |   },
26 |   "dependencies": {
27 |     "@modelcontextprotocol/sdk": "^1.0.0",
28 |     "cors": "^2.8.5",
29 |     "dotenv": "^17.2.1",
30 |     "express": "^4.18.2",
31 |     "node-fetch": "^3.3.2",
32 |     "yargs": "^17.7.2",
33 |     "zod": "^3.22.4"
34 |   },
35 |   "devDependencies": {
36 |     "@changesets/changelog-github": "^0.5.1",
37 |     "@changesets/cli": "^2.29.6",
38 |     "@types/cors": "^2.8.17",
39 |     "@types/express": "^4.17.21",
40 |     "@types/node": "^20.10.0",
41 |     "@types/node-fetch": "^2.6.9",
42 |     "@types/yargs": "^17.0.32",
43 |     "tsx": "^4.6.0",
44 |     "typescript": "^5.3.0"
45 |   },
46 |   "keywords": [
47 |     "figma",
48 |     "flutter",
49 |     "mcp",
50 |     "claude",
51 |     "design"
52 |   ],
53 |   "repository": "github:mhmzdev/figma-flutter-mcp",
54 |   "homepage": "https://github.com/mhmzdev/figma-flutter-mcp#readme",
55 |   "engines": {
56 |     "node": ">=18.0.0"
57 |   }
58 | }
```

--------------------------------------------------------------------------------
/src/extractors/components/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // src/extractors/components/index.mts
 2 | 
 3 | // Core functionality
 4 | export {
 5 |     ComponentExtractor,
 6 |     analyzeComponent,
 7 |     analyzeComponentWithVariants
 8 | } from './core.js';
 9 | 
10 | export {
11 |     extractMetadata,
12 |     extractLayoutInfo,
13 |     extractStylingInfo,
14 |     analyzeChildren,
15 |     createNestedComponentInfo,
16 |     createComponentChild,
17 |     calculateVisualImportance,
18 |     isComponentNode,
19 |     determineLayoutType,
20 |     hasPadding,
21 |     extractPadding,
22 |     convertFillToColorInfo,
23 |     convertStrokeInfo,
24 |     categorizeEffects,
25 |     extractCornerRadius,
26 |     extractBasicLayout,
27 |     extractBasicStyling,
28 |     extractTextInfo,
29 |     rgbaToHex
30 | } from './extractor.js';
31 | 
32 | export {VariantAnalyzer} from './variant-analyzer.js';
33 | 
34 | // Deduplicated extractor
35 | export {
36 |     DeduplicatedComponentExtractor,
37 |     type DeduplicatedComponentAnalysis,
38 |     type DeduplicatedComponentChild
39 | } from './deduplicated-extractor.js';
40 | 
41 | // Types
42 | export type {
43 |     ComponentAnalysis,
44 |     ComponentMetadata,
45 |     LayoutInfo,
46 |     StylingInfo,
47 |     ComponentChild,
48 |     NestedComponentInfo,
49 |     ComponentVariant,
50 |     SkippedNodeInfo,
51 |     CategorizedEffects,
52 |     DropShadowEffect,
53 |     InnerShadowEffect,
54 |     BlurEffect,
55 |     ColorInfo,
56 |     StrokeInfo,
57 |     CornerRadii,
58 |     PaddingInfo,
59 |     TextInfo,
60 |     ComponentExtractionOptions
61 | } from './types.js';
62 | 
63 | // Convenience functions
64 | export {
65 |     parseComponentInput,
66 |     isValidNodeIdFormat,
67 |     extractIds,
68 |     generateFigmaUrl,
69 |     isFigmaUrl
70 | } from '../../utils/figma-url-parser.js';
71 | 
72 | // Re-export commonly used types from figma types
73 | export type {FigmaNode, FigmaColor, FigmaEffect} from '../../types/figma.js';
```

--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: MCP Server Release
 2 | 
 3 | on:
 4 |   push:
 5 |     branches:
 6 |       - main
 7 | 
 8 | jobs:
 9 |   release:
10 |     name: Create a PR for release workflow
11 |     runs-on: ubuntu-latest
12 |     # Skip if commit message contains [skip ci], [ci skip], or docs:
13 |     if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]') && !startsWith(github.event.head_commit.message, 'docs:')"
14 |     
15 |     steps:
16 |       - uses: actions/checkout@v4
17 |         with:
18 |           fetch-depth: 0
19 |       
20 |       - name: Setup Node.js
21 |         uses: actions/setup-node@v4
22 |         with:
23 |           node-version: '20'
24 |       
25 |       - name: Install dependencies
26 |         run: npm install
27 |       
28 |       - name: Build the package
29 |         run: npm run build
30 |       
31 |       - name: Create Version PR or Publish to NPM
32 |         id: changesets
33 |         uses: changesets/action@v1
34 |         with:
35 |           commit: "chore(release): version packages"
36 |           title: "chore(release): version packages"
37 |           version: npm run version
38 |           publish: npm run release
39 |         env:
40 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41 |           NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
42 | 
43 |       - name: Create GitHub Release
44 |         if: steps.changesets.outputs.published == 'true'
45 |         uses: softprops/action-gh-release@v1
46 |         with:
47 |           tag_name: v${{ fromJson(steps.changesets.outputs.publishedPackages)[0].version }}
48 |           name: Release v${{ fromJson(steps.changesets.outputs.publishedPackages)[0].version }}
49 |           body: |
50 |             ## What's Changed
51 |             
52 |             ${{ fromJson(steps.changesets.outputs.publishedPackages)[0].changelog }}
53 |             
54 |             **Full Changelog**: https://github.com/mhmzdev/figma-flutter-mcp/compare/v${{ fromJson(steps.changesets.outputs.publishedPackages)[0].version }}...HEAD
55 |           draft: false
56 |           prerelease: false
57 |         env:
58 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------

```typescript
 1 | #!/usr/bin/env node
 2 | import {getServerConfig} from './config.js';
 3 | import {startMcpServer, startHttpServer} from './server.js';
 4 | 
 5 | async function startServer(): Promise<void> {
 6 |     const config = getServerConfig();
 7 | 
 8 |     if (config.isStdioMode) {
 9 |         await startMcpServer(config.figmaApiKey!);
10 |     } else if (config.isHttpMode) {
11 |         if (config.isRemoteMode) {
12 |             console.log('Starting Figma Flutter MCP Server in REMOTE mode...');
13 |             if (config.figmaApiKey) {
14 |                 console.log('✅ Server has fallback API key, but users can provide their own via:');
15 |             } else {
16 |                 console.log('⚠️  Users MUST provide their own Figma API keys via:');
17 |             }
18 |             console.log('  - Authorization header (Bearer token)');
19 |             console.log('  - X-Figma-Api-Key header');
20 |             console.log('  - figmaApiKey query parameter');
21 |             console.log('📝 Get API key: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens');
22 |         } else {
23 |             console.log('Starting Figma Flutter MCP Server in HTTP mode...');
24 |         }
25 |         await startHttpServer(config.httpPort, config.figmaApiKey);
26 |     } else {
27 |         console.log('Starting Figma Flutter MCP Server...');
28 |         console.log('⚠️  You must provide your Figma API key via:');
29 |         console.log('   • CLI argument: --figma-api-key=YOUR_KEY');
30 |         console.log('   • Environment: FIGMA_API_KEY=YOUR_KEY in .env file');
31 |         console.log('');
32 |         console.log('Available modes:');
33 |         console.log('  --stdio   MCP client communication (requires API key)');
34 |         console.log('  --http    Local testing via HTTP (requires API key)');
35 |         console.log('  --remote  Remote deployment (users provide keys via HTTP headers)');
36 |         console.log('');
37 |         console.log('📝 Get your API key: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens');
38 |         console.log('Use --help for more options');
39 |     }
40 | }
41 | 
42 | startServer().catch((error) => {
43 |     console.error("Failed to start server:", error);
44 |     process.exit(1);
45 | });
```

--------------------------------------------------------------------------------
/src/types/errors.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // types/errors.mts
 2 | import type {Response} from 'node-fetch';
 3 | 
 4 | export class FigmaError extends Error {
 5 |     constructor(message: string, public code?: string, public statusCode?: number) {
 6 |         super(message);
 7 |         this.name = 'FigmaError';
 8 |     }
 9 | }
10 | 
11 | export class FigmaAuthError extends FigmaError {
12 |     constructor(message: string = 'Invalid Figma access token') {
13 |         super(message, 'AUTH_ERROR', 401);
14 |         this.name = 'FigmaAuthError';
15 |     }
16 | }
17 | 
18 | export class FigmaNotFoundError extends FigmaError {
19 |     constructor(resource: string, id: string) {
20 |         super(`${resource} not found: ${id}`, 'NOT_FOUND', 404);
21 |         this.name = 'FigmaNotFoundError';
22 |     }
23 | }
24 | 
25 | export class FigmaRateLimitError extends FigmaError {
26 |     constructor(retryAfter?: number) {
27 |         super(`Rate limit exceeded${retryAfter ? `. Retry after ${retryAfter} seconds` : ''}`, 'RATE_LIMIT', 429);
28 |         this.name = 'FigmaRateLimitError';
29 |         this.retryAfter = retryAfter;
30 |     }
31 | 
32 |     public retryAfter?: number;
33 | }
34 | 
35 | export class FigmaNetworkError extends FigmaError {
36 |     constructor(message: string, public originalError?: Error) {
37 |         super(`Network error: ${message}`, 'NETWORK_ERROR');
38 |         this.name = 'FigmaNetworkError';
39 |     }
40 | }
41 | 
42 | export class FigmaParseError extends FigmaError {
43 |     constructor(message: string, public rawResponse?: any) {
44 |         super(`Failed to parse Figma response: ${message}`, 'PARSE_ERROR');
45 |         this.name = 'FigmaParseError';
46 |     }
47 | }
48 | 
49 | export function createFigmaError(response: Response, message?: string): FigmaError {
50 |     const defaultMessage = message || `Figma API error: ${response.status} ${response.statusText}`;
51 | 
52 |     switch (response.status) {
53 |         case 401:
54 |         case 403:
55 |             return new FigmaAuthError(defaultMessage);
56 |         case 404:
57 |             return new FigmaNotFoundError('Resource', 'unknown');
58 |         case 429:
59 |             const retryAfter = response.headers.get('Retry-After');
60 |             return new FigmaRateLimitError(retryAfter ? parseInt(retryAfter, 10) : undefined);
61 |         default:
62 |             return new FigmaError(defaultMessage, 'API_ERROR', response.status);
63 |     }
64 | }
```

--------------------------------------------------------------------------------
/src/extractors/screens/core.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // src/extractors/screens/core.mts
 2 | 
 3 | import type {FigmaNode} from '../../types/figma.js';
 4 | import type {
 5 |     ScreenAnalysis,
 6 |     ScreenExtractionOptions
 7 | } from './types.js';
 8 | import {
 9 |     extractScreenMetadata,
10 |     extractScreenLayoutInfo,
11 |     analyzeScreenSections,
12 |     extractNavigationInfo,
13 |     extractScreenAssets
14 | } from './extractor.js';
15 | 
16 | /**
17 |  * Screen extraction and analysis class
18 |  */
19 | export class ScreenExtractor {
20 |     private options: Required<ScreenExtractionOptions>;
21 | 
22 |     constructor(options: ScreenExtractionOptions = {}) {
23 |         this.options = {
24 |             maxSections: options.maxSections ?? 15,
25 |             maxDepth: options.maxDepth ?? 4,
26 |             includeHiddenNodes: options.includeHiddenNodes ?? false,
27 |             extractNavigation: options.extractNavigation ?? true,
28 |             extractAssets: options.extractAssets ?? true,
29 |             deviceTypeDetection: options.deviceTypeDetection ?? true
30 |         };
31 |     }
32 | 
33 |     /**
34 |      * Main screen analysis method
35 |      */
36 |     async analyzeScreen(node: FigmaNode): Promise<ScreenAnalysis> {
37 |         const metadata = extractScreenMetadata(node);
38 |         const layout = extractScreenLayoutInfo(node);
39 |         
40 |         const {sections, components, skippedNodes} = analyzeScreenSections(node, this.options);
41 |         
42 |         const navigation = this.options.extractNavigation 
43 |             ? extractNavigationInfo(node)
44 |             : { navigationElements: [] };
45 |             
46 |         const assets = this.options.extractAssets 
47 |             ? extractScreenAssets(node)
48 |             : [];
49 | 
50 |         return {
51 |             metadata,
52 |             layout,
53 |             sections,
54 |             components,
55 |             navigation,
56 |             assets,
57 |             skippedNodes: skippedNodes.length > 0 ? skippedNodes : undefined
58 |         };
59 |     }
60 | 
61 |     /**
62 |      * Get extractor options
63 |      */
64 |     getOptions(): Required<ScreenExtractionOptions> {
65 |         return this.options;
66 |     }
67 | }
68 | 
69 | /**
70 |  * Convenience function to analyze a screen
71 |  */
72 | export async function analyzeScreen(
73 |     node: FigmaNode,
74 |     options: ScreenExtractionOptions = {}
75 | ): Promise<ScreenAnalysis> {
76 |     const extractor = new ScreenExtractor(options);
77 |     return extractor.analyzeScreen(node);
78 | }
79 | 
```

--------------------------------------------------------------------------------
/src/extractors/typography/types.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // src/extractors/typography/types.mts
 2 | 
 3 | import type {FigmaNode} from '../../types/figma.js';
 4 | 
 5 | /**
 6 |  * Typography style definition from theme extraction
 7 |  */
 8 | export interface TypographyStyle {
 9 |     name: string;
10 |     fontFamily: string;
11 |     fontSize: number;
12 |     fontWeight: number;
13 |     lineHeight: number;
14 |     letterSpacing: number;
15 |     nodeId: string;
16 |     // Optional properties for enhanced text styles
17 |     textAlign?: string;
18 |     textDecoration?: string;
19 | }
20 | 
21 | /**
22 |  * Deduplicated typography definition for design system
23 |  */
24 | export interface TypographyDefinition {
25 |     id: string;
26 |     name: string;
27 |     fontFamily: string;
28 |     fontSize: number;
29 |     fontWeight: number;
30 |     lineHeight: number;
31 |     letterSpacing: number;
32 |     usage: 'heading' | 'body' | 'caption' | 'button' | 'label' | 'other';
33 |     usageCount: number;
34 |     // Additional Flutter-specific properties
35 |     dartName?: string; // Dart-safe property name
36 | }
37 | 
38 | /**
39 |  * Typography extraction context
40 |  */
41 | export interface TypographyExtractionContext {
42 |     typographyMap: Map<string, string>; // style hash -> typography ID
43 |     currentDepth: number;
44 |     maxDepth: number;
45 | }
46 | 
47 | /**
48 |  * Typography extractor function type
49 |  */
50 | export type TypographyExtractorFn = (
51 |     node: FigmaNode,
52 |     context: TypographyExtractionContext,
53 |     typographyLibrary: TypographyDefinition[]
54 | ) => string[] | null; // Returns typography IDs
55 | 
56 | /**
57 |  * Typography extraction options
58 |  */
59 | export interface TypographyExtractionOptions {
60 |     maxDepth?: number;
61 |     includeHiddenText?: boolean;
62 |     minUsageCount?: number;
63 |     excludeEmptyText?: boolean;
64 | }
65 | 
66 | /**
67 |  * Flutter typography generation options
68 |  */
69 | export interface TypographyGenerationOptions {
70 |     generateAppText?: boolean;
71 |     generateTextTheme?: boolean;
72 |     familyVariableName?: string; // Name for shared font family variable
73 |     includeLineHeight?: boolean;
74 |     includeLetterSpacing?: boolean;
75 | }
76 | 
77 | /**
78 |  * Font weight mapping for Flutter
79 |  */
80 | export interface FontWeightMapping {
81 |     [key: number]: string; // Figma weight -> Flutter FontWeight
82 | }
83 | 
84 | /**
85 |  * Text style hash components for deduplication
86 |  */
87 | export interface TextStyleHash {
88 |     fontFamily: string;
89 |     fontSize: number;
90 |     fontWeight: number;
91 |     lineHeight: number;
92 |     letterSpacing: number;
93 | }
94 | 
```

--------------------------------------------------------------------------------
/docs/cursor_rules_example.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Flutter & Dart LLM Assistant Prompt
 2 | 
 3 | You are a **senior Flutter & Dart developer** experienced in building **production-ready apps**. You follow best practices, performance optimization techniques, and clean, maintainable architecture.
 4 | 
 5 | ## Responsibilities
 6 | 
 7 | 1. Help me **write, refactor, or debug** Dart and Flutter code.
 8 | 2. Explain code and concepts **clearly and concisely**.
 9 | 3. Follow **best practices**: null safety, widget structure, idiomatic Dart, and clean state management.
10 | 4. Prefer **private widget classes over function widgets**.
11 | 5. Always give **code examples** when needed, and provide **short, meaningful explanations**.
12 | 6. For any feature you write that involves business logic or architecture, create a Markdown documentation file in `docs/` explaining the purpose. Update this file if you make changes in any of the related feature and search docs before applying feature to use the existing ones as your context
13 | 7. Use `///` comments to document each function, focusing on the **“why”**, not the “how”.
14 | 8. When unsure, **ask clarifying questions before coding**.
15 | 
16 | ## Architecture Guidelines
17 | 
18 | ### Widgets
19 | 
20 | - Avoid function-based widgets like `Widget _someWidget() => Container();`
21 | - Use **private widget classes** within their screen directories:
22 | 
23 |   ```
24 |   lib/ui/screens/login/widgets/_body.dart  => class _Body extends StatelessWidget
25 |   lib/ui/screens/login/widgets/_header.dart => class _Header extends StatelessWidget
26 |   ```
27 | 
28 | - If a widget is reused across multiple screens, extract and place it under:
29 | 
30 |   ```
31 |   lib/ui/widgets/
32 |   ```
33 | 
34 | - Use named constructors for variants, but:
35 |   - Make sure to have variants as `Enum` not `String` type.
36 |   - Keep the variants style in separate files and use `part` and `part of` declarative to connect files
37 | 
38 | ## Coding Conventions
39 | 
40 | - Write methods as `getters` if they don't take any parameters and simply return a value.
41 | - UI files should not exceed **200–250 lines**.
42 |   - Break files using `part` / `part of`.
43 | - No single function should exceed **30–50 lines**. Refactor into smaller helpers if needed.
44 | 
45 | ## Enum
46 | - Use `Enum` instead of `String` where needed
47 | - Write `bool` extensions getters for enums every time
48 | 
49 | For example:
50 | ```
51 | enum SomeEnumType {
52 |     type1,
53 |     type2,
54 | }
55 | 
56 | extenion SomeEnumTypeX on SomeEnumType {
57 |     bool get isType1 => this == type1;
58 |     bool get isType2 => this == type2;
59 | }
60 | ```
```

--------------------------------------------------------------------------------
/src/utils/retry.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // utils/retry.mts
 2 | 
 3 | import {FigmaError, FigmaRateLimitError, FigmaNetworkError} from '../types/errors.js';
 4 | 
 5 | export interface RetryOptions {
 6 |     maxAttempts?: number;
 7 |     initialDelayMs?: number;
 8 |     maxDelayMs?: number;
 9 |     backoffMultiplier?: number;
10 |     retryableErrors?: (error: Error) => boolean;
11 | }
12 | 
13 | export interface RetryState {
14 |     attempt: number;
15 |     totalElapsed: number;
16 |     lastError?: Error;
17 | }
18 | 
19 | const DEFAULT_OPTIONS: Required<RetryOptions> = {
20 |     maxAttempts: 3,
21 |     initialDelayMs: 1000,
22 |     maxDelayMs: 30000,
23 |     backoffMultiplier: 2,
24 |     retryableErrors: (error: Error): boolean => {
25 |         // Retry on network errors and rate limits, but not auth or parse errors
26 |         return (error instanceof FigmaNetworkError) ||
27 |             (error instanceof FigmaRateLimitError) ||
28 |             (error instanceof FigmaError && error.statusCode && error.statusCode >= 500) || false;
29 |     }
30 | };
31 | 
32 | export async function withRetry<T>(
33 |     operation: () => Promise<T>,
34 |     options: RetryOptions = {}
35 | ): Promise<T> {
36 |     const opts = {...DEFAULT_OPTIONS, ...options};
37 |     let lastError: Error;
38 | 
39 |     for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
40 |         try {
41 |             return await operation();
42 |         } catch (error) {
43 |             lastError = error as Error;
44 | 
45 |             // Don't retry if this is the last attempt
46 |             if (attempt === opts.maxAttempts) {
47 |                 break;
48 |             }
49 | 
50 |             // Don't retry if error is not retryable
51 |             if (!opts.retryableErrors(lastError)) {
52 |                 break;
53 |             }
54 | 
55 |             // Calculate delay
56 |             let delay = opts.initialDelayMs * Math.pow(opts.backoffMultiplier, attempt - 1);
57 | 
58 |             // Handle rate limit specific delay
59 |             if (lastError instanceof FigmaRateLimitError && lastError.retryAfter) {
60 |                 delay = lastError.retryAfter * 1000; // Convert to ms
61 |             }
62 | 
63 |             // Cap the delay
64 |             delay = Math.min(delay, opts.maxDelayMs);
65 | 
66 |             console.log(`Attempt ${attempt} failed: ${lastError.message}. Retrying in ${delay}ms...`);
67 | 
68 |             await sleep(delay);
69 |         }
70 |     }
71 | 
72 |     throw lastError!;
73 | }
74 | 
75 | function sleep(ms: number): Promise<void> {
76 |     return new Promise(resolve => setTimeout(resolve, ms));
77 | }
78 | 
79 | // Helper function for logging retry state
80 | export function logRetryAttempt(state: RetryState, error: Error): void {
81 |     console.warn(`Retry attempt ${state.attempt} failed after ${state.totalElapsed}ms: ${error.message}`);
82 | }
```

--------------------------------------------------------------------------------
/src/extractors/screens/types.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // src/extractors/screens/types.mts
  2 | 
  3 | import type {FigmaNode, FigmaColor} from '../../types/figma.js';
  4 | import type {ComponentChild, NestedComponentInfo, LayoutInfo, StylingInfo} from '../components/types.js';
  5 | 
  6 | /**
  7 |  * Main screen analysis result
  8 |  */
  9 | export interface ScreenAnalysis {
 10 |     metadata: ScreenMetadata;
 11 |     layout: ScreenLayoutInfo;
 12 |     sections: ScreenSection[];
 13 |     components: NestedComponentInfo[];
 14 |     navigation: NavigationInfo;
 15 |     assets: ScreenAssetInfo[];
 16 |     skippedNodes?: SkippedNodeInfo[];
 17 | }
 18 | 
 19 | /**
 20 |  * Screen metadata information
 21 |  */
 22 | export interface ScreenMetadata {
 23 |     name: string;
 24 |     type: 'FRAME' | 'PAGE' | 'COMPONENT';
 25 |     nodeId: string;
 26 |     description?: string;
 27 |     deviceType?: 'mobile' | 'tablet' | 'desktop' | 'unknown';
 28 |     orientation?: 'portrait' | 'landscape';
 29 |     dimensions: {
 30 |         width: number;
 31 |         height: number;
 32 |     };
 33 | }
 34 | 
 35 | /**
 36 |  * Screen layout information optimized for full screens
 37 |  */
 38 | export interface ScreenLayoutInfo extends LayoutInfo {
 39 |     scrollable?: boolean;
 40 |     hasHeader?: boolean;
 41 |     hasFooter?: boolean;
 42 |     hasNavigation?: boolean;
 43 |     contentArea?: {
 44 |         x: number;
 45 |         y: number;
 46 |         width: number;
 47 |         height: number;
 48 |     };
 49 | }
 50 | 
 51 | /**
 52 |  * Screen section (header, content, footer, etc.)
 53 |  */
 54 | export interface ScreenSection {
 55 |     id: string;
 56 |     name: string;
 57 |     type: 'header' | 'navigation' | 'content' | 'footer' | 'sidebar' | 'modal' | 'other';
 58 |     nodeId: string;
 59 |     layout: Partial<LayoutInfo>;
 60 |     styling?: Partial<StylingInfo>;
 61 |     children: ComponentChild[];
 62 |     components: NestedComponentInfo[];
 63 |     importance: number; // 1-10 score
 64 | }
 65 | 
 66 | /**
 67 |  * Navigation information
 68 |  */
 69 | export interface NavigationInfo {
 70 |     hasTabBar?: boolean;
 71 |     hasAppBar?: boolean;
 72 |     hasDrawer?: boolean;
 73 |     hasBottomSheet?: boolean;
 74 |     navigationElements: NavigationElement[];
 75 | }
 76 | 
 77 | /**
 78 |  * Navigation element
 79 |  */
 80 | export interface NavigationElement {
 81 |     nodeId: string;
 82 |     name: string;
 83 |     type: 'tab' | 'button' | 'link' | 'icon' | 'menu' | 'other';
 84 |     text?: string;
 85 |     icon?: boolean;
 86 |     isActive?: boolean;
 87 | }
 88 | 
 89 | /**
 90 |  * Screen asset information
 91 |  */
 92 | export interface ScreenAssetInfo {
 93 |     nodeId: string;
 94 |     name: string;
 95 |     type: 'image' | 'icon' | 'illustration' | 'background';
 96 |     size: 'small' | 'medium' | 'large';
 97 |     usage: 'decorative' | 'content' | 'navigation' | 'branding';
 98 | }
 99 | 
100 | /**
101 |  * Information about nodes that were skipped
102 |  */
103 | export interface SkippedNodeInfo {
104 |     nodeId: string;
105 |     name: string;
106 |     type: string;
107 |     reason: 'depth_limit' | 'complexity' | 'max_sections' | 'device_ui_element';
108 | }
109 | 
110 | /**
111 |  * Screen extraction options
112 |  */
113 | export interface ScreenExtractionOptions {
114 |     maxSections?: number;
115 |     maxDepth?: number;
116 |     includeHiddenNodes?: boolean;
117 |     extractNavigation?: boolean;
118 |     extractAssets?: boolean;
119 |     deviceTypeDetection?: boolean;
120 | }
121 | 
```

--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
 1 | # figma-flutter-mcp
 2 | 
 3 | ## 0.3.3
 4 | 
 5 | ### Patch Changes
 6 | 
 7 | - [#42](https://github.com/mhmzdev/figma-flutter-mcp/pull/42) [`869314e`](https://github.com/mhmzdev/figma-flutter-mcp/commit/869314e16a2a393ddba22383a8e576161901ab71) Thanks [@mhmzdev](https://github.com/mhmzdev)! - feat: implement advanced style deduplication with semantic matching and auto-optimization
 8 | 
 9 | ## 0.3.2
10 | 
11 | ### Patch Changes
12 | 
13 | - [#40](https://github.com/mhmzdev/figma-flutter-mcp/pull/40) [`287dbcc`](https://github.com/mhmzdev/figma-flutter-mcp/commit/287dbcc3a25040fe0f1f05aaf6374472683b8cba) Thanks [@mhmzdev](https://github.com/mhmzdev)! - StreamableHTTP and API key setup improved
14 | 
15 | ## 0.3.1
16 | 
17 | ### Patch Changes
18 | 
19 | - [#38](https://github.com/mhmzdev/figma-flutter-mcp/pull/38) [`089b764`](https://github.com/mhmzdev/figma-flutter-mcp/commit/089b7647d5e4e37452b973ba521fb3bc878427dd) Thanks [@mhmzdev](https://github.com/mhmzdev)! - Docs updated with more understanding
20 | 
21 | ## 0.3.0
22 | 
23 | ### Minor Changes
24 | 
25 | - [#36](https://github.com/mhmzdev/figma-flutter-mcp/pull/36) [`d3e64e8`](https://github.com/mhmzdev/figma-flutter-mcp/commit/d3e64e8c3a64d07deed0c2102b79bd9e2c51cfc3) Thanks [@mhmzdev](https://github.com/mhmzdev)! - Migrated .mts to .ts, added semantic detection and visual context approaches
26 | 
27 | ## 0.2.0
28 | 
29 | ### Minor Changes
30 | 
31 | - [#33](https://github.com/mhmzdev/figma-flutter-mcp/pull/33) [`990dfc6`](https://github.com/mhmzdev/figma-flutter-mcp/commit/990dfc6070a64131182df9371ad94b7d5fb114ab) Thanks [@mhmzdev](https://github.com/mhmzdev)! - Break-even point: ~22 component analyses
32 | 
33 | ## 0.1.8
34 | 
35 | ### Patch Changes
36 | 
37 | - [#28](https://github.com/mhmzdev/figma-flutter-mcp/pull/28) [`cc3c184`](https://github.com/mhmzdev/figma-flutter-mcp/commit/cc3c18489fc40bb9849671710b0b97dd95bc2a31) Thanks [@mhmzdev](https://github.com/mhmzdev)! - Getting started docs added, Demo video added
38 | 
39 | ## 0.1.7
40 | 
41 | ### Patch Changes
42 | 
43 | - [#25](https://github.com/mhmzdev/figma-flutter-mcp/pull/25) [`4d438a9`](https://github.com/mhmzdev/figma-flutter-mcp/commit/4d438a95f6ba703a971b2c5dceb0af8d245d78c8) Thanks [@mhmzdev](https://github.com/mhmzdev)! - Docs updated for local setup
44 | 
45 | ## 0.1.6
46 | 
47 | ### Patch Changes
48 | 
49 | - [#22](https://github.com/mhmzdev/figma-flutter-mcp/pull/22) [`fffb12a`](https://github.com/mhmzdev/figma-flutter-mcp/commit/fffb12ab10b281f632a6506c5cb5053a039c57e7) Thanks [@mhmzdev](https://github.com/mhmzdev)! - Figma API key logic improved for API access
50 | 
51 | ## 0.1.5
52 | 
53 | ### Patch Changes
54 | 
55 | - [`611ee64`](https://github.com/mhmzdev/figma-flutter-mcp/commit/611ee646d684925808ae9191ba851c9a86cb1d7b) Thanks [@mhmzdev](https://github.com/mhmzdev)! - Removed empty files so that AI model doesn't get confused
56 | 
57 | ## 0.1.4
58 | 
59 | ### Patch Changes
60 | 
61 | - [#19](https://github.com/mhmzdev/figma-flutter-mcp/pull/19) [`5226c25`](https://github.com/mhmzdev/figma-flutter-mcp/commit/5226c250a19b96d4664ab80805f3efac7c0c4b75) Thanks [@mhmzdev](https://github.com/mhmzdev)! - Fixed the TYPO for API key that was causing a crash
62 | 
63 | ## 0.1.3
64 | 
65 | ### Patch Changes
66 | 
67 | - [#15](https://github.com/mhmzdev/figma-flutter-mcp/pull/15) [`692dd43`](https://github.com/mhmzdev/figma-flutter-mcp/commit/692dd43b364f50055cf577715dd7921e430f05a1) Thanks [@mhmzdev](https://github.com/mhmzdev)! - Added workflows for better CI/CD
68 | 
```

--------------------------------------------------------------------------------
/src/extractors/components/core.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // src/extractors/components/core.mts
  2 | 
  3 | import type {FigmaNode} from '../../types/figma.js';
  4 | import type {
  5 |     ComponentAnalysis,
  6 |     ComponentExtractionOptions,
  7 |     ComponentVariant
  8 | } from './types.js';
  9 | import {
 10 |     extractMetadata,
 11 |     extractLayoutInfo,
 12 |     extractStylingInfo,
 13 |     analyzeChildren,
 14 | } from './extractor.js';
 15 | 
 16 | /**
 17 |  * Component extraction and analysis class
 18 |  */
 19 | export class ComponentExtractor {
 20 |     private options: Required<ComponentExtractionOptions>;
 21 | 
 22 |     constructor(options: ComponentExtractionOptions = {}) {
 23 |         this.options = {
 24 |             maxChildNodes: options.maxChildNodes ?? 10,
 25 |             maxDepth: options.maxDepth ?? 3,
 26 |             includeHiddenNodes: options.includeHiddenNodes ?? false,
 27 |             prioritizeComponents: options.prioritizeComponents ?? true,
 28 |             extractTextContent: options.extractTextContent ?? true
 29 |         };
 30 |     }
 31 | 
 32 |     /**
 33 |      * Main component analysis method
 34 |      */
 35 |     async analyzeComponent(node: FigmaNode, userDefinedAsComponent: boolean = false): Promise<ComponentAnalysis> {
 36 |         const metadata = extractMetadata(node, userDefinedAsComponent);
 37 |         const layout = extractLayoutInfo(node);
 38 |         const styling = extractStylingInfo(node);
 39 | 
 40 |         const {children, nestedComponents, skippedNodes} = analyzeChildren(node, this.options);
 41 | 
 42 |         return {
 43 |             metadata,
 44 |             layout,
 45 |             styling,
 46 |             children,
 47 |             nestedComponents,
 48 |             skippedNodes: skippedNodes.length > 0 ? skippedNodes : undefined
 49 |         };
 50 |     }
 51 | 
 52 |     /**
 53 |      * Get extractor options
 54 |      */
 55 |     getOptions(): Required<ComponentExtractionOptions> {
 56 |         return this.options;
 57 |     }
 58 | }
 59 | 
 60 | /**
 61 |  * Convenience function to analyze a single component
 62 |  */
 63 | export async function analyzeComponent(
 64 |     node: FigmaNode,
 65 |     options: ComponentExtractionOptions = {},
 66 |     userDefinedAsComponent: boolean = false
 67 | ): Promise<ComponentAnalysis> {
 68 |     const extractor = new ComponentExtractor(options);
 69 |     return extractor.analyzeComponent(node, userDefinedAsComponent);
 70 | }
 71 | 
 72 | /**
 73 |  * Convenience function to analyze component with variants
 74 |  */
 75 | export async function analyzeComponentWithVariants(
 76 |     componentSetNode: FigmaNode,
 77 |     options: ComponentExtractionOptions = {}
 78 | ): Promise<{
 79 |     variants: ComponentVariant[];
 80 |     defaultAnalysis?: ComponentAnalysis;
 81 | }> {
 82 |     if (componentSetNode.type !== 'COMPONENT_SET') {
 83 |         throw new Error('Node is not a COMPONENT_SET');
 84 |     }
 85 | 
 86 |     const {VariantAnalyzer} = await import('./variant-analyzer.js');
 87 |     const variantAnalyzer = new VariantAnalyzer();
 88 |     const variants = await variantAnalyzer.analyzeComponentSet(componentSetNode);
 89 | 
 90 |     // Find and analyze the default variant
 91 |     const defaultVariant = variants.find(v => v.isDefault);
 92 |     let defaultAnalysis: ComponentAnalysis | undefined;
 93 | 
 94 |     if (defaultVariant && componentSetNode.children) {
 95 |         const defaultVariantNode = componentSetNode.children.find(child => child.id === defaultVariant.nodeId);
 96 |         if (defaultVariantNode) {
 97 |             const extractor = new ComponentExtractor(options);
 98 |             defaultAnalysis = await extractor.analyzeComponent(defaultVariantNode, false);
 99 |         }
100 |     }
101 | 
102 |     return {
103 |         variants,
104 |         defaultAnalysis
105 |     };
106 | }
107 | 
```

--------------------------------------------------------------------------------
/src/tools/flutter/theme/colors/theme-generator.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // src/tools/flutter/simple-theme-generator.mts
  2 | import {writeFile, mkdir} from 'fs/promises';
  3 | import {join} from 'path';
  4 | import type {ThemeColor, ThemeGenerationOptions} from '../../../../extractors/colors/index.js';
  5 | 
  6 | export class SimpleThemeGenerator {
  7 |     /**
  8 |      * Generate AppColors Dart class from theme colors
  9 |      */
 10 |     async generateAppColors(colors: ThemeColor[], outputPath: string, options: ThemeGenerationOptions = {}): Promise<string> {
 11 |         // Create output directory
 12 |         await mkdir(outputPath, {recursive: true});
 13 |         const filePath = join(outputPath, 'app_colors.dart');
 14 | 
 15 |         // Generate Dart content
 16 |         const content = this.generateDartContent(colors);
 17 | 
 18 |         await writeFile(filePath, content);
 19 | 
 20 |         // Generate ThemeData if requested
 21 |         if (options.generateThemeData) {
 22 |             await this.generateThemeData(colors, outputPath, options);
 23 |         }
 24 | 
 25 |         return filePath;
 26 |     }
 27 | 
 28 |     /**
 29 |      * Generate Flutter ThemeData from theme colors
 30 |      */
 31 |     async generateThemeData(colors: ThemeColor[], outputPath: string, options: ThemeGenerationOptions = {}): Promise<string> {
 32 |         const filePath = join(outputPath, 'app_theme.dart');
 33 |         const content = this.generateThemeDataContent(colors, options);
 34 | 
 35 |         await writeFile(filePath, content);
 36 |         return filePath;
 37 |     }
 38 | 
 39 |     private generateDartContent(colors: ThemeColor[]): string {
 40 | 
 41 |         let content = `// Generated AppColors from Figma theme frame
 42 | 
 43 | import 'package:flutter/material.dart';
 44 | 
 45 | class AppColors {
 46 | `;
 47 | 
 48 |         // Generate color constants
 49 |         colors.forEach(color => {
 50 |             const constantName = this.toDartConstantName(color.name);
 51 |             content += `  /// ${color.name}
 52 |   static const Color ${constantName} = Color(0xFF${color.hex.substring(1)});
 53 | 
 54 | `;
 55 |         });
 56 | 
 57 |         content += `}\n`;
 58 |         return content;
 59 |     }
 60 | 
 61 |     private generateThemeDataContent(colors: ThemeColor[], options: ThemeGenerationOptions): string {
 62 |         const timestamp = new Date().toISOString().split('T')[0];
 63 |         const colorMap = this.createColorMap(colors);
 64 | 
 65 |         let content = `// Generated Flutter ThemeData from Figma theme frame
 66 | 
 67 | import 'package:flutter/material.dart';
 68 | import 'app_colors.dart';
 69 | 
 70 | class AppTheme {
 71 |   // Light Theme
 72 |   static ThemeData get lightTheme {
 73 |     return ThemeData(
 74 |       useMaterial3: true,
 75 |       brightness: Brightness.light,
 76 | `;
 77 | 
 78 |         // Add ColorScheme if requested
 79 |         if (options.includeColorScheme !== false) {
 80 |             content += this.generateColorScheme(colorMap);
 81 |         }
 82 | 
 83 |         content += `    );
 84 |   }
 85 | }
 86 | `;
 87 | 
 88 |         return content;
 89 |     }
 90 | 
 91 |     private createColorMap(colors: ThemeColor[]): Map<string, string> {
 92 |         const colorMap = new Map<string, string>();
 93 | 
 94 |         colors.forEach(color => {
 95 |             const key = color.name.toLowerCase().replace(/\s+/g, '');
 96 |             colorMap.set(key, `AppColors.${this.toDartConstantName(color.name)}`);
 97 |         });
 98 | 
 99 |         return colorMap;
100 |     }
101 | 
102 |     private generateColorScheme(colorMap: Map<string, string>): string {
103 |         const primary = colorMap.get('primary') || 'AppColors.primary';
104 |         const secondary = colorMap.get('secondary') || colorMap.get('accent') || 'Colors.blue.shade300';
105 |         const background = colorMap.get('background') || colorMap.get('backgroundlight') || 'Colors.white';
106 |         const surface = colorMap.get('surface') || background;
107 |         const error = colorMap.get('error') || colorMap.get('danger') || 'Colors.red';
108 | 
109 |         return `      colorScheme: ColorScheme.fromSeed(
110 |         seedColor: ${primary},
111 |         brightness: Brightness.light,
112 |         primary: ${primary},
113 |         secondary: ${secondary},
114 |         background: ${background},
115 |         surface: ${surface},
116 |         error: ${error},
117 |       ),
118 | `;
119 |     }
120 | 
121 |     private toDartConstantName(name: string): string {
122 |         // Convert to camelCase for Dart constants
123 |         return name
124 |             .replace(/[^a-zA-Z0-9]/g, ' ')
125 |             .split(' ')
126 |             .filter(word => word.length > 0)
127 |             .map((word, index) => {
128 |                 if (index === 0) {
129 |                     return word.toLowerCase();
130 |                 }
131 |                 return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
132 |             })
133 |             .join('');
134 |     }
135 | }
```

--------------------------------------------------------------------------------
/src/extractors/components/types.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // src/extractors/components/types.mts
  2 | 
  3 | import type {FigmaNode, FigmaColor, FigmaEffect} from '../../types/figma.js';
  4 | 
  5 | /**
  6 |  * Main component analysis result
  7 |  */
  8 | export interface ComponentAnalysis {
  9 |     metadata: ComponentMetadata;
 10 |     layout: LayoutInfo;
 11 |     styling: StylingInfo;
 12 |     children: ComponentChild[];
 13 |     nestedComponents: NestedComponentInfo[];
 14 |     variants?: ComponentVariant[];
 15 |     skippedNodes?: SkippedNodeInfo[];
 16 | }
 17 | 
 18 | /**
 19 |  * Component metadata information
 20 |  */
 21 | export interface ComponentMetadata {
 22 |     name: string;
 23 |     type: 'COMPONENT' | 'COMPONENT_SET' | 'FRAME';
 24 |     nodeId: string;
 25 |     description?: string;
 26 |     variantCount?: number;
 27 |     isUserDefinedComponent?: boolean; // When user treats FRAME as component
 28 |     componentKey?: string; // For actual Figma components
 29 | }
 30 | 
 31 | /**
 32 |  * Layout structure information
 33 |  */
 34 | export interface LayoutInfo {
 35 |     type: 'auto-layout' | 'absolute' | 'frame';
 36 |     direction?: 'horizontal' | 'vertical';
 37 |     spacing?: number;
 38 |     padding?: PaddingInfo;
 39 |     constraints?: any;
 40 |     dimensions: {
 41 |         width: number;
 42 |         height: number;
 43 |     };
 44 |     alignItems?: string;
 45 |     justifyContent?: string;
 46 | }
 47 | 
 48 | /**
 49 |  * Padding information
 50 |  */
 51 | export interface PaddingInfo {
 52 |     top: number;
 53 |     right: number;
 54 |     bottom: number;
 55 |     left: number;
 56 |     isUniform: boolean;
 57 | }
 58 | 
 59 | /**
 60 |  * Visual styling information
 61 |  */
 62 | export interface StylingInfo {
 63 |     fills?: ColorInfo[];
 64 |     strokes?: StrokeInfo[];
 65 |     effects?: CategorizedEffects;
 66 |     cornerRadius?: number | CornerRadii;
 67 |     opacity?: number;
 68 | }
 69 | 
 70 | /**
 71 |  * Color information extracted from fills
 72 |  */
 73 | export interface ColorInfo {
 74 |     type: string;
 75 |     color?: FigmaColor;
 76 |     hex?: string;
 77 |     opacity?: number;
 78 |     gradientStops?: Array<{
 79 |         color: FigmaColor;
 80 |         position: number;
 81 |     }>;
 82 | }
 83 | 
 84 | /**
 85 |  * Stroke information
 86 |  */
 87 | export interface StrokeInfo {
 88 |     type: string;
 89 |     color: FigmaColor;
 90 |     hex: string;
 91 |     weight: number;
 92 |     align?: string;
 93 | }
 94 | 
 95 | /**
 96 |  * Corner radius information
 97 |  */
 98 | export interface CornerRadii {
 99 |     topLeft: number;
100 |     topRight: number;
101 |     bottomLeft: number;
102 |     bottomRight: number;
103 |     isUniform: boolean;
104 | }
105 | 
106 | /**
107 |  * Categorized effects for Flutter mapping
108 |  */
109 | export interface CategorizedEffects {
110 |     dropShadows: DropShadowEffect[];
111 |     innerShadows: InnerShadowEffect[];
112 |     blurs: BlurEffect[];
113 | }
114 | 
115 | /**
116 |  * Drop shadow effect
117 |  */
118 | export interface DropShadowEffect {
119 |     color: FigmaColor;
120 |     hex: string;
121 |     offset: {x: number; y: number};
122 |     radius: number;
123 |     spread?: number;
124 |     opacity: number;
125 | }
126 | 
127 | /**
128 |  * Inner shadow effect
129 |  */
130 | export interface InnerShadowEffect {
131 |     color: FigmaColor;
132 |     hex: string;
133 |     offset: {x: number; y: number};
134 |     radius: number;
135 |     spread?: number;
136 |     opacity: number;
137 | }
138 | 
139 | /**
140 |  * Blur effect
141 |  */
142 | export interface BlurEffect {
143 |     type: string;
144 |     radius: number;
145 | }
146 | 
147 | /**
148 |  * Child component information
149 |  */
150 | export interface ComponentChild {
151 |     nodeId: string;
152 |     name: string;
153 |     type: string;
154 |     isNestedComponent: boolean;
155 |     visualImportance: number; // 1-10 score for prioritization
156 |     basicInfo?: {
157 |         layout?: Partial<LayoutInfo>;
158 |         styling?: Partial<StylingInfo>;
159 |         text?: TextInfo;
160 |     };
161 | }
162 | 
163 | /**
164 |  * Enhanced text-specific information
165 |  */
166 | export interface TextInfo {
167 |     content: string;
168 |     isPlaceholder: boolean;
169 |     fontFamily?: string;
170 |     fontSize?: number;
171 |     fontWeight?: number;
172 |     textAlign?: string;
173 |     textCase?: 'uppercase' | 'lowercase' | 'capitalize' | 'sentence' | 'mixed';
174 |     semanticType?: 'heading' | 'body' | 'label' | 'button' | 'link' | 'caption' | 'error' | 'success' | 'warning' | 'other';
175 |     placeholder?: boolean; // Flag for Flutter implementation
176 | }
177 | 
178 | /**
179 |  * Nested component that should be analyzed separately
180 |  */
181 | export interface NestedComponentInfo {
182 |     nodeId: string;
183 |     name: string;
184 |     componentKey?: string;
185 |     masterComponent?: string;
186 |     isComponentInstance: boolean;
187 |     needsSeparateAnalysis: boolean;
188 |     instanceType?: 'COMPONENT' | 'COMPONENT_SET';
189 | }
190 | 
191 | /**
192 |  * Component variant information
193 |  */
194 | export interface ComponentVariant {
195 |     nodeId: string;
196 |     name: string;
197 |     properties: Record<string, string>;
198 |     isDefault: boolean;
199 | }
200 | 
201 | /**
202 |  * Information about nodes that were skipped due to limits
203 |  */
204 | export interface SkippedNodeInfo {
205 |     nodeId: string;
206 |     name: string;
207 |     type: string;
208 |     reason: 'depth_limit' | 'visual_importance' | 'max_nodes';
209 | }
210 | 
211 | /**
212 |  * Component extraction options
213 |  */
214 | export interface ComponentExtractionOptions {
215 |     maxChildNodes?: number;
216 |     maxDepth?: number;
217 |     includeHiddenNodes?: boolean;
218 |     prioritizeComponents?: boolean;
219 |     extractTextContent?: boolean;
220 | }
```

--------------------------------------------------------------------------------
/src/types/figma.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // src/types/figma.mts
  2 | 
  3 | export interface FigmaFile {
  4 |     document: FigmaNode;
  5 |     components: {[key: string]: FigmaComponent};
  6 |     styles: {[key: string]: FigmaStyle};
  7 |     name: string;
  8 |     lastModified: string;
  9 |     version: string;
 10 |     role: string;
 11 |     editorType: string;
 12 | }
 13 | 
 14 | export interface FigmaNode {
 15 |     id: string;
 16 |     name: string;
 17 |     type: string;
 18 |     visible?: boolean;
 19 |     children?: FigmaNode[];
 20 |     fills?: FigmaFill[];
 21 |     strokes?: FigmaStroke[];
 22 |     effects?: FigmaEffect[];
 23 |     backgroundColor?: FigmaColor;
 24 |     style?: FigmaTextStyle;
 25 |     constraints?: FigmaConstraints;
 26 |     absoluteBoundingBox?: FigmaBoundingBox;
 27 |     layoutMode?: string;
 28 |     primaryAxisSizingMode?: string;
 29 |     counterAxisSizingMode?: string;
 30 |     paddingLeft?: number;
 31 |     paddingRight?: number;
 32 |     paddingTop?: number;
 33 |     paddingBottom?: number;
 34 |     itemSpacing?: number;
 35 |     // Text-specific properties
 36 |     characters?: string; // Actual text content for TEXT nodes
 37 |     characterStyleOverrides?: number[];
 38 |     styleOverrideTable?: {[key: string]: FigmaTextStyle};
 39 | }
 40 | 
 41 | export interface FigmaComponent {
 42 |     key: string;
 43 |     file_key: string;
 44 |     node_id: string;
 45 |     thumbnail_url: string;
 46 |     name: string;
 47 |     description: string;
 48 |     created_at: string;
 49 |     updated_at: string;
 50 |     user: {
 51 |         id: string;
 52 |         handle: string;
 53 |         img_url: string;
 54 |     };
 55 |     containing_frame?: {
 56 |         name: string;
 57 |         node_id: string;
 58 |     };
 59 | }
 60 | 
 61 | export interface FigmaComponentSet {
 62 |     key: string;
 63 |     file_key: string;
 64 |     node_id: string;
 65 |     thumbnail_url: string;
 66 |     name: string;
 67 |     description: string;
 68 |     created_at: string;
 69 |     updated_at: string;
 70 |     user: {
 71 |         id: string;
 72 |         handle: string;
 73 |         img_url: string;
 74 |     };
 75 |     containing_frame?: {
 76 |         name: string;
 77 |         node_id: string;
 78 |     };
 79 | }
 80 | 
 81 | export interface FigmaStyle {
 82 |     key: string;
 83 |     file_key: string;
 84 |     node_id: string;
 85 |     style_type: 'FILL' | 'TEXT' | 'EFFECT' | 'GRID';
 86 |     thumbnail_url: string;
 87 |     name: string;
 88 |     description: string;
 89 |     created_at: string;
 90 |     updated_at: string;
 91 |     user: {
 92 |         id: string;
 93 |         handle: string;
 94 |         img_url: string;
 95 |     };
 96 |     sort_position: string;
 97 | }
 98 | 
 99 | export interface FigmaColor {
100 |     r: number;
101 |     g: number;
102 |     b: number;
103 |     a: number;
104 | }
105 | 
106 | export interface FigmaFill {
107 |     type: string;
108 |     color?: FigmaColor;
109 |     gradientStops?: Array<{
110 |         color: FigmaColor;
111 |         position: number;
112 |     }>;
113 |     visible?: boolean;
114 | }
115 | 
116 | export interface FigmaStroke {
117 |     type: string;
118 |     color: FigmaColor;
119 |     strokeWeight?: number;
120 |     visible?: boolean;
121 | }
122 | 
123 | export interface FigmaEffect {
124 |     type: string;
125 |     color?: FigmaColor;
126 |     offset?: {x: number; y: number};
127 |     radius: number;
128 |     spread?: number;
129 |     visible?: boolean;
130 | }
131 | 
132 | export interface FigmaTextStyle {
133 |     fontFamily: string;
134 |     fontWeight: number;
135 |     fontSize: number;
136 |     letterSpacing: number;
137 |     lineHeightPx: number;
138 |     textAlignHorizontal: string;
139 |     textAlignVertical: string;
140 | }
141 | 
142 | export interface FigmaConstraints {
143 |     vertical: string;
144 |     horizontal: string;
145 | }
146 | 
147 | export interface FigmaBoundingBox {
148 |     x: number;
149 |     y: number;
150 |     width: number;
151 |     height: number;
152 | }
153 | 
154 | export interface FigmaPageInfo {
155 |     id: string;
156 |     name: string;
157 |     type: string;
158 | }
159 | 
160 | export interface FigmaFileInfo {
161 |     name: string;
162 |     lastModified: string;
163 |     version: string;
164 |     role: string;
165 |     editorType: string;
166 |     componentCount: number;
167 |     styleCount: number;
168 |     pageCount: number;
169 | }
170 | 
171 | // Image Export Types
172 | export interface ImageExportOptions {
173 |     format?: 'png' | 'jpg' | 'svg' | 'pdf';
174 |     scale?: number; // 1, 2, 3, 4 for PNG/JPG
175 |     svgIncludeId?: boolean;
176 |     svgSimplifyStroke?: boolean;
177 |     svgOutlineText?: boolean;
178 |     useAbsoluteBounds?: boolean;
179 |     version?: string;
180 | }
181 | 
182 | export interface ImageExportResponse {
183 |     err?: string;
184 |     images: {[nodeId: string]: string | null};
185 | }
186 | 
187 | export interface ImageFillsResponse {
188 |     err?: string;
189 |     meta: {
190 |         images: {[imageRef: string]: string};
191 |     };
192 | }
193 | 
194 | // Responses
195 | export interface ComponentResponse {
196 |     meta: {
197 |         components: FigmaComponent[];
198 |     };
199 | }
200 | 
201 | export interface ComponentSetResponse {
202 |     meta: {
203 |         component_sets: FigmaComponentSet[];
204 |     };
205 | }
206 | 
207 | export interface StylesResponse {
208 |     meta: {
209 |         styles: FigmaStyle[];
210 |     };
211 | }
212 | 
213 | // Single Node Response
214 | export interface NodeResponse {
215 |     nodes: {
216 |         [nodeId: string]: {
217 |             document: FigmaNode;
218 |             components?: {[key: string]: FigmaComponent};
219 |             styles?: {[key: string]: FigmaStyle};
220 |         };
221 |     };
222 | }
223 | 
224 | // Page Response  
225 | export interface PageResponse {
226 |     document: FigmaNode;
227 |     components?: {[key: string]: FigmaComponent};
228 |     styles?: {[key: string]: FigmaStyle};
229 | }
```

--------------------------------------------------------------------------------
/src/extractors/colors/core.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // src/extractors/colors/core.mts
  2 | 
  3 | import type {FigmaNode} from '../../types/figma.js';
  4 | import type {
  5 |     ThemeColor,
  6 |     ColorDefinition,
  7 |     ColorExtractionContext,
  8 |     ColorExtractorFn,
  9 | } from './types.js';
 10 | import {
 11 |     extractColorsFromThemeFrame,
 12 | } from './extractor.js';
 13 | 
 14 | /**
 15 |  * Main color extraction orchestrator
 16 |  */
 17 | export class ColorExtractor {
 18 |     private colorLibrary: ColorDefinition[] = [];
 19 |     private colorMap = new Map<string, string>(); // color value -> ID
 20 | 
 21 |     /**
 22 |      * Extract theme colors from a specific frame
 23 |      */
 24 |     extractThemeFromFrame(frameNode: FigmaNode): ThemeColor[] {
 25 |         return extractColorsFromThemeFrame(frameNode);
 26 |     }
 27 | 
 28 |     /**
 29 |      * Extract colors from a single node recursively
 30 |      */
 31 |     private extractFromNode(
 32 |         node: FigmaNode,
 33 |         extractors: ColorExtractorFn[],
 34 |         context: ColorExtractionContext
 35 |     ): void {
 36 |         if (context.currentDepth > context.maxDepth) {
 37 |             return;
 38 |         }
 39 | 
 40 |         // Apply all color extractors to this node
 41 |         extractors.forEach(extractor => {
 42 |             extractor(node, context, this.colorLibrary);
 43 |         });
 44 | 
 45 |         // Process children
 46 |         if (node.children && node.children.length > 0) {
 47 |             const childContext = {...context, currentDepth: context.currentDepth + 1};
 48 | 
 49 |             node.children.forEach(child => {
 50 |                 this.extractFromNode(child, extractors, childContext);
 51 |             });
 52 |         }
 53 |     }
 54 | 
 55 |     /**
 56 |      * Add or get existing color from library
 57 |      */
 58 |     addColor(colorValue: string, nodeName: string): string {
 59 |         // Check if color already exists
 60 |         const existingId = this.colorMap.get(colorValue);
 61 |         if (existingId) {
 62 |             // Increment usage count
 63 |             const color = this.colorLibrary.find(c => c.id === existingId);
 64 |             if (color) {
 65 |                 color.usageCount++;
 66 |             }
 67 |             return existingId;
 68 |         }
 69 | 
 70 |         // Create new color definition
 71 |         const colorId = `color_${this.colorLibrary.length + 1}`;
 72 |         const colorDef: ColorDefinition = {
 73 |             id: colorId,
 74 |             name: this.generateColorName(colorValue, nodeName),
 75 |             value: colorValue,
 76 |             usage: this.categorizeColorUsage(nodeName),
 77 |             usageCount: 1
 78 |         };
 79 | 
 80 |         this.colorLibrary.push(colorDef);
 81 |         this.colorMap.set(colorValue, colorId);
 82 | 
 83 |         return colorId;
 84 |     }
 85 | 
 86 |     /**
 87 |      * Get current color library
 88 |      */
 89 |     getColorLibrary(): ColorDefinition[] {
 90 |         return [...this.colorLibrary];
 91 |     }
 92 | 
 93 |     /**
 94 |      * Get colors by usage category
 95 |      */
 96 |     getColorsByUsage(usage: ColorDefinition['usage']): ColorDefinition[] {
 97 |         return this.colorLibrary.filter(color => color.usage === usage);
 98 |     }
 99 | 
100 |     /**
101 |      * Get most used colors
102 |      */
103 |     getMostUsedColors(limit: number = 10): ColorDefinition[] {
104 |         return [...this.colorLibrary]
105 |             .sort((a, b) => b.usageCount - a.usageCount)
106 |             .slice(0, limit);
107 |     }
108 | 
109 |     /**
110 |      * Reset all libraries for new extraction
111 |      */
112 |     private resetLibraries(): void {
113 |         this.colorLibrary = [];
114 |         this.colorMap.clear();
115 |     }
116 | 
117 |     /**
118 |      * Generate meaningful color name from hex value and context
119 |      */
120 |     private generateColorName(colorValue: string, nodeName: string): string {
121 |         // Try to infer name from node context
122 |         const name = nodeName.toLowerCase();
123 | 
124 |         if (name.includes('primary')) return 'Primary';
125 |         if (name.includes('secondary')) return 'Secondary';
126 |         if (name.includes('background')) return 'Background';
127 |         if (name.includes('text')) return 'Text';
128 |         if (name.includes('accent')) return 'Accent';
129 | 
130 |         // Generate name based on color value
131 |         const colorNames: Record<string, string> = {
132 |             '#ffffff': 'White',
133 |             '#000000': 'Black',
134 |             '#ff0000': 'Red',
135 |             '#00ff00': 'Green',
136 |             '#0000ff': 'Blue',
137 |         };
138 | 
139 |         return colorNames[colorValue.toLowerCase()] || `Color${this.colorLibrary.length + 1}`;
140 |     }
141 | 
142 |     /**
143 |      * Categorize color usage
144 |      */
145 |     private categorizeColorUsage(nodeName: string): ColorDefinition['usage'] {
146 |         const name = nodeName.toLowerCase();
147 | 
148 |         if (name.includes('primary')) return 'primary';
149 |         if (name.includes('secondary')) return 'secondary';
150 |         if (name.includes('background') || name.includes('bg')) return 'background';
151 |         if (name.includes('text') || name.includes('label')) return 'text';
152 |         if (name.includes('accent') || name.includes('highlight')) return 'accent';
153 | 
154 |         return 'other';
155 |     }
156 | }
157 | 
158 | /**
159 |  * Convenience function to extract theme colors from a frame
160 |  */
161 | export function extractThemeColors(frameNode: FigmaNode): ThemeColor[] {
162 |     const extractor = new ColorExtractor();
163 |     return extractor.extractThemeFromFrame(frameNode);
164 | }
165 | 
```

--------------------------------------------------------------------------------
/src/utils/validation.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // utils/validation.mts
  2 | import {FigmaError} from '../types/errors.js';
  3 | 
  4 | /**
  5 |  * Utility class for validating Figma-related inputs
  6 |  */
  7 | export class FigmaValidator {
  8 |     /**
  9 |      * Validate Figma file ID
 10 |      * @param fileId The file ID to validate
 11 |      * @param fieldName Optional field name for error messages
 12 |      */
 13 |     static validateFileId(fileId: unknown, fieldName = 'fileId'): string {
 14 |         if (typeof fileId !== 'string') {
 15 |             throw new FigmaError(`${fieldName} must be a string`, 'VALIDATION_ERROR');
 16 |         }
 17 | 
 18 |         const trimmed = fileId.trim();
 19 | 
 20 |         if (trimmed.length === 0) {
 21 |             throw new FigmaError(`${fieldName} cannot be empty`, 'VALIDATION_ERROR');
 22 |         }
 23 | 
 24 |         if (trimmed.length < 10 || trimmed.length > 50) {
 25 |             throw new FigmaError(
 26 |                 `${fieldName} length must be between 10-50 characters, got ${trimmed.length}`,
 27 |                 'VALIDATION_ERROR'
 28 |             );
 29 |         }
 30 | 
 31 |         const validPattern = /^[a-zA-Z0-9\-_]+$/;
 32 |         if (!validPattern.test(trimmed)) {
 33 |             throw new FigmaError(
 34 |                 `${fieldName} contains invalid characters. Only alphanumeric, hyphens, and underscores allowed`,
 35 |                 'VALIDATION_ERROR'
 36 |             );
 37 |         }
 38 | 
 39 |         return trimmed;
 40 |     }
 41 | 
 42 |     /**
 43 |      * Validate Figma node ID
 44 |      * @param nodeId The node ID to validate
 45 |      * @param fieldName Optional field name for error messages
 46 |      */
 47 |     static validateNodeId(nodeId: unknown, fieldName = 'nodeId'): string {
 48 |         if (typeof nodeId !== 'string') {
 49 |             throw new FigmaError(`${fieldName} must be a string`, 'VALIDATION_ERROR');
 50 |         }
 51 | 
 52 |         const trimmed = nodeId.trim();
 53 | 
 54 |         if (trimmed.length === 0) {
 55 |             throw new FigmaError(`${fieldName} cannot be empty`, 'VALIDATION_ERROR');
 56 |         }
 57 | 
 58 |         // Figma node IDs are typically in format "number:number"
 59 |         const nodePattern = /^\d+:\d+$/;
 60 |         if (!nodePattern.test(trimmed)) {
 61 |             throw new FigmaError(
 62 |                 `${fieldName} must be in format "123:456", got "${trimmed}"`,
 63 |                 'VALIDATION_ERROR'
 64 |             );
 65 |         }
 66 | 
 67 |         return trimmed;
 68 |     }
 69 | 
 70 |     /**
 71 |      * Validate depth parameter
 72 |      * @param depth The depth to validate
 73 |      * @param fieldName Optional field name for error messages
 74 |      */
 75 |     static validateDepth(depth: unknown, fieldName = 'depth'): number {
 76 |         if (depth === undefined || depth === null) {
 77 |             return 3; // Default depth
 78 |         }
 79 | 
 80 |         if (typeof depth !== 'number' || !Number.isInteger(depth)) {
 81 |             throw new FigmaError(`${fieldName} must be an integer`, 'VALIDATION_ERROR');
 82 |         }
 83 | 
 84 |         if (depth < 1 || depth > 10) {
 85 |             throw new FigmaError(
 86 |                 `${fieldName} must be between 1-10, got ${depth}`,
 87 |                 'VALIDATION_ERROR'
 88 |             );
 89 |         }
 90 | 
 91 |         return depth;
 92 |     }
 93 | 
 94 |     /**
 95 |      * Validate max components parameter
 96 |      * @param maxComponents The max components to validate
 97 |      * @param fieldName Optional field name for error messages
 98 |      */
 99 |     static validateMaxComponents(maxComponents: unknown, fieldName = 'maxComponents'): number {
100 |         if (maxComponents === undefined || maxComponents === null) {
101 |             return 10; // Default max components
102 |         }
103 | 
104 |         if (typeof maxComponents !== 'number' || !Number.isInteger(maxComponents)) {
105 |             throw new FigmaError(`${fieldName} must be an integer`, 'VALIDATION_ERROR');
106 |         }
107 | 
108 |         if (maxComponents < 1 || maxComponents > 100) {
109 |             throw new FigmaError(
110 |                 `${fieldName} must be between 1-100, got ${maxComponents}`,
111 |                 'VALIDATION_ERROR'
112 |             );
113 |         }
114 | 
115 |         return maxComponents;
116 |     }
117 | 
118 |     /**
119 |      * Validate and sanitize widget name for Flutter
120 |      * @param name The widget name to validate
121 |      * @param fieldName Optional field name for error messages
122 |      */
123 |     static validateWidgetName(name: unknown, fieldName = 'widgetName'): string {
124 |         if (typeof name !== 'string') {
125 |             throw new FigmaError(`${fieldName} must be a string`, 'VALIDATION_ERROR');
126 |         }
127 | 
128 |         const trimmed = name.trim();
129 | 
130 |         if (trimmed.length === 0) {
131 |             throw new FigmaError(`${fieldName} cannot be empty`, 'VALIDATION_ERROR');
132 |         }
133 | 
134 |         // Check for valid Dart/Flutter class name
135 |         const validDartName = /^[A-Z][a-zA-Z0-9_]*$/;
136 |         if (!validDartName.test(trimmed)) {
137 |             throw new FigmaError(
138 |                 `${fieldName} "${trimmed}" is not a valid Flutter class name. Must start with uppercase letter and contain only letters, numbers, and underscores`,
139 |                 'VALIDATION_ERROR'
140 |             );
141 |         }
142 | 
143 |         return trimmed;
144 |     }
145 | 
146 |     /**
147 |      * Comprehensive validation for tool inputs
148 |      * @param inputs Object containing inputs to validate
149 |      */
150 |     static validateToolInputs(inputs: {
151 |         fileId?: unknown;
152 |         nodeId?: unknown;
153 |         depth?: unknown;
154 |         maxComponents?: unknown;
155 |         widgetName?: unknown;
156 |     }): {
157 |         fileId?: string;
158 |         nodeId?: string;
159 |         depth?: number;
160 |         maxComponents?: number;
161 |         widgetName?: string;
162 |     } {
163 |         const result: any = {};
164 | 
165 |         if (inputs.fileId !== undefined) {
166 |             result.fileId = this.validateFileId(inputs.fileId);
167 |         }
168 | 
169 |         if (inputs.nodeId !== undefined) {
170 |             result.nodeId = this.validateNodeId(inputs.nodeId);
171 |         }
172 | 
173 |         if (inputs.depth !== undefined) {
174 |             result.depth = this.validateDepth(inputs.depth);
175 |         }
176 | 
177 |         if (inputs.maxComponents !== undefined) {
178 |             result.maxComponents = this.validateMaxComponents(inputs.maxComponents);
179 |         }
180 | 
181 |         if (inputs.widgetName !== undefined) {
182 |             result.widgetName = this.validateWidgetName(inputs.widgetName);
183 |         }
184 | 
185 |         return result;
186 |     }
187 | }
```

--------------------------------------------------------------------------------
/src/extractors/components/deduplicated-extractor.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // src/extractors/components/deduplicated-extractor.mts
  2 | 
  3 | import type { FigmaNode } from '../../types/figma.js';
  4 | import type { FlutterStyleDefinition } from '../flutter/style-library.js';
  5 | import { FlutterStyleLibrary } from '../flutter/style-library.js';
  6 | import { GlobalStyleManager } from '../flutter/global-vars.js';
  7 | import { 
  8 |   extractStylingInfo, 
  9 |   extractLayoutInfo, 
 10 |   extractMetadata,
 11 |   extractTextInfo,
 12 |   createNestedComponentInfo
 13 | } from './extractor.js';
 14 | import type {
 15 |   ComponentMetadata,
 16 |   LayoutInfo,
 17 |   StylingInfo,
 18 |   NestedComponentInfo,
 19 |   TextInfo
 20 | } from './types.js';
 21 | 
 22 | export interface DeduplicatedComponentAnalysis {
 23 |   metadata: ComponentMetadata;
 24 |   styleRefs: Record<string, string>;
 25 |   children: DeduplicatedComponentChild[];
 26 |   nestedComponents: NestedComponentInfo[];
 27 |   newStyleDefinitions?: Record<string, FlutterStyleDefinition>;
 28 | }
 29 | 
 30 | export interface DeduplicatedComponentChild {
 31 |   nodeId: string;
 32 |   name: string;
 33 |   type: string;
 34 |   styleRefs: string[];
 35 |   semanticType?: string;
 36 |   textContent?: string;
 37 | }
 38 | 
 39 | export class DeduplicatedComponentExtractor {
 40 |   private styleLibrary = FlutterStyleLibrary.getInstance();
 41 |   private globalStyleManager = new GlobalStyleManager();
 42 |   
 43 |   async analyzeComponent(node: FigmaNode, trackNewStyles = false): Promise<DeduplicatedComponentAnalysis> {
 44 |     const styling = extractStylingInfo(node);
 45 |     const layout = extractLayoutInfo(node);
 46 |     const metadata = extractMetadata(node, false); // assuming not user-defined unless specified
 47 |     
 48 |     const styleRefs: Record<string, string> = {};
 49 |     const newStyles = new Set<string>();
 50 |     
 51 |     // Process decoration styles using the enhanced global style manager
 52 |     if (this.hasDecorationProperties(styling)) {
 53 |       const beforeCount = this.styleLibrary.getAllStyles().length;
 54 |       styleRefs.decoration = this.globalStyleManager.addStyle({
 55 |         fills: styling.fills,
 56 |         cornerRadius: styling.cornerRadius,
 57 |         effects: styling.effects
 58 |       }, 'decoration');
 59 |       if (trackNewStyles && this.styleLibrary.getAllStyles().length > beforeCount) {
 60 |         newStyles.add(styleRefs.decoration);
 61 |       }
 62 |     }
 63 |     
 64 |     // Process padding styles using the enhanced global style manager
 65 |     if (layout.padding) {
 66 |       const beforeCount = this.styleLibrary.getAllStyles().length;
 67 |       styleRefs.padding = this.globalStyleManager.addStyle({ padding: layout.padding }, 'padding');
 68 |       if (trackNewStyles && this.styleLibrary.getAllStyles().length > beforeCount) {
 69 |         newStyles.add(styleRefs.padding);
 70 |       }
 71 |     }
 72 |     
 73 |     // Process children with deduplication
 74 |     const children = await this.analyzeChildren(node);
 75 |     const nestedComponents = this.extractNestedComponents(node);
 76 |     
 77 |     const result: DeduplicatedComponentAnalysis = {
 78 |       metadata,
 79 |       styleRefs,
 80 |       children,
 81 |       nestedComponents
 82 |     };
 83 |     
 84 |     if (trackNewStyles && newStyles.size > 0) {
 85 |       result.newStyleDefinitions = this.getStyleDefinitions(Array.from(newStyles));
 86 |     }
 87 |     
 88 |     return result;
 89 |   }
 90 |   
 91 |   private async analyzeChildren(node: FigmaNode): Promise<DeduplicatedComponentChild[]> {
 92 |     if (!node.children) return [];
 93 |     
 94 |     const children: DeduplicatedComponentChild[] = [];
 95 |     
 96 |     for (const child of node.children) {
 97 |       if (!child.visible) continue;
 98 |       
 99 |       const childStyleRefs: string[] = [];
100 |       
101 |       // Extract child styling using enhanced global style manager
102 |       const childStyling = extractStylingInfo(child);
103 |       if (this.hasDecorationProperties(childStyling)) {
104 |         const decorationRef = this.globalStyleManager.addStyle({
105 |           fills: childStyling.fills,
106 |           cornerRadius: childStyling.cornerRadius,
107 |           effects: childStyling.effects
108 |         }, 'decoration');
109 |         childStyleRefs.push(decorationRef);
110 |       }
111 |       
112 |       // Extract text styling for text nodes using enhanced global style manager
113 |       let textContent: string | undefined;
114 |       if (child.type === 'TEXT') {
115 |         const textInfo = extractTextInfo(child);
116 |         if (textInfo) {
117 |           textContent = textInfo.content;
118 |           
119 |           // Add text style to library using enhanced deduplication
120 |           if (child.style) {
121 |             const textStyleRef = this.globalStyleManager.addStyle({
122 |               fontFamily: child.style.fontFamily,
123 |               fontSize: child.style.fontSize,
124 |               fontWeight: child.style.fontWeight
125 |             }, 'text');
126 |             childStyleRefs.push(textStyleRef);
127 |           }
128 |         }
129 |       }
130 |       
131 |       children.push({
132 |         nodeId: child.id,
133 |         name: child.name,
134 |         type: child.type,
135 |         styleRefs: childStyleRefs,
136 |         semanticType: this.detectSemanticType(child),
137 |         textContent
138 |       });
139 |     }
140 |     
141 |     return children;
142 |   }
143 |   
144 |   private hasDecorationProperties(styling: StylingInfo): boolean {
145 |     return !!(styling.fills?.length || styling.cornerRadius !== undefined || styling.effects?.dropShadows?.length);
146 |   }
147 |   
148 |   private extractTextContent(node: any): string {
149 |     return node.characters || node.name || '';
150 |   }
151 |   
152 |   private detectSemanticType(node: any): string | undefined {
153 |     // Simplified semantic detection
154 |     if (node.type === 'TEXT') {
155 |       const content = this.extractTextContent(node).toLowerCase();
156 |       if (['click', 'submit', 'save', 'cancel'].some(word => content.includes(word))) {
157 |         return 'button';
158 |       }
159 |       return 'text';
160 |     }
161 |     return undefined;
162 |   }
163 |   
164 |   private extractNestedComponents(node: FigmaNode): NestedComponentInfo[] {
165 |     if (!node.children) return [];
166 |     
167 |     const nestedComponents: NestedComponentInfo[] = [];
168 |     
169 |     for (const child of node.children) {
170 |       if (child.type === 'COMPONENT' || child.type === 'INSTANCE' || child.type === 'COMPONENT_SET') {
171 |         nestedComponents.push(createNestedComponentInfo(child));
172 |       }
173 |     }
174 |     
175 |     return nestedComponents;
176 |   }
177 |   
178 |   private getStyleDefinitions(styleIds: string[]): Record<string, FlutterStyleDefinition> {
179 |     const definitions: Record<string, FlutterStyleDefinition> = {};
180 |     styleIds.forEach(id => {
181 |       const definition = this.styleLibrary.getStyle(id);
182 |       if (definition) {
183 |         definitions[id] = definition;
184 |       }
185 |     });
186 |     return definitions;
187 |   }
188 | }
189 | 
```

--------------------------------------------------------------------------------
/src/figma-config.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // figma-config.mts (enhanced version)
  2 | import {FigmaError} from './types/errors.js';
  3 | 
  4 | export interface FigmaConfig {
  5 |     readonly fileId: string;
  6 |     readonly defaultPageName: string;
  7 |     readonly maxDepth: number;
  8 |     readonly maxComponents: number;
  9 |     readonly commonNodes: Record<string, string>;
 10 | }
 11 | 
 12 | export interface FigmaConfigOptions {
 13 |     fileId?: string;
 14 |     defaultPageName?: string;
 15 |     maxDepth?: number;
 16 |     maxComponents?: number;
 17 |     commonNodes?: Record<string, string>;
 18 | }
 19 | 
 20 | // Default configuration
 21 | const DEFAULT_CONFIG: Omit<FigmaConfig, 'fileId'> = {
 22 |     defaultPageName: 'Design',
 23 |     maxDepth: 3,
 24 |     maxComponents: 10,
 25 |     commonNodes: {}
 26 | };
 27 | 
 28 | class FigmaConfigManager {
 29 |     private config: FigmaConfig;
 30 | 
 31 |     constructor(options: FigmaConfigOptions = {}) {
 32 |         // Try to get fileId from multiple sources
 33 |         const fileId = options.fileId ||
 34 |             process.env.FIGMA_FILE_ID ||
 35 |             '83dXk35avf0BTHtYPWeyl7'; // fallback to your current ID
 36 | 
 37 |         this.validateFileId(fileId);
 38 | 
 39 |         this.config = {
 40 |             fileId,
 41 |             defaultPageName: options.defaultPageName || DEFAULT_CONFIG.defaultPageName,
 42 |             maxDepth: options.maxDepth || DEFAULT_CONFIG.maxDepth,
 43 |             maxComponents: options.maxComponents || DEFAULT_CONFIG.maxComponents,
 44 |             commonNodes: {...DEFAULT_CONFIG.commonNodes, ...(options.commonNodes || {})}
 45 |         };
 46 |     }
 47 | 
 48 |     /**
 49 |      * Validate Figma file ID format
 50 |      * Figma file IDs are typically 22 characters, alphanumeric
 51 |      */
 52 |     private validateFileId(fileId: string): void {
 53 |         if (!fileId || typeof fileId !== 'string') {
 54 |             throw new FigmaError('Figma file ID is required', 'INVALID_CONFIG');
 55 |         }
 56 | 
 57 |         // Remove any whitespace
 58 |         fileId = fileId.trim();
 59 | 
 60 |         if (fileId.length === 0) {
 61 |             throw new FigmaError('Figma file ID cannot be empty', 'INVALID_CONFIG');
 62 |         }
 63 | 
 64 |         // Figma file IDs are typically 22 characters, alphanumeric
 65 |         if (fileId.length < 10 || fileId.length > 50) {
 66 |             throw new FigmaError(
 67 |                 `Invalid Figma file ID length: ${fileId.length}. Expected 10-50 characters.`,
 68 |                 'INVALID_CONFIG'
 69 |             );
 70 |         }
 71 | 
 72 |         // Check for valid characters (alphanumeric and some special chars)
 73 |         const validPattern = /^[a-zA-Z0-9\-_]+$/;
 74 |         if (!validPattern.test(fileId)) {
 75 |             throw new FigmaError(
 76 |                 'Invalid Figma file ID format. Only alphanumeric characters, hyphens, and underscores are allowed.',
 77 |                 'INVALID_CONFIG'
 78 |             );
 79 |         }
 80 |     }
 81 | 
 82 |     /**
 83 |      * Validate node ID format
 84 |      * Figma node IDs are typically in format "123:456"
 85 |      */
 86 |     private validateNodeId(nodeId: string): void {
 87 |         if (!nodeId || typeof nodeId !== 'string') {
 88 |             throw new FigmaError('Node ID is required', 'INVALID_CONFIG');
 89 |         }
 90 | 
 91 |         // Figma node IDs are typically in format "number:number"
 92 |         const nodePattern = /^\d+:\d+$/;
 93 |         if (!nodePattern.test(nodeId.trim())) {
 94 |             throw new FigmaError(
 95 |                 `Invalid node ID format: "${nodeId}". Expected format: "123:456"`,
 96 |                 'INVALID_CONFIG'
 97 |             );
 98 |         }
 99 |     }
100 | 
101 |     // Getters for config values
102 |     get fileId(): string {
103 |         return this.config.fileId;
104 |     }
105 | 
106 |     get defaultPageName(): string {
107 |         return this.config.defaultPageName;
108 |     }
109 | 
110 |     get maxDepth(): number {
111 |         return this.config.maxDepth;
112 |     }
113 | 
114 |     get maxComponents(): number {
115 |         return this.config.maxComponents;
116 |     }
117 | 
118 |     get commonNodes(): Record<string, string> {
119 |         return {...this.config.commonNodes}; // Return copy to prevent mutation
120 |     }
121 | 
122 |     /**
123 |      * Get a specific common node ID with validation
124 |      */
125 |     getCommonNode(name: string): string {
126 |         const nodeId = this.config.commonNodes[name];
127 |         if (!nodeId) {
128 |             const availableNodes = Object.keys(this.config.commonNodes).join(', ');
129 |             throw new FigmaError(
130 |                 `Common node "${name}" not found. Available nodes: ${availableNodes || 'none'}`,
131 |                 'NODE_NOT_FOUND'
132 |             );
133 |         }
134 |         return nodeId;
135 |     }
136 | 
137 |     /**
138 |      * Create a new config instance with updated values
139 |      * This maintains immutability
140 |      */
141 |     withUpdates(updates: FigmaConfigOptions): FigmaConfigManager {
142 |         return new FigmaConfigManager({
143 |             fileId: updates.fileId || this.config.fileId,
144 |             defaultPageName: updates.defaultPageName || this.config.defaultPageName,
145 |             maxDepth: updates.maxDepth || this.config.maxDepth,
146 |             maxComponents: updates.maxComponents || this.config.maxComponents,
147 |             commonNodes: {...this.config.commonNodes, ...(updates.commonNodes || {})}
148 |         });
149 |     }
150 | 
151 |     /**
152 |      * Add or update a common node
153 |      */
154 |     withCommonNode(name: string, nodeId: string): FigmaConfigManager {
155 |         this.validateNodeId(nodeId);
156 |         return this.withUpdates({
157 |             commonNodes: {[name]: nodeId}
158 |         });
159 |     }
160 | 
161 |     /**
162 |      * Get config summary for debugging
163 |      */
164 |     getSummary(): string {
165 |         return `Figma Config:
166 |   File ID: ${this.fileId}
167 |   Default Page: ${this.defaultPageName}
168 |   Max Depth: ${this.maxDepth}
169 |   Max Components: ${this.maxComponents}
170 |   Common Nodes: ${Object.keys(this.commonNodes).length} defined`;
171 |     }
172 | 
173 |     /**
174 |      * Export config as plain object
175 |      */
176 |     toObject(): FigmaConfig {
177 |         return {...this.config};
178 |     }
179 | }
180 | 
181 | // Create default instance
182 | export const figmaConfig = new FigmaConfigManager();
183 | 
184 | // Helper functions for backward compatibility
185 | export function getFileId(): string {
186 |     return figmaConfig.fileId;
187 | }
188 | 
189 | export function validateFileId(fileId?: string): boolean {
190 |     try {
191 |         new FigmaConfigManager({fileId: fileId || figmaConfig.fileId});
192 |         return true;
193 |     } catch (error) {
194 |         if (error instanceof FigmaError) {
195 |             throw error;
196 |         }
197 |         throw new FigmaError(`Validation failed: ${error}`, 'VALIDATION_ERROR');
198 |     }
199 | }
200 | 
201 | // Enhanced helper functions
202 | export function validateNodeId(nodeId: string): boolean {
203 |     const config = new FigmaConfigManager();
204 |     try {
205 |         config['validateNodeId'](nodeId); // Access private method for validation
206 |         return true;
207 |     } catch (error) {
208 |         if (error instanceof FigmaError) {
209 |             throw error;
210 |         }
211 |         throw new FigmaError(`Node ID validation failed: ${error}`, 'VALIDATION_ERROR');
212 |     }
213 | }
214 | 
215 | export function createConfig(options: FigmaConfigOptions): FigmaConfigManager {
216 |     return new FigmaConfigManager(options);
217 | }
218 | 
219 | // Environment-based config creation
220 | export function createConfigFromEnv(): FigmaConfigManager {
221 |     return new FigmaConfigManager({
222 |         fileId: process.env.FIGMA_FILE_ID,
223 |         maxDepth: process.env.FIGMA_MAX_DEPTH ? parseInt(process.env.FIGMA_MAX_DEPTH, 10) : undefined,
224 |         maxComponents: process.env.FIGMA_MAX_COMPONENTS ? parseInt(process.env.FIGMA_MAX_COMPONENTS, 10) : undefined
225 |     });
226 | }
```

--------------------------------------------------------------------------------
/src/utils/figma-url-parser.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // src/utils/figma-url-parser.mts
  2 | 
  3 | import {FigmaError} from '../types/errors.js';
  4 | 
  5 | /**
  6 |  * Component input parsing result
  7 |  */
  8 | export interface ComponentInput {
  9 |     fileId: string;
 10 |     nodeId: string;
 11 |     source: 'url' | 'direct';
 12 |     isValid: boolean;
 13 |     error?: string;
 14 | }
 15 | 
 16 | /**
 17 |  * Parse Figma component input from URL or direct parameters
 18 |  * Supports:
 19 |  * - https://www.figma.com/file/{fileId}/...?node-id={nodeId}
 20 |  * - https://www.figma.com/design/{fileId}/...?node-id={nodeId}
 21 |  * - Direct fileId and nodeId parameters
 22 |  */
 23 | export function parseComponentInput(input: string, nodeId?: string): ComponentInput {
 24 |     try {
 25 |         // If nodeId is provided separately, treat as direct input
 26 |         if (nodeId) {
 27 |             const validatedFileId = validateFileId(input.trim());
 28 |             const validatedNodeId = validateAndConvertNodeId(nodeId.trim());
 29 | 
 30 |             return {
 31 |                 fileId: validatedFileId,
 32 |                 nodeId: validatedNodeId,
 33 |                 source: 'direct',
 34 |                 isValid: true
 35 |             };
 36 |         }
 37 | 
 38 |         // Try to parse as URL first
 39 |         if (input.includes('figma.com')) {
 40 |             return parseFromUrl(input);
 41 |         }
 42 | 
 43 |         // Check if it's in fileId:nodeId format
 44 |         if (input.includes(':') && input.split(':').length === 2) {
 45 |             const [fileIdPart, nodeIdPart] = input.split(':');
 46 |             const validatedFileId = validateFileId(fileIdPart.trim());
 47 |             const validatedNodeId = validateAndConvertNodeId(`${fileIdPart.trim()}:${nodeIdPart.trim()}`);
 48 | 
 49 |             return {
 50 |                 fileId: validatedFileId,
 51 |                 nodeId: validatedNodeId,
 52 |                 source: 'direct',
 53 |                 isValid: true
 54 |             };
 55 |         }
 56 | 
 57 |         throw new FigmaError('Invalid input format. Expected Figma URL or fileId:nodeId format', 'INVALID_INPUT');
 58 | 
 59 |     } catch (error) {
 60 |         return {
 61 |             fileId: '',
 62 |             nodeId: '',
 63 |             source: 'direct',
 64 |             isValid: false,
 65 |             error: error instanceof Error ? error.message : String(error)
 66 |         };
 67 |     }
 68 | }
 69 | 
 70 | /**
 71 |  * Parse component info from Figma URL
 72 |  */
 73 | function parseFromUrl(url: string): ComponentInput {
 74 |     try {
 75 |         const urlObj = new URL(url.trim());
 76 | 
 77 |         // Check if it's a valid Figma URL
 78 |         if (!urlObj.hostname.includes('figma.com')) {
 79 |             throw new FigmaError('Not a valid Figma URL', 'INVALID_URL');
 80 |         }
 81 | 
 82 |         // Extract file ID from path
 83 |         // Paths can be: /file/{fileId}/... or /design/{fileId}/...
 84 |         const pathMatch = urlObj.pathname.match(/\/(file|design)\/([a-zA-Z0-9\-_]+)/);
 85 |         if (!pathMatch || !pathMatch[2]) {
 86 |             throw new FigmaError('Could not extract file ID from URL', 'INVALID_URL');
 87 |         }
 88 | 
 89 |         const fileId = pathMatch[2];
 90 |         const validatedFileId = validateFileId(fileId);
 91 | 
 92 |         // Extract node ID from query parameters
 93 |         const nodeIdParam = urlObj.searchParams.get('node-id') || urlObj.searchParams.get('node_id');
 94 |         if (!nodeIdParam) {
 95 |             throw new FigmaError('Node ID not found in URL parameters', 'INVALID_URL');
 96 |         }
 97 | 
 98 |         // Node ID in URL is often in format "123-456" but API expects "123:456"
 99 |         const validatedNodeId = validateAndConvertNodeId(nodeIdParam);
100 | 
101 |         return {
102 |             fileId: validatedFileId,
103 |             nodeId: validatedNodeId,
104 |             source: 'url',
105 |             isValid: true
106 |         };
107 | 
108 |     } catch (error) {
109 |         throw new FigmaError(
110 |             `Failed to parse Figma URL: ${error instanceof Error ? error.message : String(error)}`,
111 |             'URL_PARSE_ERROR'
112 |         );
113 |     }
114 | }
115 | 
116 | /**
117 |  * Validate Figma file ID format
118 |  */
119 | function validateFileId(fileId: string): string {
120 |     if (!fileId || typeof fileId !== 'string') {
121 |         throw new FigmaError('File ID is required', 'INVALID_FILE_ID');
122 |     }
123 | 
124 |     const trimmed = fileId.trim();
125 | 
126 |     if (trimmed.length === 0) {
127 |         throw new FigmaError('File ID cannot be empty', 'INVALID_FILE_ID');
128 |     }
129 | 
130 |     if (trimmed.length < 10 || trimmed.length > 50) {
131 |         throw new FigmaError(
132 |             `Invalid file ID length: ${trimmed.length}. Expected 10-50 characters.`,
133 |             'INVALID_FILE_ID'
134 |         );
135 |     }
136 | 
137 |     // File IDs contain alphanumeric characters, hyphens, and underscores
138 |     const validPattern = /^[a-zA-Z0-9\-_]+$/;
139 |     if (!validPattern.test(trimmed)) {
140 |         throw new FigmaError(
141 |             'Invalid file ID format. Only alphanumeric characters, hyphens, and underscores allowed.',
142 |             'INVALID_FILE_ID'
143 |         );
144 |     }
145 | 
146 |     return trimmed;
147 | }
148 | 
149 | /**
150 |  * Validate and convert node ID to correct format
151 |  * Handles both "123-456" (URL format) and "123:456" (API format)
152 |  */
153 | function validateAndConvertNodeId(nodeId: string): string {
154 |     if (!nodeId || typeof nodeId !== 'string') {
155 |         throw new FigmaError('Node ID is required', 'INVALID_NODE_ID');
156 |     }
157 | 
158 |     const trimmed = nodeId.trim();
159 | 
160 |     if (trimmed.length === 0) {
161 |         throw new FigmaError('Node ID cannot be empty', 'INVALID_NODE_ID');
162 |     }
163 | 
164 |     // Convert URL format (123-456) to API format (123:456) if needed
165 |     let apiFormat = trimmed;
166 |     if (trimmed.includes('-') && !trimmed.includes(':')) {
167 |         // Replace first hyphen with colon (handle cases like "123-456-789")
168 |         const parts = trimmed.split('-');
169 |         if (parts.length >= 2) {
170 |             apiFormat = `${parts[0]}:${parts.slice(1).join('-')}`;
171 |         }
172 |     }
173 | 
174 |     // Validate API format
175 |     const nodePattern = /^\d+:\d+$/;
176 |     if (!nodePattern.test(apiFormat)) {
177 |         throw new FigmaError(
178 |             `Invalid node ID format: "${trimmed}". Expected format: "123:456" or "123-456"`,
179 |             'INVALID_NODE_ID'
180 |         );
181 |     }
182 | 
183 |     return apiFormat;
184 | }
185 | 
186 | /**
187 |  * Check if a string is a valid node ID format
188 |  */
189 | export function isValidNodeIdFormat(nodeId: string): boolean {
190 |     try {
191 |         validateAndConvertNodeId(nodeId);
192 |         return true;
193 |     } catch {
194 |         return false;
195 |     }
196 | }
197 | 
198 | /**
199 |  * Extract file ID and node ID from various input formats
200 |  * Used for batch operations or validation
201 |  */
202 | export function extractIds(input: string): {fileId?: string; nodeId?: string} {
203 |     try {
204 |         const parsed = parseComponentInput(input);
205 |         if (parsed.isValid) {
206 |             return {
207 |                 fileId: parsed.fileId,
208 |                 nodeId: parsed.nodeId
209 |             };
210 |         }
211 |     } catch {
212 |         // Ignore errors for this helper function
213 |     }
214 | 
215 |     return {};
216 | }
217 | 
218 | /**
219 |  * Generate Figma URL from file ID and node ID
220 |  */
221 | export function generateFigmaUrl(fileId: string, nodeId: string): string {
222 |     const validatedFileId = validateFileId(fileId);
223 |     const validatedNodeId = validateAndConvertNodeId(nodeId);
224 | 
225 |     // Convert API format back to URL format (123:456 -> 123-456)
226 |     const urlNodeId = validatedNodeId.replace(':', '-');
227 | 
228 |     return `https://www.figma.com/file/${validatedFileId}?node-id=${urlNodeId}`;
229 | }
230 | 
231 | /**
232 |  * Check if input looks like a Figma URL
233 |  */
234 | export function isFigmaUrl(input: string): boolean {
235 |     try {
236 |         const url = new URL(input.trim());
237 |         return url.hostname.includes('figma.com') &&
238 |             (url.pathname.includes('/file/') || url.pathname.includes('/design/'));
239 |     } catch {
240 |         return false;
241 |     }
242 | }
```

--------------------------------------------------------------------------------
/src/services/figma.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // services/figma.mts
  2 | import fetch from 'node-fetch';
  3 | import type {FigmaNode, NodeResponse} from '../types/figma.js';
  4 | import {
  5 |     FigmaError,
  6 |     FigmaAuthError,
  7 |     FigmaNotFoundError,
  8 |     FigmaNetworkError,
  9 |     FigmaParseError,
 10 |     createFigmaError
 11 | } from '../types/errors.js';
 12 | import {withRetry} from '../utils/retry.js';
 13 | 
 14 | export class FigmaService {
 15 |     private accessToken: string;
 16 |     private baseUrl = 'https://api.figma.com/v1';
 17 | 
 18 |     constructor(accessToken: string) {
 19 |         if (!accessToken || accessToken.trim().length === 0) {
 20 |             throw new FigmaAuthError('Figma access token is required');
 21 |         }
 22 |         this.accessToken = accessToken;
 23 |     }
 24 | 
 25 |     /**
 26 |      * Core API request method with retry logic
 27 |      */
 28 |     private async makeRequest<T>(endpoint: string): Promise<T> {
 29 |         return withRetry(async () => {
 30 |             const url = `${this.baseUrl}${endpoint}`;
 31 | 
 32 |             try {
 33 |                 console.log(`🔄 Making Figma API request: ${endpoint}`);
 34 | 
 35 |                 const response = await fetch(url, {
 36 |                     headers: {
 37 |                         'X-Figma-Token': this.accessToken,
 38 |                         'Content-Type': 'application/json'
 39 |                     }
 40 |                 });
 41 | 
 42 |                 if (!response.ok) {
 43 |                     let errorDetails = '';
 44 |                     try {
 45 |                         const errorBody = await response.text();
 46 |                         if (errorBody) {
 47 |                             const parsedError = JSON.parse(errorBody);
 48 |                             errorDetails = parsedError.message || parsedError.error || errorBody;
 49 |                         }
 50 |                     } catch {
 51 |                         // Ignore parse errors for error body
 52 |                     }
 53 | 
 54 |                     const error = createFigmaError(response, errorDetails);
 55 | 
 56 |                     if (response.status === 404) {
 57 |                         throw new FigmaNotFoundError('API endpoint', endpoint);
 58 |                     }
 59 | 
 60 |                     throw error;
 61 |                 }
 62 | 
 63 |                 const data = await response.json() as T;
 64 |                 console.log(`✅ Successfully fetched: ${endpoint}`);
 65 |                 return data;
 66 | 
 67 |             } catch (error) {
 68 |                 if (error instanceof FigmaError) {
 69 |                     throw error;
 70 |                 }
 71 | 
 72 |                 if (error instanceof Error) {
 73 |                     if (error.message.includes('ENOTFOUND') || error.message.includes('ECONNREFUSED')) {
 74 |                         throw new FigmaNetworkError('Unable to connect to Figma API', error);
 75 |                     }
 76 | 
 77 |                     if (error.name === 'SyntaxError') {
 78 |                         throw new FigmaParseError('Invalid JSON response from Figma API', error);
 79 |                     }
 80 |                 }
 81 | 
 82 |                 throw new FigmaNetworkError(`Unexpected error: ${error}`, error as Error);
 83 |             }
 84 |         }, {
 85 |             maxAttempts: 3,
 86 |             initialDelayMs: 1000,
 87 |             maxDelayMs: 10000
 88 |         });
 89 |     }
 90 | 
 91 |     /**
 92 |      * Get specific nodes by IDs
 93 |      */
 94 |     async getNodes(fileId: string, nodeIds: string[]): Promise<Record<string, FigmaNode>> {
 95 |         if (!nodeIds || nodeIds.length === 0) {
 96 |             throw new FigmaError('At least one node ID is required', 'INVALID_INPUT');
 97 |         }
 98 | 
 99 |         try {
100 |             const data = await this.makeRequest<any>(`/files/${fileId}/nodes?ids=${nodeIds.join(',')}`);
101 | 
102 |             const nodes: Record<string, FigmaNode> = {};
103 |             Object.entries(data.nodes || {}).forEach(([nodeId, nodeData]: [string, any]) => {
104 |                 if (nodeData?.document) {
105 |                     nodes[nodeId] = nodeData.document;
106 |                 }
107 |             });
108 | 
109 |             return nodes;
110 |         } catch (error) {
111 |             if (error instanceof FigmaError) {
112 |                 throw error;
113 |             }
114 |             throw new FigmaError(`Failed to fetch nodes: ${error}`, 'FETCH_ERROR');
115 |         }
116 |     }
117 | 
118 |     /**
119 |      * Get a single node by ID
120 |      */
121 |     async getNode(fileId: string, nodeId: string): Promise<FigmaNode> {
122 |         if (!nodeId || nodeId.trim().length === 0) {
123 |             throw new FigmaError('Node ID is required', 'INVALID_INPUT');
124 |         }
125 | 
126 |         try {
127 |             const data = await this.makeRequest<NodeResponse>(`/files/${fileId}/nodes?ids=${nodeId}`);
128 | 
129 |             if (!data.nodes || !data.nodes[nodeId]) {
130 |                 throw new FigmaNotFoundError('Node', nodeId);
131 |             }
132 | 
133 |             const nodeData = data.nodes[nodeId];
134 |             if (!nodeData.document) {
135 |                 throw new FigmaParseError('Invalid node structure received from Figma API', nodeData);
136 |             }
137 | 
138 |             return nodeData.document;
139 |         } catch (error) {
140 |             if (error instanceof FigmaError) {
141 |                 throw error;
142 |             }
143 |             throw new FigmaError(`Failed to fetch node ${nodeId}: ${error}`, 'FETCH_ERROR');
144 |         }
145 |     }
146 | 
147 |     /**
148 |      * Get image export URLs
149 |      */
150 |     async getImageExportUrls(
151 |         fileId: string,
152 |         nodeIds: string[],
153 |         options: {
154 |             format?: 'png' | 'jpg' | 'svg' | 'pdf';
155 |             scale?: number;
156 |             svgIncludeId?: boolean;
157 |             svgSimplifyStroke?: boolean;
158 |             svgOutlineText?: boolean;
159 |         } = {}
160 |     ): Promise<Record<string, string>> {
161 |         if (!nodeIds || nodeIds.length === 0) {
162 |             return {};
163 |         }
164 | 
165 |         const params = new URLSearchParams({
166 |             ids: nodeIds.join(','),
167 |             format: options.format || 'png'
168 |         });
169 | 
170 |         if (options.scale && ['png', 'jpg'].includes(options.format || 'png')) {
171 |             params.append('scale', options.scale.toString());
172 |         }
173 | 
174 |         if (options.format === 'svg') {
175 |             if (options.svgIncludeId !== undefined) {
176 |                 params.append('svg_include_id', options.svgIncludeId.toString());
177 |             }
178 |             if (options.svgSimplifyStroke !== undefined) {
179 |                 params.append('svg_simplify_stroke', options.svgSimplifyStroke.toString());
180 |             }
181 |             if (options.svgOutlineText !== undefined) {
182 |                 params.append('svg_outline_text', options.svgOutlineText.toString());
183 |             }
184 |         }
185 | 
186 |         try {
187 |             const response = await this.makeRequest<any>(`/images/${fileId}?${params}`);
188 | 
189 |             if (response.err) {
190 |                 throw new FigmaError(`Image export failed: ${response.err}`, 'EXPORT_ERROR');
191 |             }
192 | 
193 |             // Filter out null values
194 |             const validImages: Record<string, string> = {};
195 |             Object.entries(response.images || {}).forEach(([nodeId, url]) => {
196 |                 if (url && typeof url === 'string') {
197 |                     validImages[nodeId] = url;
198 |                 }
199 |             });
200 | 
201 |             return validImages;
202 |         } catch (error) {
203 |             if (error instanceof FigmaError) {
204 |                 throw error;
205 |             }
206 |             throw new FigmaError(`Failed to export images: ${error}`, 'EXPORT_ERROR');
207 |         }
208 |     }
209 | 
210 |     /**
211 |      * Get image fills used in the file
212 |      */
213 |     async getImageFillUrls(fileId: string): Promise<Record<string, string>> {
214 |         try {
215 |             const response = await this.makeRequest<any>(`/files/${fileId}/images`);
216 |             return response.meta?.images || {};
217 |         } catch (error) {
218 |             if (error instanceof FigmaError) {
219 |                 throw error;
220 |             }
221 |             throw new FigmaError(`Failed to fetch image fills: ${error}`, 'FETCH_ERROR');
222 |         }
223 |     }
224 | }
```

--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {config as loadEnv} from "dotenv";
  2 | import yargs from "yargs";
  3 | import {hideBin} from "yargs/helpers";
  4 | import {resolve} from "path";
  5 | import {readFileSync} from "fs";
  6 | import {fileURLToPath} from "url";
  7 | import {dirname, join} from "path";
  8 | 
  9 | export interface ServerConfig {
 10 |     figmaApiKey?: string;
 11 |     outputFormat: "yaml" | "json";
 12 |     isStdioMode: boolean;
 13 |     isHttpMode: boolean;
 14 |     isRemoteMode: boolean;
 15 |     httpPort: number;
 16 |     configSources: {
 17 |         figmaApiKey: "cli" | "env" | "none";
 18 |         envFile: "cli" | "default";
 19 |         stdio: "cli" | "env" | "default";
 20 |         http: "cli" | "env" | "default";
 21 |         remote: "cli" | "env" | "default";
 22 |         port: "cli" | "env" | "default";
 23 |     };
 24 | }
 25 | 
 26 | function maskApiKey(key: string): string {
 27 |     if (!key || key.length <= 4) return "****";
 28 |     return `****${key.slice(-4)}`;
 29 | }
 30 | 
 31 | function getPackageVersion(): string {
 32 |     try {
 33 |         // Get the directory of the current module
 34 |         const __filename = fileURLToPath(import.meta.url);
 35 |         const __dirname = dirname(__filename);
 36 |         
 37 |         // Read package.json from the project root (one level up from src)
 38 |         const packageJsonPath = join(__dirname, '..', 'package.json');
 39 |         const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
 40 |         return packageJson.version || '0.0.1';
 41 |     } catch (error) {
 42 |         // Fallback to environment variable or default
 43 |         return process.env.npm_package_version || '0.0.1';
 44 |     }
 45 | }
 46 | 
 47 | interface CliArgs {
 48 |     "figma-api-key"?: string;
 49 |     env?: string;
 50 |     stdio?: boolean;
 51 |     http?: boolean;
 52 |     remote?: boolean;
 53 |     port?: number;
 54 | }
 55 | 
 56 | export function getServerConfig(): ServerConfig {
 57 |     // Parse command line arguments
 58 |     const argv = yargs(hideBin(process.argv))
 59 |         .options({
 60 |             "figma-api-key": {
 61 |                 type: "string",
 62 |                 description: "Your Figma API key (can also be set via FIGMA_API_KEY env var)",
 63 |             },
 64 |             env: {
 65 |                 type: "string",
 66 |                 description: "Path to custom .env file to load environment variables from",
 67 |             },
 68 |             stdio: {
 69 |                 type: "boolean",
 70 |                 description: "Run in stdio mode for MCP client communication",
 71 |                 default: false,
 72 |             },
 73 |             http: {
 74 |                 type: "boolean",
 75 |                 description: "Run in HTTP mode for local testing",
 76 |                 default: false,
 77 |             },
 78 |             remote: {
 79 |                 type: "boolean",
 80 |                 description: "Run in remote mode - users provide their own Figma API keys",
 81 |                 default: false,
 82 |             },
 83 |             port: {
 84 |                 type: "number",
 85 |                 description: "Port number for HTTP server",
 86 |                 default: 3333,
 87 |             },
 88 |         })
 89 |         .help()
 90 |         .version(getPackageVersion())
 91 |         .parseSync() as CliArgs;
 92 | 
 93 |     // Load environment variables from custom path or default
 94 |     let envFilePath: string;
 95 |     let envFileSource: "cli" | "default";
 96 | 
 97 |     if (argv.env) {
 98 |         envFilePath = resolve(argv.env);
 99 |         envFileSource = "cli";
100 |     } else {
101 |         envFilePath = resolve(process.cwd(), ".env");
102 |         envFileSource = "default";
103 |     }
104 | 
105 |     // Load .env file with override if custom path provided
106 |     loadEnv({path: envFilePath, override: !!argv.env});
107 | 
108 |     const config: ServerConfig = {
109 |         figmaApiKey: undefined,
110 |         outputFormat: "json",
111 |         isStdioMode: false,
112 |         isHttpMode: false,
113 |         isRemoteMode: false,
114 |         httpPort: 3333,
115 |         configSources: {
116 |             figmaApiKey: "none",
117 |             envFile: envFileSource,
118 |             stdio: "default",
119 |             http: "default",
120 |             remote: "default",
121 |             port: "default",
122 |         },
123 |     };
124 | 
125 |     // Handle FIGMA_API_KEY - Users must provide their own API key
126 |     if (argv["figma-api-key"]) {
127 |         config.figmaApiKey = argv["figma-api-key"];
128 |         config.configSources.figmaApiKey = "cli";
129 |     } else if (process.env.FIGMA_API_KEY) {
130 |         config.figmaApiKey = process.env.FIGMA_API_KEY;
131 |         config.configSources.figmaApiKey = "env";
132 |     }
133 |     // Users can provide API key via CLI args, .env file, or HTTP headers (in remote mode)
134 | 
135 |     // Handle stdio mode
136 |     if (argv.stdio) {
137 |         config.isStdioMode = true;
138 |         config.configSources.stdio = "cli";
139 |     } else if (process.env.NODE_ENV === "cli") {
140 |         config.isStdioMode = true;
141 |         config.configSources.stdio = "env";
142 |     }
143 | 
144 |     // Handle HTTP mode
145 |     if (argv.http) {
146 |         config.isHttpMode = true;
147 |         config.configSources.http = "cli";
148 |     } else if (process.env.HTTP_MODE === "true") {
149 |         config.isHttpMode = true;
150 |         config.configSources.http = "env";
151 |     }
152 | 
153 |     // Handle remote mode
154 |     if (argv.remote) {
155 |         config.isRemoteMode = true;
156 |         config.isHttpMode = true; // Remote mode implies HTTP mode
157 |         config.configSources.remote = "cli";
158 |     } else if (process.env.REMOTE_MODE === "true") {
159 |         config.isRemoteMode = true;
160 |         config.isHttpMode = true; // Remote mode implies HTTP mode
161 |         config.configSources.remote = "env";
162 |     }
163 | 
164 |     // Handle port configuration
165 |     if (argv.port) {
166 |         config.httpPort = argv.port;
167 |         config.configSources.port = "cli";
168 |     } else if (process.env.HTTP_PORT) {
169 |         config.httpPort = parseInt(process.env.HTTP_PORT, 10);
170 |         config.configSources.port = "env";
171 |     }
172 | 
173 |     // Validate configuration - Users must provide their own API key for ALL modes
174 |     if (!config.figmaApiKey) {
175 |         console.error("Error: FIGMA_API_KEY is required for all modes.");
176 |         console.error("Please provide your Figma API key via one of these methods:");
177 |         console.error("  1. CLI argument: --figma-api-key=YOUR_API_KEY");
178 |         console.error("  2. Environment variable: FIGMA_API_KEY=YOUR_API_KEY in .env file");
179 |         console.error("");
180 |         console.error("Get your API key from: https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens");
181 |         console.error("");
182 |         if (config.isRemoteMode) {
183 |             console.error("Note: In remote mode, this key serves as a fallback.");
184 |             console.error("Users can still provide their own keys via HTTP headers for isolation.");
185 |         }
186 |         console.error("");
187 |         console.error("Examples:");
188 |         console.error("  npx figma-flutter-mcp --figma-api-key=YOUR_KEY --stdio");
189 |         console.error("  echo 'FIGMA_API_KEY=YOUR_KEY' > .env && npx figma-flutter-mcp --stdio");
190 |         console.error("  npx figma-flutter-mcp --figma-api-key=YOUR_KEY --remote");
191 |         process.exit(1);
192 |     }
193 | 
194 |     // Log configuration sources (only in non-stdio mode)
195 |     if (!config.isStdioMode) {
196 |         console.log("\nConfiguration:");
197 |         console.log(`- ENV_FILE: ${envFilePath} (source: ${config.configSources.envFile})`);
198 |         if (config.figmaApiKey) {
199 |             console.log(
200 |                 `- FIGMA_API_KEY: ${maskApiKey(config.figmaApiKey)} (source: ${config.configSources.figmaApiKey})`
201 |             );
202 |         } else {
203 |             console.log(`- FIGMA_API_KEY: Not set - users will provide their own (source: ${config.configSources.figmaApiKey})`);
204 |         }
205 |         console.log(`- STDIO_MODE: ${config.isStdioMode} (source: ${config.configSources.stdio})`);
206 |         console.log(`- HTTP_MODE: ${config.isHttpMode} (source: ${config.configSources.http})`);
207 |         console.log(`- REMOTE_MODE: ${config.isRemoteMode} (source: ${config.configSources.remote})`);
208 |         if (config.isHttpMode) {
209 |             console.log(`- HTTP_PORT: ${config.httpPort} (source: ${config.configSources.port})`);
210 |         }
211 |         console.log(); // Empty line for better readability
212 |     }
213 | 
214 |     return config;
215 | }
```

--------------------------------------------------------------------------------
/src/tools/flutter/assets/assets.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // tools/flutter/assets.mts
  2 | import {z} from "zod";
  3 | import type {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
  4 | import {FigmaService} from "../../../services/figma.js";
  5 | import {join} from 'path';
  6 | import {
  7 |     createAssetsDirectory,
  8 |     generateAssetFilename,
  9 |     downloadImage,
 10 |     getFileStats,
 11 |     updatePubspecAssets,
 12 |     generateAssetConstants,
 13 |     groupAssetsByBaseName,
 14 |     type AssetInfo
 15 | } from "./asset-manager.js";
 16 | 
 17 | export function registerFlutterAssetTools(server: McpServer, figmaApiKey: string) {
 18 |     // Tool: Export Flutter Assets
 19 |     server.registerTool(
 20 |         "export_flutter_assets",
 21 |         {
 22 |             title: "Export Flutter Assets",
 23 |             description: "Export images from Figma nodes and set up Flutter assets directory with pubspec.yaml",
 24 |             inputSchema: {
 25 |                 fileId: z.string().describe("Figma file ID"),
 26 |                 nodeIds: z.array(z.string()).describe("Array of node IDs to export as images"),
 27 |                 projectPath: z.string().optional().describe("Path to Flutter project (defaults to current directory)"),
 28 |                 format: z.enum(['png', 'jpg', 'svg']).default('png').describe("Export format"),
 29 |                 scale: z.number().optional().default(2).describe("Export scale (1x, 2x, 3x, 4x)"),
 30 |                 includeMultipleResolutions: z.boolean().optional().default(false).describe("Generate @2x, @3x variants for different screen densities")
 31 |             }
 32 |         },
 33 |         async ({fileId, nodeIds, projectPath = process.cwd(), format = 'png', scale = 2, includeMultipleResolutions = false}) => {
 34 |             const token = figmaApiKey;
 35 |             if (!token) {
 36 |                 return {
 37 |                     content: [{
 38 |                         type: "text",
 39 |                         text: "Error: Figma access token not configured."
 40 |                     }]
 41 |                 };
 42 |             }
 43 | 
 44 |             try {
 45 |                 const figmaService = new FigmaService(token);
 46 | 
 47 |                 // First, get node details to filter for actual images/illustrations
 48 |                 const imageNodes = await filterImageNodes(fileId, nodeIds, figmaService);
 49 | 
 50 |                 if (imageNodes.length === 0) {
 51 |                     return {
 52 |                         content: [{
 53 |                             type: "text",
 54 |                             text: "No image assets found in the specified nodes. Only custom illustrations, photos, and non-icon graphics are exported."
 55 |                         }]
 56 |                     };
 57 |                 }
 58 | 
 59 |                 // Create assets directory structure
 60 |                 const assetsDir = await createAssetsDirectory(projectPath);
 61 | 
 62 |                 let downloadedAssets: AssetInfo[] = [];
 63 | 
 64 |                 // Process each resolution if multi-resolution is enabled
 65 |                 const scales = includeMultipleResolutions ? [1, 2, 3] : [scale];
 66 | 
 67 |                 for (const currentScale of scales) {
 68 |                     const imageUrls = await figmaService.getImageExportUrls(fileId, imageNodes.map(n => n.id), {
 69 |                         format,
 70 |                         scale: currentScale
 71 |                     });
 72 | 
 73 |                     for (const node of imageNodes) {
 74 |                         const imageUrl = imageUrls[node.id];
 75 |                         if (!imageUrl) continue;
 76 | 
 77 |                         const filename = generateAssetFilename(node.name, format, currentScale, includeMultipleResolutions);
 78 |                         const filepath = join(assetsDir, filename);
 79 | 
 80 |                         // Download the image
 81 |                         await downloadImage(imageUrl, filepath);
 82 | 
 83 |                         // Get file size for reporting
 84 |                         const stats = await getFileStats(filepath);
 85 | 
 86 |                         downloadedAssets.push({
 87 |                             nodeId: node.id,
 88 |                             nodeName: node.name,
 89 |                             filename,
 90 |                             path: `assets/images/${filename}`,
 91 |                             size: stats.size
 92 |                         });
 93 |                     }
 94 |                 }
 95 | 
 96 |                 // Update pubspec.yaml
 97 |                 const pubspecPath = join(projectPath, 'pubspec.yaml');
 98 |                 await updatePubspecAssets(pubspecPath, downloadedAssets);
 99 | 
100 |                 // Generate asset constants file
101 |                 const constantsFile = await generateAssetConstants(downloadedAssets, projectPath);
102 | 
103 |                 let output = `Successfully exported ${imageNodes.length} image assets to Flutter project!\n\n`;
104 |                 output += `Assets Directory: ${assetsDir}\n\n`;
105 |                 output += `Downloaded Assets:\n`;
106 | 
107 |                 // Group by base name for cleaner output
108 |                 const groupedAssets = groupAssetsByBaseName(downloadedAssets);
109 |                 Object.entries(groupedAssets).forEach(([baseName, assets]) => {
110 |                     output += `- ${baseName}:\n`;
111 |                     assets.forEach(asset => {
112 |                         output += `  • ${asset.filename} (${asset.size})\n`;
113 |                     });
114 |                 });
115 | 
116 |                 output += `\nPubspec Configuration:\n`;
117 |                 output += `- Merged asset declarations into pubspec.yaml\n`;
118 |                 output += `- Assets available under: assets/images/\n\n`;
119 | 
120 |                 output += `Generated Code:\n`;
121 |                 output += `- Merged asset constants into: ${constantsFile}\n`;
122 |                 output += `- Import in your Flutter code: import 'package:your_app/constants/assets.dart';\n`;
123 | 
124 |                 return {
125 |                     content: [{type: "text", text: output}]
126 |                 };
127 |             } catch (error) {
128 |                 return {
129 |                     content: [{
130 |                         type: "text",
131 |                         text: `Error exporting assets: ${error instanceof Error ? error.message : String(error)}`
132 |                     }]
133 |                 };
134 |             }
135 |         }
136 |     );
137 | }
138 | 
139 | // OPTIMIZED: Helper functions for filtering image nodes - only searches within target nodes
140 | async function filterImageNodes(fileId: string, targetNodeIds: string[], figmaService: any): Promise<Array<{id: string, name: string, node: any}>> {
141 |     // OPTIMIZED: Only get the target nodes instead of the entire file (massive performance improvement)
142 |     const targetNodes = await figmaService.getNodes(fileId, targetNodeIds);
143 | 
144 |     const allNodesWithImages: Array<{id: string, name: string, node: any}> = [];
145 | 
146 |     function extractImageNodes(node: any, nodeId: string = node.id): void {
147 |         // Check if this node has image fills
148 |         if (node.fills && node.fills.some((fill: any) => fill.type === 'IMAGE' && fill.visible !== false)) {
149 |             allNodesWithImages.push({
150 |                 id: nodeId,
151 |                 name: node.name,
152 |                 node: node
153 |             });
154 |         }
155 | 
156 |         // Check if this is a vector/illustration that should be exported
157 |         if (node.type === 'VECTOR' && node.name) {
158 |             const name = node.name.toLowerCase();
159 |             if ((name.includes('image') || name.includes('illustration') || name.includes('graphic')) &&
160 |                 !name.includes('icon') && !name.includes('button')) {
161 |                 allNodesWithImages.push({
162 |                     id: nodeId,
163 |                     name: node.name,
164 |                     node: node
165 |                 });
166 |             }
167 |         }
168 | 
169 |         // Recursively check children
170 |         if (node.children) {
171 |             node.children.forEach((child: any) => {
172 |                 extractImageNodes(child, child.id);
173 |             });
174 |         }
175 |     }
176 | 
177 |     // OPTIMIZED: Extract only from target nodes instead of entire file
178 |     // This eliminates the need for expensive boundary checking since we only search within target nodes
179 |     Object.values(targetNodes).forEach((node: any) => {
180 |         extractImageNodes(node);
181 |     });
182 | 
183 |     // OPTIMIZED: No filtering needed since we only searched within target nodes
184 |     return allNodesWithImages;
185 | }
186 | 
187 | // REMOVED: isNodeWithinTarget function no longer needed since we only search within target nodes
188 | 
189 | function toCamelCase(str: string): string {
190 |     return str
191 |         .toLowerCase()
192 |         .replace(/[^a-z0-9]/g, '_')
193 |         .replace(/_+/g, '_')
194 |         .replace(/^_|_$/g, '')
195 |         .replace(/_(.)/g, (_, char) => char.toUpperCase());
196 | }
197 | 
```

--------------------------------------------------------------------------------
/src/extractors/typography/core.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // src/extractors/typography/core.mts
  2 | 
  3 | import type {FigmaNode} from '../../types/figma.js';
  4 | import type {
  5 |     TypographyStyle,
  6 |     TypographyDefinition,
  7 |     TypographyExtractionContext,
  8 |     TypographyExtractorFn,
  9 |     TypographyExtractionOptions
 10 | } from './types.js';
 11 | import {
 12 |     extractTypographyFromThemeFrame,
 13 | } from './extractor.js';
 14 | 
 15 | /**
 16 |  * Main typography extraction orchestrator
 17 |  */
 18 | export class TypographyExtractor {
 19 |     private typographyLibrary: TypographyDefinition[] = [];
 20 |     private typographyMap = new Map<string, string>(); // style hash -> ID
 21 |     private fontFamilies = new Set<string>();
 22 | 
 23 |     /**
 24 |      * Extract theme typography from a specific frame
 25 |      */
 26 |     extractThemeFromFrame(frameNode: FigmaNode): TypographyStyle[] {
 27 |         return extractTypographyFromThemeFrame(frameNode);
 28 |     }
 29 | 
 30 |     /**
 31 |      * Extract typography from a single node recursively
 32 |      */
 33 |     private extractFromNode(
 34 |         node: FigmaNode,
 35 |         extractors: TypographyExtractorFn[],
 36 |         context: TypographyExtractionContext,
 37 |         options: TypographyExtractionOptions
 38 |     ): void {
 39 |         if (context.currentDepth > context.maxDepth) {
 40 |             return;
 41 |         }
 42 | 
 43 |         // Skip hidden nodes unless explicitly included
 44 |         if (!options.includeHiddenText && node.visible === false) {
 45 |             return;
 46 |         }
 47 | 
 48 |         // Apply all typography extractors to this node
 49 |         extractors.forEach(extractor => {
 50 |             extractor(node, context, this.typographyLibrary);
 51 |         });
 52 | 
 53 |         // Process children
 54 |         if (node.children && node.children.length > 0) {
 55 |             const childContext = {...context, currentDepth: context.currentDepth + 1};
 56 | 
 57 |             node.children.forEach(child => {
 58 |                 this.extractFromNode(child, extractors, childContext, options);
 59 |             });
 60 |         }
 61 |     }
 62 | 
 63 |     /**
 64 |      * Add or get existing typography from library
 65 |      */
 66 |     addTypography(
 67 |         fontFamily: string,
 68 |         fontSize: number,
 69 |         fontWeight: number,
 70 |         lineHeight: number,
 71 |         letterSpacing: number,
 72 |         nodeName: string
 73 |     ): string {
 74 |         // Create style hash for deduplication
 75 |         const styleHash = JSON.stringify({
 76 |             fontFamily,
 77 |             fontSize,
 78 |             fontWeight,
 79 |             lineHeight,
 80 |             letterSpacing
 81 |         });
 82 | 
 83 |         // Check if typography already exists
 84 |         const existingId = this.typographyMap.get(styleHash);
 85 |         if (existingId) {
 86 |             // Increment usage count
 87 |             const typography = this.typographyLibrary.find(t => t.id === existingId);
 88 |             if (typography) {
 89 |                 typography.usageCount++;
 90 |             }
 91 |             return existingId;
 92 |         }
 93 | 
 94 |         // Create new typography definition
 95 |         const typographyId = `typography_${this.typographyLibrary.length + 1}`;
 96 |         const typographyDef: TypographyDefinition = {
 97 |             id: typographyId,
 98 |             name: this.generateTypographyName(fontSize, fontWeight, nodeName),
 99 |             fontFamily,
100 |             fontSize,
101 |             fontWeight,
102 |             lineHeight,
103 |             letterSpacing,
104 |             usage: this.categorizeTypographyUsage(fontSize, nodeName),
105 |             usageCount: 1,
106 |             dartName: this.generateDartSafeName(fontSize, fontWeight, nodeName)
107 |         };
108 | 
109 |         this.typographyLibrary.push(typographyDef);
110 |         this.typographyMap.set(styleHash, typographyId);
111 |         this.fontFamilies.add(fontFamily);
112 | 
113 |         return typographyId;
114 |     }
115 | 
116 |     /**
117 |      * Get current typography library
118 |      */
119 |     getTypographyLibrary(): TypographyDefinition[] {
120 |         return [...this.typographyLibrary];
121 |     }
122 | 
123 |     /**
124 |      * Get typography by usage category
125 |      */
126 |     getTypographyByUsage(usage: TypographyDefinition['usage']): TypographyDefinition[] {
127 |         return this.typographyLibrary.filter(typography => typography.usage === usage);
128 |     }
129 | 
130 |     /**
131 |      * Get most used typography
132 |      */
133 |     getMostUsedTypography(limit: number = 10): TypographyDefinition[] {
134 |         return [...this.typographyLibrary]
135 |             .sort((a, b) => b.usageCount - a.usageCount)
136 |             .slice(0, limit);
137 |     }
138 | 
139 |     /**
140 |      * Get all font families used
141 |      */
142 |     getFontFamilies(): Set<string> {
143 |         return new Set(this.fontFamilies);
144 |     }
145 | 
146 |     /**
147 |      * Get primary font family (most used)
148 |      */
149 |     getPrimaryFontFamily(): string | null {
150 |         if (this.fontFamilies.size === 0) return null;
151 | 
152 |         // Count usage of each font family
153 |         const familyCount = new Map<string, number>();
154 |         this.typographyLibrary.forEach(typography => {
155 |             const count = familyCount.get(typography.fontFamily) || 0;
156 |             familyCount.set(typography.fontFamily, count + typography.usageCount);
157 |         });
158 | 
159 |         // Find most used font family
160 |         let maxCount = 0;
161 |         let primaryFamily = null;
162 |         familyCount.forEach((count, family) => {
163 |             if (count > maxCount) {
164 |                 maxCount = count;
165 |                 primaryFamily = family;
166 |             }
167 |         });
168 | 
169 |         return primaryFamily;
170 |     }
171 | 
172 |     /**
173 |      * Reset all libraries for new extraction
174 |      */
175 |     private resetLibraries(): void {
176 |         this.typographyLibrary = [];
177 |         this.typographyMap.clear();
178 |         this.fontFamilies.clear();
179 |     }
180 | 
181 |     /**
182 |      * Generate meaningful typography name
183 |      */
184 |     private generateTypographyName(fontSize: number, fontWeight: number, nodeName: string): string {
185 |         // Try to infer name from node context
186 |         const name = nodeName.toLowerCase();
187 | 
188 |         if (name.includes('heading') || name.includes('title')) return 'Heading';
189 |         if (name.includes('subtitle')) return 'Subtitle';
190 |         if (name.includes('body')) return 'Body';
191 |         if (name.includes('caption')) return 'Caption';
192 |         if (name.includes('button')) return 'Button';
193 |         if (name.includes('label')) return 'Label';
194 | 
195 |         // Generate name based on size and weight
196 |         let sizeName = 'Body';
197 |         if (fontSize >= 32) sizeName = 'DisplayLarge';
198 |         else if (fontSize >= 28) sizeName = 'DisplayMedium';
199 |         else if (fontSize >= 24) sizeName = 'DisplaySmall';
200 |         else if (fontSize >= 22) sizeName = 'HeadlineLarge';
201 |         else if (fontSize >= 20) sizeName = 'HeadlineMedium';
202 |         else if (fontSize >= 18) sizeName = 'HeadlineSmall';
203 |         else if (fontSize >= 16) sizeName = 'BodyLarge';
204 |         else if (fontSize >= 14) sizeName = 'BodyMedium';
205 |         else if (fontSize >= 12) sizeName = 'BodySmall';
206 |         else if (fontSize >= 11) sizeName = 'LabelLarge';
207 |         else if (fontSize >= 10) sizeName = 'LabelMedium';
208 |         else sizeName = 'LabelSmall';
209 | 
210 |         // Add weight if not regular
211 |         if (fontWeight >= 700) {
212 |             sizeName += 'Bold';
213 |         } else if (fontWeight >= 600) {
214 |             sizeName += 'SemiBold';
215 |         } else if (fontWeight >= 500) {
216 |             sizeName += 'Medium';
217 |         } else if (fontWeight <= 300) {
218 |             sizeName += 'Light';
219 |         }
220 | 
221 |         return sizeName;
222 |     }
223 | 
224 |     /**
225 |      * Generate Dart-safe property name
226 |      */
227 |     private generateDartSafeName(fontSize: number, fontWeight: number, nodeName: string): string {
228 |         const baseName = this.generateTypographyName(fontSize, fontWeight, nodeName);
229 |         
230 |         // Convert to camelCase and ensure it's Dart-safe
231 |         return baseName.charAt(0).toLowerCase() + baseName.slice(1)
232 |             .replace(/[^a-zA-Z0-9]/g, '')
233 |             .replace(/^\d/, '_$&'); // Prefix with underscore if starts with number
234 |     }
235 | 
236 |     /**
237 |      * Categorize typography usage
238 |      */
239 |     private categorizeTypographyUsage(fontSize: number, nodeName: string): TypographyDefinition['usage'] {
240 |         const name = nodeName.toLowerCase();
241 | 
242 |         // Check name patterns first
243 |         if (name.includes('heading') || name.includes('title') || name.includes('h1') || name.includes('h2') || name.includes('h3')) {
244 |             return 'heading';
245 |         }
246 |         if (name.includes('body') || name.includes('paragraph')) return 'body';
247 |         if (name.includes('caption') || name.includes('small')) return 'caption';
248 |         if (name.includes('button')) return 'button';
249 |         if (name.includes('label')) return 'label';
250 | 
251 |         // Fallback to size-based categorization
252 |         if (fontSize >= 20) return 'heading';
253 |         if (fontSize >= 14) return 'body';
254 |         if (fontSize >= 12) return 'caption';
255 | 
256 |         return 'other';
257 |     }
258 | }
259 | 
260 | /**
261 |  * Convenience function to extract theme typography from a frame
262 |  */
263 | export function extractThemeTypography(frameNode: FigmaNode): TypographyStyle[] {
264 |     const extractor = new TypographyExtractor();
265 |     return extractor.extractThemeFromFrame(frameNode);
266 | }
```

--------------------------------------------------------------------------------
/src/tools/flutter/theme/colors/theme-tool.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // src/tools/flutter/simple-theme-tool.mts
  2 | import {z} from "zod";
  3 | import type {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
  4 | import {FigmaService} from "../../../../services/figma.js";
  5 | import {extractThemeColors} from "../../../../extractors/colors/index.js";
  6 | import {SimpleThemeGenerator} from "./theme-generator.js";
  7 | import {join} from 'path';
  8 | 
  9 | export function registerThemeTools(server: McpServer, figmaApiKey: string) {
 10 |     server.registerTool(
 11 |         "extract_theme_colors",
 12 |         {
 13 |             title: "Extract Theme Colors from Frame",
 14 |             description: "Extract colors from a Figma theme frame containing color swatches with labels",
 15 |             inputSchema: {
 16 |                 fileId: z.string().describe("Figma file ID"),
 17 |                 nodeId: z.string().describe("Theme frame node ID containing color swatches"),
 18 |                 projectPath: z.string().optional().describe("Path to Flutter project (defaults to current directory)"),
 19 |                 generateThemeData: z.boolean().optional().describe("Generate Flutter ThemeData class (defaults to false)")
 20 |             }
 21 |         },
 22 |         async ({fileId, nodeId, projectPath = process.cwd(), generateThemeData = false}) => {
 23 |             const token = figmaApiKey;
 24 |             if (!token) {
 25 |                 return {
 26 |                     content: [{
 27 |                         type: "text",
 28 |                         text: "Error: Figma access token not configured."
 29 |                     }]
 30 |                 };
 31 |             }
 32 | 
 33 |             try {
 34 |                 // Initialize services
 35 |                 const figmaService = new FigmaService(token);
 36 |                 const generator = new SimpleThemeGenerator();
 37 | 
 38 |                 // Get the specific theme frame node
 39 |                 const themeFrame = await figmaService.getNode(fileId, nodeId);
 40 | 
 41 |                 if (!themeFrame) {
 42 |                     return {
 43 |                         content: [{
 44 |                             type: "text",
 45 |                             text: `Theme frame with node ID "${nodeId}" not found.`
 46 |                         }]
 47 |                     };
 48 |                 }
 49 | 
 50 |                 // Extract colors from the theme frame
 51 |                 const themeColors = extractThemeColors(themeFrame);
 52 | 
 53 |                 if (themeColors.length === 0) {
 54 |                     return {
 55 |                         content: [{
 56 |                             type: "text",
 57 |                             text: `No colors found in theme frame "${themeFrame.name}". Make sure the frame contains color swatches with text labels.`
 58 |                         }]
 59 |                     };
 60 |                 }
 61 | 
 62 |                 // Generate AppColors class
 63 |                 const outputPath = join(projectPath, 'lib', 'theme');
 64 |                 const generatedFilePath = await generator.generateAppColors(themeColors, outputPath, {
 65 |                     generateThemeData,
 66 |                     includeColorScheme: true,
 67 |                     includeMaterialColors: true
 68 |                 });
 69 | 
 70 |                 // Create success report
 71 |                 let output = `Successfully extracted theme colors!\n\n`;
 72 |                 output += `Theme Frame: ${themeFrame.name}\n`;
 73 |                 output += `Node ID: ${nodeId}\n`;
 74 |                 output += `Colors found: ${themeColors.length}\n`;
 75 |                 output += `Generated: ${generatedFilePath}\n`;
 76 |                 if (generateThemeData) {
 77 |                     output += `Theme Data: ${join(outputPath, 'app_theme.dart')}\n`;
 78 |                 }
 79 |                 output += `\n`;
 80 | 
 81 |                 output += `Extracted Colors:\n`;
 82 |                 themeColors.forEach((color, index) => {
 83 |                     output += `${index + 1}. ${color.name}: ${color.hex}\n`;
 84 |                 });
 85 | 
 86 |                 output += `\nGenerated Files:\n`;
 87 |                 output += `• app_colors.dart - Color constants\n`;
 88 |                 if (generateThemeData) {
 89 |                     output += `• app_theme.dart - Flutter ThemeData\n`;
 90 |                 }
 91 | 
 92 |                 output += `\nUsage Examples:\n`;
 93 |                 output += `// Colors:\n`;
 94 |                 output += `Container(color: AppColors.primary)\n`;
 95 |                 output += `Text('Hello', style: TextStyle(color: AppColors.backgroundDark))\n`;
 96 |                 
 97 |                 if (generateThemeData) {
 98 |                     output += `\n// Theme:\n`;
 99 |                     output += `MaterialApp(\n`;
100 |                     output += `  theme: AppTheme.lightTheme,\n`;
101 |                     output += `  // ... your app\n`;
102 |                     output += `)\n`;
103 |                 }
104 | 
105 |                 return {
106 |                     content: [{type: "text", text: output}]
107 |                 };
108 | 
109 |             } catch (error) {
110 |                 return {
111 |                     content: [{
112 |                         type: "text",
113 |                         text: `Error extracting theme colors: ${error instanceof Error ? error.message : String(error)}`
114 |                     }]
115 |                 };
116 |             }
117 |         }
118 |     );
119 | 
120 |     // Helper tool to inspect a frame structure
121 |     server.registerTool(
122 |         "inspect_theme_frame",
123 |         {
124 |             title: "Inspect Theme Frame Structure",
125 |             description: "Inspect the structure of a theme frame to understand its contents before extraction",
126 |             inputSchema: {
127 |                 fileId: z.string().describe("Figma file ID"),
128 |                 nodeId: z.string().describe("Frame node ID to inspect")
129 |             }
130 |         },
131 |         async ({fileId, nodeId}) => {
132 |             const token = figmaApiKey;
133 |             if (!token) {
134 |                 return {
135 |                     content: [{
136 |                         type: "text",
137 |                         text: "Error: Figma access token not configured."
138 |                     }]
139 |                 };
140 |             }
141 | 
142 |             try {
143 |                 const figmaService = new FigmaService(token);
144 |                 const frameNode = await figmaService.getNode(fileId, nodeId);
145 | 
146 |                 if (!frameNode) {
147 |                     return {
148 |                         content: [{
149 |                             type: "text",
150 |                             text: `Frame with node ID "${nodeId}" not found.`
151 |                         }]
152 |                     };
153 |                 }
154 | 
155 |                 let output = `Frame Inspection Report\n\n`;
156 |                 output += `Frame Name: ${frameNode.name}\n`;
157 |                 output += `Frame Type: ${frameNode.type}\n`;
158 |                 output += `Node ID: ${nodeId}\n`;
159 |                 output += `Children: ${frameNode.children?.length || 0}\n\n`;
160 | 
161 |                 if (frameNode.children && frameNode.children.length > 0) {
162 |                     output += `Frame Contents:\n`;
163 |                     frameNode.children.forEach((child, index) => {
164 |                         output += `${index + 1}. ${child.name} (${child.type})\n`;
165 | 
166 |                         // Show color if it has one
167 |                         if (child.fills && Array.isArray(child.fills)) {
168 |                             const solidFill = child.fills.find(fill => fill.type === 'SOLID' && fill.color);
169 |                             if (solidFill && solidFill.color) {
170 |                                 const hex = rgbaToHex(solidFill.color);
171 |                                 output += `   Color: ${hex}\n`;
172 |                             }
173 |                         }
174 | 
175 |                         // Show text children
176 |                         if (child.children) {
177 |                             const textChildren = child.children.filter(c => c.type === 'TEXT');
178 |                             if (textChildren.length > 0) {
179 |                                 output += `   Text: ${textChildren.map(t => t.name).join(', ')}\n`;
180 |                             }
181 |                         }
182 |                     });
183 |                 } else {
184 |                     output += `Frame is empty or has no children.\n`;
185 |                 }
186 | 
187 |                 output += `\nThis frame ${frameNode.children && frameNode.children.length > 0 ? 'can' : 'cannot'} be used for theme color extraction.\n`;
188 | 
189 |                 return {
190 |                     content: [{type: "text", text: output}]
191 |                 };
192 | 
193 |             } catch (error) {
194 |                 return {
195 |                     content: [{
196 |                         type: "text",
197 |                         text: `Error inspecting frame: ${error instanceof Error ? error.message : String(error)}`
198 |                     }]
199 |                 };
200 |             }
201 |         }
202 |     );
203 | }
204 | 
205 | function rgbaToHex(color: {r: number; g: number; b: number; a?: number}): string {
206 |     const r = Math.round(color.r * 255);
207 |     const g = Math.round(color.g * 255);
208 |     const b = Math.round(color.b * 255);
209 | 
210 |     return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase();
211 | }
```

--------------------------------------------------------------------------------
/src/tools/flutter/assets/asset-manager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // tools/flutter/asset-manager.mts
  2 | import {writeFile, mkdir, readFile} from 'fs/promises';
  3 | import {join, dirname} from 'path';
  4 | 
  5 | export interface AssetInfo {
  6 |     nodeId: string;
  7 |     nodeName: string;
  8 |     filename: string;
  9 |     path: string;
 10 |     size: string;
 11 | }
 12 | 
 13 | export async function createAssetsDirectory(projectPath: string): Promise<string> {
 14 |     const assetsDir = join(projectPath, 'assets', 'images');
 15 |     await mkdir(assetsDir, {recursive: true});
 16 |     return assetsDir;
 17 | }
 18 | 
 19 | export async function createSvgAssetsDirectory(projectPath: string): Promise<string> {
 20 |     const assetsDir = join(projectPath, 'assets', 'svgs');
 21 |     await mkdir(assetsDir, {recursive: true});
 22 |     return assetsDir;
 23 | }
 24 | 
 25 | export function generateAssetFilename(nodeName: string, format: string, scale: number, multiRes: boolean): string {
 26 |     // Clean the node name for filename
 27 |     const cleanName = nodeName
 28 |         .toLowerCase()
 29 |         .replace(/[^a-z0-9]/g, '_')
 30 |         .replace(/_+/g, '_')
 31 |         .replace(/^_|_$/g, '');
 32 | 
 33 |     if (multiRes && scale > 1) {
 34 |         return `${cleanName}@${scale}x.${format}`;
 35 |     }
 36 | 
 37 |     return `${cleanName}.${format}`;
 38 | }
 39 | 
 40 | export function generateSvgFilename(nodeName: string): string {
 41 |     // Clean the node name for SVG filename
 42 |     const cleanName = nodeName
 43 |         .toLowerCase()
 44 |         .replace(/[^a-z0-9]/g, '_')
 45 |         .replace(/_+/g, '_')
 46 |         .replace(/^_|_$/g, '');
 47 | 
 48 |     return `${cleanName}.svg`;
 49 | }
 50 | 
 51 | export async function downloadImage(url: string, filepath: string): Promise<void> {
 52 |     const response = await fetch(url);
 53 |     if (!response.ok) {
 54 |         throw new Error(`Failed to download image: ${response.statusText}`);
 55 |     }
 56 | 
 57 |     const buffer = await response.arrayBuffer();
 58 |     await mkdir(dirname(filepath), {recursive: true});
 59 |     await writeFile(filepath, Buffer.from(buffer));
 60 | }
 61 | 
 62 | export async function getFileStats(filepath: string): Promise<{size: string}> {
 63 |     try {
 64 |         const {size} = await import('fs').then(fs => fs.promises.stat(filepath));
 65 |         return {
 66 |             size: size > 1024 * 1024
 67 |                 ? `${(size / 1024 / 1024).toFixed(1)}MB`
 68 |                 : `${Math.round(size / 1024)}KB`
 69 |         };
 70 |     } catch {
 71 |         return {size: 'Unknown'};
 72 |     }
 73 | }
 74 | 
 75 | export async function updatePubspecAssets(pubspecPath: string, assets: Array<{path: string}>): Promise<void> {
 76 |     let pubspecContent: string;
 77 | 
 78 |     try {
 79 |         pubspecContent = await readFile(pubspecPath, 'utf-8');
 80 |     } catch {
 81 |         // If pubspec doesn't exist, create a basic one
 82 |         pubspecContent = `name: flutter_app
 83 | description: A Flutter application
 84 | 
 85 | version: 1.0.0+1
 86 | 
 87 | environment:
 88 |   sdk: '>=3.0.0 <4.0.0'
 89 | 
 90 | dependencies:
 91 |   flutter:
 92 |     sdk: flutter
 93 | 
 94 | dev_dependencies:
 95 |   flutter_test:
 96 |     sdk: flutter
 97 | 
 98 | flutter:
 99 |   uses-material-design: true
100 | `;
101 |     }
102 | 
103 |     // Extract existing assets from pubspec
104 |     const existingAssets = new Set<string>();
105 |     const assetMatch = pubspecContent.match(/assets:\s*\n((?:    - .*\n)*)/);
106 |     if (assetMatch) {
107 |         const existingAssetLines = assetMatch[1].match(/    - .*/g) || [];
108 |         existingAssetLines.forEach(line => {
109 |             const assetPath = line.replace(/^\s*-\s*/, '').trim();
110 |             existingAssets.add(assetPath);
111 |         });
112 |     }
113 | 
114 |     // Add new assets to existing ones
115 |     const newAssetPaths = assets.map(a => a.path);
116 |     newAssetPaths.forEach(path => existingAssets.add(path));
117 | 
118 |     // Convert back to formatted lines
119 |     const allAssetPaths = Array.from(existingAssets).sort().map(path => `    - ${path}`);
120 | 
121 |     if (pubspecContent.includes('assets:')) {
122 |         // Replace existing assets section with merged assets
123 |         pubspecContent = pubspecContent.replace(
124 |             /assets:\s*\n(?:    - .*\n)*/,
125 |             `assets:\n${allAssetPaths.join('\n')}\n`
126 |         );
127 |     } else if (pubspecContent.includes('flutter:')) {
128 |         // Add assets to existing flutter section
129 |         pubspecContent = pubspecContent.replace(
130 |             'flutter:',
131 |             `flutter:\n  assets:\n${allAssetPaths.join('\n')}`
132 |         );
133 |     } else {
134 |         // Add flutter section with assets
135 |         pubspecContent += `\nflutter:\n  assets:\n${allAssetPaths.join('\n')}\n`;
136 |     }
137 | 
138 |     await writeFile(pubspecPath, pubspecContent);
139 | }
140 | 
141 | export async function generateAssetConstants(assets: Array<{filename: string, nodeName: string}>, projectPath: string): Promise<string> {
142 |     const constantsDir = join(projectPath, 'lib', 'constants');
143 |     await mkdir(constantsDir, {recursive: true});
144 | 
145 |     const constantsPath = join(constantsDir, 'assets.dart');
146 | 
147 |     // Read existing constants if they exist
148 |     const existingConstants = new Map<string, string>();
149 |     try {
150 |         const existingContent = await readFile(constantsPath, 'utf-8');
151 |         // Extract existing constants using regex
152 |         const constantMatches = existingContent.matchAll(/static const String (\w+) = '([^']+)';/g);
153 |         for (const match of constantMatches) {
154 |             existingConstants.set(match[1], match[2]);
155 |         }
156 |     } catch {
157 |         // File doesn't exist, that's fine
158 |     }
159 | 
160 |     // Generate unique asset names from new assets
161 |     const uniqueAssets = assets.reduce((acc, asset) => {
162 |         const baseName = asset.filename.replace(/@\d+x/, '').replace(/\.[^.]+$/, '');
163 |         if (!acc[baseName]) {
164 |             acc[baseName] = asset;
165 |         }
166 |         return acc;
167 |     }, {} as Record<string, any>);
168 | 
169 |     // Add new constants to existing ones
170 |     Object.entries(uniqueAssets).forEach(([baseName, asset]) => {
171 |         const constantName = toCamelCase(asset.nodeName);
172 |         const assetPath = `assets/images/${baseName}.png`; // Use base resolution
173 |         existingConstants.set(constantName, assetPath);
174 |     });
175 | 
176 |     // Generate the complete constants file
177 |     let constantsContent = `// Generated asset constants\n// Do not edit manually\n\nclass Assets {\n`;
178 | 
179 |     // Sort constants alphabetically for consistency
180 |     const sortedConstants = Array.from(existingConstants.entries()).sort(([a], [b]) => a.localeCompare(b));
181 |     sortedConstants.forEach(([constantName, assetPath]) => {
182 |         constantsContent += `  static const String ${constantName} = '${assetPath}';\n`;
183 |     });
184 | 
185 |     constantsContent += `}\n`;
186 | 
187 |     await writeFile(constantsPath, constantsContent);
188 |     return constantsPath;
189 | }
190 | 
191 | export async function generateSvgAssetConstants(assets: Array<{filename: string, nodeName: string}>, projectPath: string): Promise<string> {
192 |     const constantsDir = join(projectPath, 'lib', 'constants');
193 |     await mkdir(constantsDir, {recursive: true});
194 | 
195 |     const constantsPath = join(constantsDir, 'svg_assets.dart');
196 | 
197 |     // Read existing SVG constants if they exist
198 |     const existingConstants = new Map<string, string>();
199 |     try {
200 |         const existingContent = await readFile(constantsPath, 'utf-8');
201 |         // Extract existing constants using regex
202 |         const constantMatches = existingContent.matchAll(/static const String (\w+) = '([^']+)';/g);
203 |         for (const match of constantMatches) {
204 |             existingConstants.set(match[1], match[2]);
205 |         }
206 |     } catch {
207 |         // File doesn't exist, that's fine
208 |     }
209 | 
210 |     // Generate unique SVG asset names from new assets
211 |     const uniqueAssets = assets.reduce((acc, asset) => {
212 |         const baseName = asset.filename.replace(/\.svg$/, '');
213 |         if (!acc[baseName]) {
214 |             acc[baseName] = asset;
215 |         }
216 |         return acc;
217 |     }, {} as Record<string, any>);
218 | 
219 |     // Add new constants to existing ones
220 |     Object.entries(uniqueAssets).forEach(([baseName, asset]) => {
221 |         const constantName = toCamelCase(asset.nodeName);
222 |         const assetPath = `assets/svgs/${baseName}.svg`;
223 |         existingConstants.set(constantName, assetPath);
224 |     });
225 | 
226 |     // Generate the complete SVG constants file
227 |     let constantsContent = `// Generated SVG asset constants\n// Do not edit manually\n\nclass SvgAssets {\n`;
228 | 
229 |     // Sort constants alphabetically for consistency
230 |     const sortedConstants = Array.from(existingConstants.entries()).sort(([a], [b]) => a.localeCompare(b));
231 |     sortedConstants.forEach(([constantName, assetPath]) => {
232 |         constantsContent += `  static const String ${constantName} = '${assetPath}';\n`;
233 |     });
234 | 
235 |     constantsContent += `}\n`;
236 | 
237 |     await writeFile(constantsPath, constantsContent);
238 |     return constantsPath;
239 | }
240 | 
241 | export function groupAssetsByBaseName(assets: Array<{filename: string, nodeName: string, size: string}>): Record<string, Array<{filename: string, size: string}>> {
242 |     return assets.reduce((acc, asset) => {
243 |         const baseName = asset.filename.replace(/@\d+x/, '').replace(/\.[^.]+$/, '');
244 |         if (!acc[baseName]) {
245 |             acc[baseName] = [];
246 |         }
247 |         acc[baseName].push({
248 |             filename: asset.filename,
249 |             size: asset.size
250 |         });
251 |         return acc;
252 |     }, {} as Record<string, Array<{filename: string, size: string}>>);
253 | }
254 | 
255 | function toCamelCase(str: string): string {
256 |     return str
257 |         .toLowerCase()
258 |         .replace(/[^a-z0-9]/g, '_')
259 |         .replace(/_+/g, '_')
260 |         .replace(/^_|_$/g, '')
261 |         .replace(/_(.)/g, (_, char) => char.toUpperCase());
262 | }
263 | 
```

--------------------------------------------------------------------------------
/docs/figma-framework-mcp.md:
--------------------------------------------------------------------------------

```markdown
  1 | ## Figma-Framework MCP
  2 | Since [Figma Context MCP](https://github.com/gLips/Figma-Context-MCP/) is framework‑agnostic, it does not output code or artifacts tailored to React, Angular, Vue, Flutter, etc. This repository adds a concrete implementation for Flutter (see `docs/figma-flutter-mcp.md`) and documents how to adapt the same architecture to any other framework.
  3 | 
  4 | ### What you get out of the box
  5 | - **Extractors (framework‑agnostic):** Parse Figma nodes into consistent, rich, typed models for components, screens, colors, and typography.
  6 | - **Framework tools (Flutter today):** Wrap extractors, add framework‑specific heuristics, asset export, guidance, and code generation.
  7 | - **CLI/MCP entry points:** Commands and tools to analyze Figma nodes and generate artifacts into a project.
  8 | 
  9 | ## Architecture overview
 10 | 
 11 | ### 1) Extractors (framework‑agnostic core)
 12 | Extractors live under `src/extractors/` and work the same regardless of the target framework. They are responsible for traversing Figma nodes and producing structured analysis results that downstream tools can consume.
 13 | 
 14 | - **Components:** `src/extractors/components/`
 15 |   - Entry points: `analyzeComponent`, `analyzeComponentWithVariants`
 16 |   - Outputs `ComponentAnalysis` with metadata, layout, styling, children, nested components, and variants when applicable.
 17 | - **Screens:** `src/extractors/screens/`
 18 |   - Entry point: `analyzeScreen`
 19 |   - Outputs `ScreenAnalysis` with sections (header/content/footer/navigation), assets, and nested components.
 20 | - **Colors:** `src/extractors/colors/`
 21 |   - Entry points: `extractThemeColors`, `extractColorsFromThemeFrame`
 22 |   - Outputs normalized theme color swatches.
 23 | - **Typography:** `src/extractors/typography/`
 24 |   - Entry point: `extractThemeTypography`
 25 |   - Outputs normalized text styles with font family, size, weight, and line height.
 26 | 
 27 | These modules use Figma API types (e.g., `FigmaNode`, `FigmaColor`, `FigmaEffect`) and heuristics (e.g., `node.type === 'TEXT' | 'FRAME' | 'COMPONENT'`) to create consistent data across frameworks. The output types are intentionally neutral so they can be mapped to any target tech stack.
 28 | 
 29 | > ⚠️ You can add more in the list if the targeted framework needs it.
 30 | 
 31 | ### 2) Framework tools (target‑specific glue)
 32 | Tools live under `src/tools/<framework>/` and are responsible for turning extractor outputs into actionable, framework‑specific results (code, guidance, assets, configuration). For Flutter these are under `src/tools/flutter/`.
 33 | 
 34 | Tools typically do the following:
 35 | - **Orchestrate extractors** (call `analyzeComponent`, `analyzeScreen`, etc.).
 36 | - **Apply classification** (e.g., treat `COMPONENT`/`INSTANCE` as reusable widgets; treat `FRAME` as a screen).
 37 | - **Map design semantics** to framework widgets/components (containers, text, icons, layout primitives).
 38 | - **Generate code or guidance** (e.g., Flutter widget tree suggestions in `src/tools/flutter/components/helpers.mts`).
 39 | - **Export assets** and update project manifests (images, SVGs, `pubspec.yaml` for Flutter).
 40 | - **Integrate theming** by referencing color schemes and text themes rather than hardcoding styles when possible.
 41 | 
 42 | In other words, extractors give you high‑quality design insights; tools translate those insights into framework‑specific outputs.
 43 | 
 44 | ## 📝 Porting to a new framework (React, React Native, Angular, Vue)
 45 | 
 46 | The fastest path is to keep the extractors intact and replace the Flutter‑specific tools with your framework’s equivalents.
 47 | 
 48 | ### Folder layout
 49 | - Create `src/tools/react/` (or `react-native`, `angular`, `vue`).
 50 | - Add submodules as needed, mirroring the Flutter structure:
 51 |   - `components/` (analyze and generate component code)
 52 |   - `screens/` (analyze frames as screens/pages)
 53 |   - `assets/` (export images/SVGs and manage paths)
 54 |   - `theme/` (map colors/typography to your framework’s theming system)
 55 |   - `helpers.mts` (framework‑specific guidance and code snippets)
 56 | 
 57 | ### Minimal tool set to implement
 58 | - A tool to analyze a component or component set (wraps `analyzeComponent` and optionally variant handling).
 59 | - A tool to analyze a full screen (wraps `analyzeScreen`).
 60 | - Optional generators to emit framework code:
 61 |   - React: `.tsx` components in `src/components/`, pages in `src/pages/` or routes.
 62 |   - React Native: `.tsx` components using `View`, `Text`, `Image`, `StyleSheet`.
 63 |   - Angular: `.ts/.html/.scss` with schematics for components and modules.
 64 |   - Vue: `.vue` Single File Components with `<template>`, `<script>`, and `<style>`.
 65 | 
 66 | ### Mapping guidelines (how to translate extractor output)
 67 | - **Layout**
 68 |   - Auto‑layout (direction + spacing) → React/React Native: `display: flex; flexDirection: row|column; gap/margins`; Angular/Vue: templates + CSS.
 69 |   - Absolute/stacked → React/React Native: `position: 'absolute'`/wrapper; Angular/Vue: positioned containers.
 70 | - **Styling**
 71 |   - Fills, strokes, corner radii, shadows → map to CSS/CSS‑in‑JS/StyleSheet equivalents.
 72 |   - Effects (`DROP_SHADOW`, `INNER_SHADOW`, blurs) → CSS `box-shadow`, `filter`, or platform‑specific fallbacks.
 73 | - **Text**
 74 |   - Use `textInfo` to choose the right semantic component (e.g., `h1/h2`, `Button`, `Link`) or apply a `Text` with style.
 75 | - **Components vs screens**
 76 |   - Treat `COMPONENT`/`INSTANCE` as reusable components.
 77 |   - Treat `FRAME` as a screen/page (with a child‑count threshold if you adopt the same heuristic as Flutter tools).
 78 | - **Theming**
 79 |   - Reference theme tokens (colors/typography) instead of hardcoding styles. For React, prefer ThemeProvider (e.g., MUI, styled‑components, or your design system).
 80 | - **Assets**
 81 |   - Export images/SVGs and write importable paths using your framework’s conventions (e.g., `public/` for Vite/Next.js, Android/iOS asset catalogs for React Native).
 82 | 
 83 | ### Example: replacing Flutter guidance with React guidance
 84 | Flutter’s helper (`src/tools/flutter/components/helpers.mts`) produces guidance like "Use Row/Column" or `BoxDecoration` for containers. For React, your equivalent helper would:
 85 | 
 86 | - Recommend `div`/`section` structure with `display: flex` and `flexDirection`.
 87 | - Map paddings/margins/spacing to CSS or a CSS‑in‑JS solution.
 88 | - Suggest semantic elements for headings/links/buttons.
 89 | - Show small code snippets using your preferred stack (e.g., React + CSS Modules or styled‑components).
 90 | 
 91 | Example guidance output snippet (React):
 92 | 
 93 | ```tsx
 94 | // Container layout
 95 | <div style={{ display: 'flex', flexDirection: 'column', gap: 12, padding: 16 }}>
 96 |   {/* ...children */}
 97 | </div>
 98 | ```
 99 | 
100 | ### Code generation (optional but recommended)
101 | Just like the Flutter tools can emit widgets, you can add a lightweight generator to create files and wire imports:
102 | 
103 | - **React**
104 |   - Components → `src/components/<PascalName>/<PascalName>.tsx`
105 |   - Screens/Pages → `src/pages/<kebab-name>.tsx` (or Next.js routes)
106 |   - Styles → collocate via CSS Modules, styled‑components, Tailwind, etc.
107 | - **React Native**
108 |   - Components → `src/components/<PascalName>.tsx`
109 |   - Use `StyleSheet.create({...})` for styles. Map shadows/radius/fills accordingly.
110 | - **Angular**
111 |   - Use schematics to scaffold `component.ts/html/scss` and update module declarations.
112 | - **Vue**
113 |   - Generate `.vue` SFCs with `<template>` mapped from layout and `<style>` from styling info.
114 | 
115 | ### Assets and theme integration
116 | - Reuse color/typography extractors to populate your theme tokens.
117 | - Export image/SVG assets and centralize paths/constants for your framework.
118 | - Keep asset policies explicit (e.g., which scales to export, where to write constants) and append new entries rather than overwriting existing ones.
119 | 
120 | ### Suggested workflow to add a new framework
121 | 1. Create `src/tools/<framework>/` with `components/`, `screens/`, `assets/`, `helpers.mts`.
122 | 2. Wrap extractors (`analyzeComponent`, `analyzeScreen`) and print a minimal analysis report.
123 | 3. Add guidance helpers that output idiomatic snippets for your framework.
124 | 4. Add asset export and theme token mapping.
125 | 5. (Optional) Add code generation and a small registry to prevent duplicate components.
126 | 6. Register your tools in `src/tools/index.mts` so they’re available via MCP/CLI.
127 | 7. Update docs with usage examples and flags.
128 | 
129 | ## 🤨 FAQs
130 | 
131 | ### What is the role of extractors?
132 | Extractors are the framework‑agnostic heart of the system. They convert Figma data into structured, typed models (components, screens, colors, typography) that downstream tools can map to any framework without re‑implementing Figma traversal.
133 | 
134 | ### What is the role of tools?
135 | Tools are the framework‑specific bridge. They orchestrate extractors, apply heuristics (e.g., classify component vs screen), generate guidance or code, export assets, and integrate with a project’s conventions (file layout, theming, configuration).
136 | 
137 | ### I’m building for React. What do I change?
138 | - Do not modify extractors.
139 | - Create `src/tools/react/` with your own helpers and generators.
140 | - Replace Flutter‑specific guidance with React guidance (flexbox, semantic HTML, CSS‑in‑JS, etc.).
141 | - Implement asset export paths and theme references that match your React app setup.
142 | 
143 | If you’d like a concrete Flutter reference, see `src/tools/flutter/components/helpers.mts` and `src/tools/flutter/screens/screen-tool.mts` and mirror their responsibilities for your framework.
144 | 
145 | ---
146 | 
147 | If you fork this repository to support another framework (React, React Native, Angular, Vue), please keep it open source so others can build on top of the extractor core and share improvements to the tooling layers.
```
Page 1/4FirstPrevNextLast