#
tokens: 48258/50000 47/73 files (page 1/8)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 8. Use http://codebase.md/1yhy/figma-context-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .editorconfig
├── .env.example
├── .github
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows
│       └── ci.yml
├── .gitignore
├── .husky
│   ├── commit-msg
│   └── pre-commit
├── .lintstagedrc.json
├── .nvmrc
├── .prettierrc
├── CHANGELOG.md
├── commitlint.config.js
├── CONTRIBUTING.md
├── Dockerfile
├── docs
│   ├── en
│   │   ├── absolute-to-relative-research.md
│   │   ├── architecture.md
│   │   ├── cache-architecture.md
│   │   ├── grid-layout-research.md
│   │   ├── icon-detection.md
│   │   ├── layout-detection-research.md
│   │   └── layout-detection.md
│   └── zh-CN
│       ├── absolute-to-relative-research.md
│       ├── architecture.md
│       ├── cache-architecture.md
│       ├── grid-layout-research.md
│       ├── icon-detection.md
│       ├── layout-detection-research.md
│       ├── layout-detection.md
│       └── TODO-feature-enhancements.md
├── eslint.config.js
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README.md
├── README.zh-CN.md
├── scripts
│   ├── fetch-test-data.ts
│   └── optimize-figma-json.ts
├── smithery.yaml
├── src
│   ├── algorithms
│   │   ├── icon
│   │   │   ├── detector.ts
│   │   │   └── index.ts
│   │   └── layout
│   │       ├── detector.ts
│   │       ├── index.ts
│   │       ├── optimizer.ts
│   │       └── spatial.ts
│   ├── config.ts
│   ├── core
│   │   ├── effects.ts
│   │   ├── layout.ts
│   │   ├── parser.ts
│   │   └── style.ts
│   ├── index.ts
│   ├── prompts
│   │   ├── design-to-code.ts
│   │   └── index.ts
│   ├── resources
│   │   ├── figma-resources.ts
│   │   └── index.ts
│   ├── server.ts
│   ├── services
│   │   ├── cache
│   │   │   ├── cache-manager.ts
│   │   │   ├── disk-cache.ts
│   │   │   ├── index.ts
│   │   │   ├── lru-cache.ts
│   │   │   └── types.ts
│   │   ├── cache.ts
│   │   ├── figma.ts
│   │   └── simplify-node-response.ts
│   ├── types
│   │   ├── figma.ts
│   │   ├── index.ts
│   │   └── simplified.ts
│   └── utils
│       ├── color.ts
│       ├── css.ts
│       ├── file.ts
│       └── validation.ts
├── tests
│   ├── fixtures
│   │   ├── expected
│   │   │   ├── node-240-32163-optimized.json
│   │   │   ├── node-402-34955-optimized.json
│   │   │   └── real-node-data-optimized.json
│   │   └── figma-data
│   │       ├── node-240-32163.json
│   │       ├── node-402-34955.json
│   │       └── real-node-data.json
│   ├── integration
│   │   ├── __snapshots__
│   │   │   ├── layout-optimization.test.ts.snap
│   │   │   └── output-quality.test.ts.snap
│   │   ├── layout-optimization.test.ts
│   │   ├── output-quality.test.ts
│   │   └── parser.test.ts
│   ├── unit
│   │   ├── algorithms
│   │   │   ├── icon-optimization.test.ts
│   │   │   ├── icon.test.ts
│   │   │   └── layout.test.ts
│   │   ├── resources
│   │   │   └── figma-resources.test.ts
│   │   └── services
│   │       └── cache.test.ts
│   └── utils
│       ├── preview-generator.ts
│       ├── preview.ts
│       ├── run-simplification.ts
│       └── viewer.html
├── tsconfig.json
├── tsup.config.ts
└── vitest.config.ts
```

# Files

--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------

```
1 | v20
```

--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------

```
1 | {
2 |   "semi": true,
3 |   "trailingComma": "all",
4 |   "singleQuote": false,
5 |   "printWidth": 100,
6 |   "tabWidth": 2,
7 |   "useTabs": false
8 | }
9 | 
```

--------------------------------------------------------------------------------
/.lintstagedrc.json:
--------------------------------------------------------------------------------

```json
1 | {
2 |   "src/**/*.{ts,tsx}": ["eslint --fix", "prettier --write"],
3 |   "tests/**/*.{ts,tsx}": ["eslint --fix", "prettier --write"],
4 |   "*.{json,md,yml,yaml}": ["prettier --write"]
5 | }
6 | 
```

--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------

```
 1 | # EditorConfig - https://editorconfig.org
 2 | root = true
 3 | 
 4 | [*]
 5 | charset = utf-8
 6 | end_of_line = lf
 7 | insert_final_newline = true
 8 | trim_trailing_whitespace = true
 9 | indent_style = space
10 | indent_size = 2
11 | 
12 | [*.md]
13 | trim_trailing_whitespace = false
14 | 
15 | [*.{json,yml,yaml}]
16 | indent_size = 2
17 | 
18 | [Makefile]
19 | indent_style = tab
20 | 
```

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

```
 1 | # Dependencies
 2 | node_modules
 3 | .pnpm-store
 4 | 
 5 | # Build output
 6 | dist
 7 | 
 8 | # Environment variables
 9 | .env
10 | .env.local
11 | .env.*.local
12 | 
13 | # IDE
14 | .vscode/*
15 | !.vscode/extensions.json
16 | !.vscode/settings.json
17 | .idea
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 | *.sw?
23 | 
24 | # Logs
25 | logs
26 | *.log
27 | npm-debug.log*
28 | yarn-debug.log*
29 | yarn-error.log*
30 | pnpm-debug.log*
31 | 
32 | # Testing
33 | coverage
34 | tests/utils/simplified-with-css.json
35 | 
36 | # OS
37 | .DS_Store
38 | Thumbs.db 
```

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

```markdown
  1 | <h1 align="center">
  2 |   <br>
  3 |   <img src="https://upload.wikimedia.org/wikipedia/commons/3/33/Figma-logo.svg" alt="Figma MCP" width="80">
  4 |   <br>
  5 |   Figma Context MCP
  6 |   <br>
  7 | </h1>
  8 | 
  9 | <p align="center">
 10 |   <strong>MCP server for seamless Figma design integration with AI coding tools</strong>
 11 | </p>
 12 | 
 13 | <p align="center">
 14 |   <a href="https://smithery.ai/server/@1yhy/Figma-Context-MCP">
 15 |     <img src="https://smithery.ai/badge/@1yhy/Figma-Context-MCP" alt="Smithery Badge">
 16 |   </a>
 17 |   <a href="https://www.npmjs.com/package/@yhy2001/figma-mcp-server">
 18 |     <img src="https://img.shields.io/npm/v/@yhy2001/figma-mcp-server" alt="npm version">
 19 |   </a>
 20 |   <a href="https://github.com/1yhy/Figma-Context-MCP/blob/main/LICENSE">
 21 |     <img src="https://img.shields.io/github/license/1yhy/Figma-Context-MCP" alt="License">
 22 |   </a>
 23 |   <a href="https://github.com/1yhy/Figma-Context-MCP/stargazers">
 24 |     <img src="https://img.shields.io/github/stars/1yhy/Figma-Context-MCP" alt="Stars">
 25 |   </a>
 26 |   <img src="https://img.shields.io/badge/TypeScript-5.7-blue?logo=typescript" alt="TypeScript">
 27 |   <img src="https://img.shields.io/badge/MCP-1.24-green" alt="MCP SDK">
 28 | </p>
 29 | 
 30 | <p align="center">
 31 |   <a href="#features">Features</a> •
 32 |   <a href="#quick-start">Quick Start</a> •
 33 |   <a href="#mcp-capabilities">MCP Capabilities</a> •
 34 |   <a href="#architecture">Architecture</a> •
 35 |   <a href="#documentation">Documentation</a> •
 36 |   <a href="./README.zh-CN.md">中文文档</a>
 37 | </p>
 38 | 
 39 | ---
 40 | 
 41 | ## What is This?
 42 | 
 43 | Figma Context MCP is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that bridges Figma designs with AI coding assistants like [Cursor](https://cursor.sh/), [Windsurf](https://codeium.com/windsurf), and [Cline](https://cline.bot/).
 44 | 
 45 | When AI tools can access Figma design data directly, they generate more accurate code on the first try—far better than using screenshots.
 46 | 
 47 | > **Note**: This project is based on [Figma-Context-MCP](https://github.com/GLips/Figma-Context-MCP), with optimized data structures and intelligent layout detection algorithms.
 48 | 
 49 | ## Features
 50 | 
 51 | ### Core Capabilities
 52 | 
 53 | | Capability                      | Description                                                         |
 54 | | ------------------------------- | ------------------------------------------------------------------- |
 55 | | **Smart Layout Detection**      | Automatically infers Flexbox/Grid layouts from absolute positioning |
 56 | | **Icon Merging**                | Intelligently merges vector layers into single exportable icons     |
 57 | | **CSS Generation**              | Converts Figma styles to clean, usable CSS                          |
 58 | | **Image Export**                | Downloads images and icons with proper naming                       |
 59 | | **Multi-layer Caching**         | L1 memory + L2 disk cache to reduce API calls                       |
 60 | | **Design-to-Code Prompts**      | Built-in professional prompt templates to guide AI code generation  |
 61 | | **Lightweight Resource Access** | Resources API provides low-token data access                        |
 62 | 
 63 | ### Key Improvements
 64 | 
 65 | | Feature          | Before          | After                           |
 66 | | ---------------- | --------------- | ------------------------------- |
 67 | | Icon exports     | ~45 fragmented  | 2 merged (96% reduction)        |
 68 | | Layout detection | Manual absolute | Auto Flexbox/Grid inference     |
 69 | | CSS output       | Raw values      | Optimized with defaults removed |
 70 | | API calls        | Every request   | 24-hour smart caching           |
 71 | 
 72 | ## Quick Start
 73 | 
 74 | ### Prerequisites
 75 | 
 76 | - Node.js >= 18.0.0
 77 | - A Figma account with API access
 78 | 
 79 | ### Installation
 80 | 
 81 | **Via Smithery (Recommended)**
 82 | 
 83 | ```bash
 84 | npx -y @smithery/cli install @1yhy/Figma-Context-MCP --client claude
 85 | ```
 86 | 
 87 | **Via npm**
 88 | 
 89 | ```bash
 90 | npm install -g @yhy2001/figma-mcp-server
 91 | ```
 92 | 
 93 | **From Source**
 94 | 
 95 | ```bash
 96 | git clone https://github.com/1yhy/Figma-Context-MCP.git
 97 | cd Figma-Context-MCP
 98 | pnpm install
 99 | pnpm build
100 | ```
101 | 
102 | ### Configuration
103 | 
104 | #### 1. Get Figma API Token
105 | 
106 | 1. Go to [Figma Account Settings](https://www.figma.com/settings)
107 | 2. Scroll to "Personal access tokens"
108 | 3. Click "Create new token"
109 | 4. Copy the token
110 | 
111 | #### 2. Configure Your AI Tool
112 | 
113 | <details>
114 | <summary><strong>Cursor / Windsurf / Cline</strong></summary>
115 | 
116 | Add to your MCP configuration file:
117 | 
118 | ```json
119 | {
120 |   "mcpServers": {
121 |     "Figma": {
122 |       "command": "npx",
123 |       "args": ["-y", "@yhy2001/figma-mcp-server", "--stdio"],
124 |       "env": {
125 |         "FIGMA_API_KEY": "your-figma-api-key"
126 |       }
127 |     }
128 |   }
129 | }
130 | ```
131 | 
132 | </details>
133 | 
134 | <details>
135 | <summary><strong>HTTP/SSE Mode (Local Development)</strong></summary>
136 | 
137 | ```bash
138 | # From source (development)
139 | cp .env.example .env  # Add FIGMA_API_KEY to .env
140 | pnpm install && pnpm build
141 | pnpm start            # Starts on port 3333
142 | 
143 | # Or with environment variable
144 | FIGMA_API_KEY=<your-key> pnpm start
145 | 
146 | # Or via global install
147 | figma-mcp --figma-api-key=<your-key> --port=3333
148 | 
149 | # Connect via SSE
150 | # URL: http://localhost:3333/sse
151 | ```
152 | 
153 | </details>
154 | 
155 | ### Usage Example
156 | 
157 | ```
158 | Please implement this Figma design: https://www.figma.com/design/abc123/MyDesign?node-id=1:234
159 | 
160 | Use React and Tailwind CSS.
161 | ```
162 | 
163 | ---
164 | 
165 | ## MCP Capabilities
166 | 
167 | This server provides full MCP capabilities support:
168 | 
169 | ```
170 | ┌─────────────────────────────────────────────────────────────┐
171 | │                  Figma MCP Server v1.1.0                    │
172 | ├─────────────────────────────────────────────────────────────┤
173 | │  Tools (2)                        AI-invoked operations     │
174 | │  ├── get_figma_data              Fetch design data          │
175 | │  └── download_figma_images       Download image assets      │
176 | ├─────────────────────────────────────────────────────────────┤
177 | │  Prompts (3)                      User-selected templates   │
178 | │  ├── design_to_code              Full design-to-code flow   │
179 | │  ├── analyze_components          Component structure        │
180 | │  └── extract_styles              Style token extraction     │
181 | ├─────────────────────────────────────────────────────────────┤
182 | │  Resources (5)                    Lightweight data sources  │
183 | │  ├── figma://help                Usage guide                │
184 | │  ├── figma://file/{key}          File metadata (~200 tok)   │
185 | │  ├── figma://file/{key}/styles   Design tokens (~500 tok)   │
186 | │  ├── figma://file/{key}/components Component list (~300 tok)│
187 | │  └── figma://file/{key}/assets   Asset inventory (~400 tok) │
188 | └─────────────────────────────────────────────────────────────┘
189 | ```
190 | 
191 | ### Tools
192 | 
193 | | Tool                    | Description                  | Parameters                        |
194 | | ----------------------- | ---------------------------- | --------------------------------- |
195 | | `get_figma_data`        | Fetch simplified design data | `fileKey`, `nodeId?`, `depth?`    |
196 | | `download_figma_images` | Download images and icons    | `fileKey`, `nodes[]`, `localPath` |
197 | 
198 | ### Prompts
199 | 
200 | Built-in professional prompt templates to help AI generate high-quality code:
201 | 
202 | | Prompt               | Description                                 | Parameters                         |
203 | | -------------------- | ------------------------------------------- | ---------------------------------- |
204 | | `design_to_code`     | Complete design-to-code workflow            | `framework?`, `includeResponsive?` |
205 | | `analyze_components` | Analyze component structure and reusability | -                                  |
206 | | `extract_styles`     | Extract design tokens                       | -                                  |
207 | 
208 | **design_to_code workflow includes:**
209 | 
210 | 1. **Project Analysis** - Read theme config, global styles, component library
211 | 2. **Structure Analysis** - Identify page patterns, component splitting strategy
212 | 3. **ASCII Layout Blueprint** - Generate layout diagram with component and asset annotations
213 | 4. **Asset Management** - Analyze, download, and organize images/icons
214 | 5. **Code Generation** - Generate code following project conventions
215 | 6. **Accessibility Optimization** - Semantic HTML, ARIA labels
216 | 7. **Responsive Adaptation** - Mobile layout adjustments
217 | 
218 | ### Resources
219 | 
220 | Lightweight data access to save tokens:
221 | 
222 | ```bash
223 | # Get file metadata (~200 tokens)
224 | figma://file/abc123
225 | 
226 | # Get design tokens (~500 tokens)
227 | figma://file/abc123/styles
228 | 
229 | # Get component list (~300 tokens)
230 | figma://file/abc123/components
231 | 
232 | # Get asset inventory (~400 tokens)
233 | figma://file/abc123/assets
234 | ```
235 | 
236 | **Resources vs Tools comparison:**
237 | 
238 | | Feature    | Tools              | Resources             |
239 | | ---------- | ------------------ | --------------------- |
240 | | Controller | AI auto-invoked    | User/client initiated |
241 | | Token cost | Higher (full data) | Lower (summaries)     |
242 | | Use case   | Execute actions    | Browse and explore    |
243 | 
244 | ---
245 | 
246 | ## Architecture
247 | 
248 | ```
249 | ┌──────────────────────────────────────────────────────────────┐
250 | │                        MCP Server                            │
251 | ├──────────────────────────────────────────────────────────────┤
252 | │  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐   │
253 | │  │   Tools     │  │   Prompts   │  │     Resources       │   │
254 | │  │ (2 tools)   │  │ (3 prompts) │  │   (5 resources)     │   │
255 | │  └──────┬──────┘  └─────────────┘  └──────────┬──────────┘   │
256 | │         │                                      │              │
257 | │         └──────────────────┬───────────────────┘              │
258 | │                            ▼                                  │
259 | │  ┌────────────────────────────────────────────────────────┐  │
260 | │  │                   FigmaService                         │  │
261 | │  │          API Calls • Validation • Error Handling       │  │
262 | │  └────────────────────────┬───────────────────────────────┘  │
263 | │                           │                                  │
264 | │         ┌─────────────────┴─────────────────┐                │
265 | │         ▼                                   ▼                │
266 | │  ┌─────────────────┐              ┌─────────────────────┐    │
267 | │  │   CacheManager  │              │    Parser + Algo    │    │
268 | │  │  L1: LRU Memory │              │  • Layout Detection │    │
269 | │  │  L2: Disk Store │              │  • Icon Merging     │    │
270 | │  └─────────────────┘              │  • CSS Generation   │    │
271 | │                                   └─────────────────────┘    │
272 | └──────────────────────────────────────────────────────────────┘
273 | ```
274 | 
275 | ### Cache System
276 | 
277 | Two-layer cache architecture significantly reduces API calls:
278 | 
279 | | Layer | Storage    | Capacity              | TTL      | Purpose              |
280 | | ----- | ---------- | --------------------- | -------- | -------------------- |
281 | | L1    | Memory LRU | 100 nodes / 50 images | 5-10 min | Hot data fast access |
282 | | L2    | Disk       | 500MB                 | 24 hours | Persistent cache     |
283 | 
284 | ### Layout Detection Algorithm
285 | 
286 | Automatically converts absolute positioning to semantic Flexbox/Grid layouts:
287 | 
288 | ```
289 | Input (Figma absolute positioning):
290 | ┌─────────────────────────┐
291 | │ ■ (10,10)  ■ (110,10)   │
292 | │ ■ (10,60)  ■ (110,60)   │
293 | └─────────────────────────┘
294 | 
295 | Output (Inferred Grid):
296 | display: grid
297 | grid-template-columns: 100px 100px
298 | grid-template-rows: 50px 50px
299 | gap: 10px
300 | ```
301 | 
302 | ---
303 | 
304 | ## Project Structure
305 | 
306 | ```
307 | src/
308 | ├── algorithms/           # Smart algorithms
309 | │   ├── layout/          # Layout detection (Flex/Grid inference)
310 | │   └── icon/            # Icon merge detection
311 | ├── core/                 # Core parsing
312 | │   ├── parser.ts        # Figma data parser
313 | │   ├── style.ts         # CSS style generation
314 | │   ├── layout.ts        # Layout processing
315 | │   └── effects.ts       # Effects handling
316 | ├── services/             # Service layer
317 | │   ├── figma.ts         # Figma API client
318 | │   └── cache/           # Multi-layer cache system
319 | ├── prompts/              # MCP prompt templates
320 | ├── resources/            # MCP resource handlers
321 | ├── types/                # TypeScript type definitions
322 | ├── utils/                # Utility functions
323 | ├── server.ts             # MCP server main entry
324 | └── index.ts              # CLI entry
325 | 
326 | tests/
327 | ├── fixtures/             # Test data
328 | │   ├── figma-data/      # Raw JSON from Figma API
329 | │   └── expected/        # Expected output snapshots
330 | ├── integration/          # Integration tests
331 | │   ├── layout-optimization.test.ts  # Layout optimization tests
332 | │   ├── output-quality.test.ts       # Output quality validation
333 | │   └── parser.test.ts               # Parser tests
334 | └── unit/                 # Unit tests
335 |     ├── algorithms/      # Algorithm tests (layout, icon detection)
336 |     ├── resources/       # Resource handler tests
337 |     └── services/        # Service layer tests
338 | 
339 | scripts/
340 | └── fetch-test-data.ts   # Figma test data fetcher
341 | ```
342 | 
343 | ---
344 | 
345 | ## Documentation
346 | 
347 | ### Core Algorithms
348 | 
349 | | English                                               | 中文                                               |
350 | | ----------------------------------------------------- | -------------------------------------------------- |
351 | | [Layout Detection](./docs/en/layout-detection.md)     | [布局检测算法](./docs/zh-CN/layout-detection.md)   |
352 | | [Icon Detection](./docs/en/icon-detection.md)         | [图标检测算法](./docs/zh-CN/icon-detection.md)     |
353 | | [Cache Architecture](./docs/en/cache-architecture.md) | [缓存架构设计](./docs/zh-CN/cache-architecture.md) |
354 | 
355 | ### Research Documents
356 | 
357 | | English                                                             | 中文                                                      |
358 | | ------------------------------------------------------------------- | --------------------------------------------------------- |
359 | | [Grid Layout Research](./docs/en/grid-layout-research.md)           | [Grid 布局研究](./docs/zh-CN/grid-layout-research.md)     |
360 | | [Layout Detection Research](./docs/en/layout-detection-research.md) | [布局检测研究](./docs/zh-CN/layout-detection-research.md) |
361 | 
362 | ### Architecture Documents
363 | 
364 | | English                                   | 中文                                     |
365 | | ----------------------------------------- | ---------------------------------------- |
366 | | [Architecture](./docs/en/architecture.md) | [系统架构](./docs/zh-CN/architecture.md) |
367 | 
368 | ---
369 | 
370 | ## Command Line Options
371 | 
372 | | Option            | Description               | Default  |
373 | | ----------------- | ------------------------- | -------- |
374 | | `--figma-api-key` | Figma API token           | Required |
375 | | `--port`          | Server port for HTTP mode | 3333     |
376 | | `--stdio`         | Run in stdio mode         | false    |
377 | | `--help`          | Show help                 | -        |
378 | 
379 | ---
380 | 
381 | ## Contributing
382 | 
383 | Contributions are welcome!
384 | 
385 | ```bash
386 | # Setup
387 | git clone https://github.com/1yhy/Figma-Context-MCP.git
388 | cd Figma-Context-MCP
389 | pnpm install
390 | 
391 | # Development
392 | pnpm dev          # Watch mode
393 | pnpm test         # Run tests (272 test cases)
394 | pnpm lint         # Lint code
395 | pnpm build        # Build
396 | 
397 | # Debug
398 | pnpm inspect      # MCP Inspector
399 | 
400 | # Test with your own Figma data
401 | pnpm tsx scripts/fetch-test-data.ts <fileKey> <nodeId> <outputName>
402 | 
403 | # Commit (uses conventional commits)
404 | git commit -m "feat: add new feature"
405 | ```
406 | 
407 | ### Commit Types
408 | 
409 | | Type       | Description   |
410 | | ---------- | ------------- |
411 | | `feat`     | New feature   |
412 | | `fix`      | Bug fix       |
413 | | `docs`     | Documentation |
414 | | `style`    | Code style    |
415 | | `refactor` | Refactoring   |
416 | | `test`     | Tests         |
417 | | `chore`    | Maintenance   |
418 | 
419 | ### Release Process (Maintainers)
420 | 
421 | ```bash
422 | # 1. Update version in package.json and CHANGELOG.md
423 | 
424 | # 2. Commit version bump
425 | git add -A
426 | git commit -m "chore: bump version to x.x.x"
427 | 
428 | # 3. Publish to npm (auto runs: type-check → lint → test → build)
429 | npm login --scope=@yhy2001  # if not logged in
430 | pnpm run pub:release
431 | 
432 | # 4. Create git tag and push
433 | git tag vx.x.x
434 | git push origin main --tags
435 | 
436 | # 5. Create GitHub Release (optional)
437 | # Go to https://github.com/1yhy/Figma-Context-MCP/releases/new
438 | ```
439 | 
440 | ### Testing with Your Own Figma Data
441 | 
442 | You can test the layout detection and optimization with your own Figma designs:
443 | 
444 | #### 1. Configure Environment Variables
445 | 
446 | ```bash
447 | # Copy the environment template
448 | cp .env.example .env
449 | 
450 | # Edit .env file with your configuration
451 | FIGMA_API_KEY=your_figma_api_key_here
452 | TEST_FIGMA_FILE_KEY=your_file_key      # Optional
453 | TEST_FIGMA_NODE_ID=your_node_id        # Optional
454 | ```
455 | 
456 | #### 2. Fetch Figma Node Data
457 | 
458 | ```bash
459 | # Method 1: Using command line arguments (recommended)
460 | pnpm tsx scripts/fetch-test-data.ts <fileKey> <nodeId> <outputName>
461 | 
462 | # Example: Fetch a specific node
463 | pnpm tsx scripts/fetch-test-data.ts UgtwrncR3GokKDIS7dpm4Z 402-34955 my-design
464 | 
465 | # Method 2: Using environment variables
466 | TEST_FIGMA_FILE_KEY=xxx TEST_FIGMA_NODE_ID=123-456 pnpm tsx scripts/fetch-test-data.ts
467 | ```
468 | 
469 | **Parameters:**
470 | 
471 | | Parameter    | Description           | How to Get                                                   |
472 | | ------------ | --------------------- | ------------------------------------------------------------ |
473 | | `fileKey`    | Figma file identifier | Part after `/design/` in URL, e.g., `UgtwrncR3GokKDIS7dpm4Z` |
474 | | `nodeId`     | Node ID               | `node-id=` parameter in URL, e.g., `402-34955`               |
475 | | `outputName` | Output filename       | Custom name, e.g., `my-design`                               |
476 | 
477 | **Example URL Parsing:**
478 | 
479 | ```
480 | https://www.figma.com/design/UgtwrncR3GokKDIS7dpm4Z/MyProject?node-id=402-34955
481 |                             ↑ fileKey                              ↑ nodeId
482 | ```
483 | 
484 | #### 3. Run Tests to Validate Output
485 | 
486 | ```bash
487 | # Run all tests
488 | pnpm test
489 | 
490 | # Run only integration tests (validate layout optimization)
491 | pnpm test tests/integration/
492 | 
493 | # View output JSON files
494 | ls tests/fixtures/figma-data/
495 | ```
496 | 
497 | #### 4. Analyze Optimization Results
498 | 
499 | Tests automatically validate:
500 | 
501 | - **Data Compression** - Typically >50% compression
502 | - **Layout Detection** - Flex/Grid layout recognition rate
503 | - **CSS Properties** - Redundant property cleanup
504 | - **Output Quality** - Structural consistency checks
505 | 
506 | If tests fail, the output may not meet expectations. Check error messages to adjust or report an issue.
507 | 
508 | ---
509 | 
510 | ## License
511 | 
512 | [MIT](./LICENSE) © 1yhy
513 | 
514 | ## Acknowledgments
515 | 
516 | - [Figma-Context-MCP](https://github.com/GLips/Figma-Context-MCP) - Original project
517 | - [Model Context Protocol](https://modelcontextprotocol.io/) - MCP specification
518 | - [Best-README-Template](https://github.com/othneildrew/Best-README-Template) - README template reference
519 | 
520 | ---
521 | 
522 | <p align="center">
523 |   Made with ❤️ for the AI coding community
524 | </p>
525 | 
```

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

```markdown
  1 | # Contributing to Figma Context MCP
  2 | 
  3 | Thank you for your interest in contributing to Figma Context MCP! This document provides guidelines and instructions for contributing.
  4 | 
  5 | ## Table of Contents
  6 | 
  7 | - [Code of Conduct](#code-of-conduct)
  8 | - [Getting Started](#getting-started)
  9 | - [Development Setup](#development-setup)
 10 | - [Making Changes](#making-changes)
 11 | - [Commit Guidelines](#commit-guidelines)
 12 | - [Pull Request Process](#pull-request-process)
 13 | - [Code Style](#code-style)
 14 | 
 15 | ## Code of Conduct
 16 | 
 17 | Please be respectful and constructive in all interactions. We welcome contributors from all backgrounds and experience levels.
 18 | 
 19 | ## Getting Started
 20 | 
 21 | 1. Fork the repository
 22 | 2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/Figma-Context-MCP.git`
 23 | 3. Add upstream remote: `git remote add upstream https://github.com/1yhy/Figma-Context-MCP.git`
 24 | 
 25 | ## Development Setup
 26 | 
 27 | ### Prerequisites
 28 | 
 29 | - Node.js >= 18.0.0
 30 | - pnpm (recommended) or npm
 31 | 
 32 | ### Installation
 33 | 
 34 | ```bash
 35 | # Install dependencies
 36 | pnpm install
 37 | 
 38 | # Build the project
 39 | pnpm build
 40 | 
 41 | # Run in development mode (with watch)
 42 | pnpm dev
 43 | ```
 44 | 
 45 | ### Running Tests
 46 | 
 47 | ```bash
 48 | # Run all tests
 49 | pnpm test
 50 | 
 51 | # Run specific test suites
 52 | pnpm test:layout    # Layout detection tests
 53 | pnpm test:icon      # Icon detection tests
 54 | pnpm test:all       # All tests including final output
 55 | ```
 56 | 
 57 | ### Code Quality
 58 | 
 59 | ```bash
 60 | # Type checking
 61 | pnpm type-check
 62 | 
 63 | # Linting
 64 | pnpm lint
 65 | pnpm lint:fix
 66 | 
 67 | # Formatting
 68 | pnpm format
 69 | ```
 70 | 
 71 | ## Making Changes
 72 | 
 73 | 1. Create a new branch from `main`:
 74 | 
 75 |    ```bash
 76 |    git checkout -b feat/your-feature-name
 77 |    # or
 78 |    git checkout -b fix/your-bug-fix
 79 |    ```
 80 | 
 81 | 2. Make your changes following the code style guidelines
 82 | 
 83 | 3. Test your changes locally
 84 | 
 85 | 4. Commit your changes following the commit guidelines
 86 | 
 87 | ## Commit Guidelines
 88 | 
 89 | We use [Conventional Commits](https://www.conventionalcommits.org/) for commit messages.
 90 | 
 91 | ### Format
 92 | 
 93 | ```
 94 | <type>(<scope>): <description>
 95 | 
 96 | [optional body]
 97 | 
 98 | [optional footer]
 99 | ```
100 | 
101 | ### Types
102 | 
103 | | Type       | Description                                        |
104 | | ---------- | -------------------------------------------------- |
105 | | `feat`     | New feature                                        |
106 | | `fix`      | Bug fix                                            |
107 | | `docs`     | Documentation only                                 |
108 | | `style`    | Code style (formatting, missing semi-colons, etc.) |
109 | | `refactor` | Code refactoring (no functional changes)           |
110 | | `perf`     | Performance improvement                            |
111 | | `test`     | Adding or updating tests                           |
112 | | `chore`    | Maintenance tasks                                  |
113 | 
114 | ### Examples
115 | 
116 | ```bash
117 | feat(layout): add support for grid layout detection
118 | fix(parser): handle empty node arrays correctly
119 | docs: update installation instructions
120 | refactor(core): simplify node processing logic
121 | ```
122 | 
123 | ## Pull Request Process
124 | 
125 | 1. Update documentation if needed
126 | 2. Ensure all tests pass: `pnpm test`
127 | 3. Ensure code quality checks pass: `pnpm lint && pnpm type-check`
128 | 4. Fill out the pull request template completely
129 | 5. Request review from maintainers
130 | 
131 | ### PR Title
132 | 
133 | Use the same format as commit messages:
134 | 
135 | ```
136 | feat(layout): add support for grid layout detection
137 | ```
138 | 
139 | ## Code Style
140 | 
141 | ### TypeScript
142 | 
143 | - Use TypeScript strict mode
144 | - Prefer `interface` over `type` for object shapes
145 | - Use explicit return types for public functions
146 | - Avoid `any` - use `unknown` if type is truly unknown
147 | 
148 | ### Formatting
149 | 
150 | - We use Prettier for code formatting
151 | - Run `pnpm format` before committing
152 | - EditorConfig is provided for consistent editor settings
153 | 
154 | ### File Organization
155 | 
156 | ```
157 | src/
158 | ├── algorithms/     # Detection algorithms (layout, icon)
159 | ├── core/           # Core parsing logic
160 | ├── services/       # External services (Figma API, cache)
161 | ├── types/          # TypeScript type definitions
162 | └── utils/          # Utility functions
163 | ```
164 | 
165 | ### Naming Conventions
166 | 
167 | | Type       | Convention                            | Example              |
168 | | ---------- | ------------------------------------- | -------------------- |
169 | | Files      | kebab-case                            | `layout-detector.ts` |
170 | | Classes    | PascalCase                            | `LayoutOptimizer`    |
171 | | Functions  | camelCase                             | `detectLayout()`     |
172 | | Constants  | UPPER_SNAKE_CASE                      | `MAX_ICON_SIZE`      |
173 | | Interfaces | PascalCase with `I` prefix (optional) | `SimplifiedNode`     |
174 | 
175 | ## Questions?
176 | 
177 | If you have questions, feel free to:
178 | 
179 | - Open an issue with the "question" label
180 | - Check existing issues and discussions
181 | 
182 | Thank you for contributing!
183 | 
```

--------------------------------------------------------------------------------
/src/prompts/index.ts:
--------------------------------------------------------------------------------

```typescript
1 | export {
2 |   DESIGN_TO_CODE_PROMPT,
3 |   COMPONENT_ANALYSIS_PROMPT,
4 |   STYLE_EXTRACTION_PROMPT,
5 | } from "./design-to-code.js";
6 | 
```

--------------------------------------------------------------------------------
/src/services/cache.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Cache Service
 3 |  *
 4 |  * Re-exports from the cache module for convenience.
 5 |  *
 6 |  * @module services/cache
 7 |  */
 8 | 
 9 | export {
10 |   CacheManager,
11 |   cacheManager,
12 |   type CacheConfig,
13 |   type CacheStatistics,
14 | } from "./cache/index.js";
15 | 
```

--------------------------------------------------------------------------------
/src/resources/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | export {
 2 |   getFileMetadata,
 3 |   getStyleTokens,
 4 |   getComponentList,
 5 |   getAssetList,
 6 |   createFileMetadataTemplate,
 7 |   createStylesTemplate,
 8 |   createComponentsTemplate,
 9 |   createAssetsTemplate,
10 |   FIGMA_MCP_HELP,
11 |   type FileMetadata,
12 |   type StyleTokens,
13 |   type ComponentSummary,
14 | } from "./figma-resources.js";
15 | 
```

--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { defineConfig } from "tsup";
 2 | 
 3 | const isDev = process.env.npm_lifecycle_event === "dev";
 4 | 
 5 | export default defineConfig({
 6 |   clean: true,
 7 |   entry: ["src/index.ts"],
 8 |   format: ["esm"],
 9 |   minify: !isDev,
10 |   target: "esnext",
11 |   outDir: "dist",
12 |   outExtension: ({ format }) => ({
13 |     js: ".js",
14 |   }),
15 |   onSuccess: isDev ? "node dist/index.js" : undefined,
16 | });
17 | 
```

--------------------------------------------------------------------------------
/src/algorithms/icon/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Icon Detection Algorithm Module
 3 |  *
 4 |  * Exports the icon detection algorithm for identifying and merging
 5 |  * fragmented icon layers in Figma designs.
 6 |  *
 7 |  * @module algorithms/icon
 8 |  */
 9 | 
10 | export {
11 |   detectIcon,
12 |   processNodeTree,
13 |   collectExportableIcons,
14 |   analyzeNodeTree,
15 |   DEFAULT_CONFIG,
16 |   type FigmaNode,
17 |   type IconDetectionResult,
18 |   type DetectionConfig,
19 | } from "./detector.js";
20 | 
```

--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { defineConfig } from "vitest/config";
 2 | import { resolve } from "path";
 3 | 
 4 | export default defineConfig({
 5 |   test: {
 6 |     globals: true,
 7 |     environment: "node",
 8 |     include: ["tests/**/*.test.ts"],
 9 |     coverage: {
10 |       provider: "v8",
11 |       reporter: ["text", "json", "html"],
12 |       include: ["src/**/*.ts"],
13 |       exclude: ["src/index.ts", "src/types/**"],
14 |     },
15 |     testTimeout: 10000,
16 |   },
17 |   resolve: {
18 |     alias: {
19 |       "~": resolve(__dirname, "./src"),
20 |     },
21 |   },
22 | });
23 | 
```

--------------------------------------------------------------------------------
/src/services/simplify-node-response.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Simplified Node Response Service
 3 |  *
 4 |  * This module re-exports the core parser functionality for backward compatibility.
 5 |  * The actual parsing logic has been moved to ~/core/parser.ts.
 6 |  *
 7 |  * @module services/simplify-node-response
 8 |  */
 9 | 
10 | // Re-export parser function
11 | export { parseFigmaResponse } from "~/core/parser.js";
12 | 
13 | // Re-export types for backward compatibility
14 | export type {
15 |   CSSStyle,
16 |   TextStyle,
17 |   SimplifiedDesign,
18 |   SimplifiedNode,
19 |   SimplifiedFill,
20 |   ExportInfo,
21 |   ImageResource,
22 |   FigmaNodeType,
23 | } from "~/types/index.js";
24 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | FROM node:lts-alpine
 3 | 
 4 | # Install pnpm globally
 5 | RUN npm install -g pnpm
 6 | 
 7 | # Set working directory
 8 | WORKDIR /app
 9 | 
10 | # Copy package files and install dependencies (cache layer)
11 | COPY package.json pnpm-lock.yaml ./
12 | RUN pnpm install
13 | 
14 | # Copy all source files
15 | COPY . .
16 | 
17 | # Build the project
18 | RUN pnpm run build
19 | 
20 | # Install this package globally so that the 'figma-mcp' command is available
21 | RUN npm install -g .
22 | 
23 | # Expose the port (default 3333)
24 | EXPOSE 3333
25 | 
26 | # Default command to run the MCP server
27 | CMD [ "figma-mcp", "--stdio" ]
28 | 
```

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

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "baseUrl": "./",
 4 |     "paths": {
 5 |       "~/*": ["./src/*"]
 6 |     },
 7 | 
 8 |     "target": "ES2020",
 9 |     "lib": ["ES2021", "DOM"],
10 |     "module": "NodeNext",
11 |     "moduleResolution": "NodeNext",
12 |     "resolveJsonModule": true,
13 |     "allowJs": true,
14 |     "checkJs": true,
15 | 
16 |     /* EMIT RULES */
17 |     "outDir": "./dist",
18 |     "declaration": true,
19 |     "declarationMap": true,
20 |     "sourceMap": true,
21 |     "removeComments": true,
22 | 
23 |     "strict": true,
24 |     "esModuleInterop": true,
25 |     "skipLibCheck": true,
26 |     "forceConsistentCasingInFileNames": true
27 |   },
28 |   "include": ["src/**/*", "tests/**/*"]
29 | }
30 | 
```

--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Figma Context MCP - Type Definitions
 3 |  *
 4 |  * Central export point for all type definitions.
 5 |  * Types are organized into separate modules by domain.
 6 |  *
 7 |  * @module types
 8 |  */
 9 | 
10 | // Figma API types
11 | export type {
12 |   FigmaNodeType,
13 |   ImageResource,
14 |   ExportFormat,
15 |   ExportInfo,
16 |   FigmaError,
17 |   RateLimitInfo,
18 |   FetchImageParams,
19 |   FetchImageFillParams,
20 | } from "./figma.js";
21 | 
22 | // Simplified output types
23 | export type {
24 |   CSSHexColor,
25 |   CSSRGBAColor,
26 |   CSSStyle,
27 |   TextStyle,
28 |   SimplifiedFill,
29 |   SimplifiedNode,
30 |   SimplifiedDesign,
31 |   IconDetectionResult,
32 |   IconDetectionConfig,
33 |   LayoutInfo,
34 | } from "./simplified.js";
35 | 
```

--------------------------------------------------------------------------------
/src/services/cache/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Cache Module
 3 |  *
 4 |  * Multi-layer caching system for Figma data.
 5 |  *
 6 |  * @module services/cache
 7 |  */
 8 | 
 9 | // Types
10 | export type {
11 |   CacheConfig,
12 |   MemoryCacheConfig,
13 |   DiskCacheConfig,
14 |   CacheEntryMeta,
15 |   NodeCacheEntry,
16 |   ImageCacheEntry,
17 |   MemoryCacheStats,
18 |   DiskCacheStats,
19 |   CacheStatistics,
20 | } from "./types.js";
21 | 
22 | export { DEFAULT_MEMORY_CONFIG, DEFAULT_DISK_CONFIG, DEFAULT_CACHE_CONFIG } from "./types.js";
23 | 
24 | // LRU Cache
25 | export { LRUCache, NodeLRUCache, type LRUCacheConfig, type CacheStats } from "./lru-cache.js";
26 | 
27 | // Disk Cache
28 | export { DiskCache } from "./disk-cache.js";
29 | 
30 | // Cache Manager
31 | export { CacheManager, cacheManager } from "./cache-manager.js";
32 | 
```

--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------

```javascript
 1 | export default {
 2 |   extends: ["@commitlint/config-conventional"],
 3 |   rules: {
 4 |     "type-enum": [
 5 |       2,
 6 |       "always",
 7 |       [
 8 |         "feat",     // New feature
 9 |         "fix",      // Bug fix
10 |         "docs",     // Documentation
11 |         "style",    // Code style (formatting, etc.)
12 |         "refactor", // Code refactoring
13 |         "perf",     // Performance improvement
14 |         "test",     // Tests
15 |         "build",    // Build system
16 |         "ci",       // CI configuration
17 |         "chore",    // Maintenance
18 |         "revert",   // Revert commit
19 |       ],
20 |     ],
21 |     "subject-case": [0], // Disable case checking for Chinese commits
22 |     "header-max-length": [2, "always", 100],
23 |   },
24 | };
25 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - figmaApiKey
10 |     properties:
11 |       figmaApiKey:
12 |         type: string
13 |         description: Your Figma API access token
14 |       port:
15 |         type: number
16 |         default: 3333
17 |         description: Port for the server to run on (default 3333)
18 |   commandFunction:
19 |     # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
20 |     |-
21 |     (config) => ({
22 |       command: 'figma-mcp',
23 |       args: [`--figma-api-key=${config.figmaApiKey}`, '--stdio', `--port=${config.port}`],
24 |       env: {}
25 |     })
26 |   exampleConfig:
27 |     figmaApiKey: dummy-figma-api-key
28 |     port: 3333
29 | 
```

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

```typescript
 1 | #!/usr/bin/env node
 2 | 
 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
 4 | import { FigmaMcpServer } from "./server.js";
 5 | import { getServerConfig } from "./config.js";
 6 | import { resolve } from "path";
 7 | import { config } from "dotenv";
 8 | 
 9 | // Load .env from the current working directory
10 | config({ path: resolve(process.cwd(), ".env") });
11 | 
12 | export async function startServer(): Promise<void> {
13 |   // Check if we're running in stdio mode (e.g., via CLI)
14 |   const isStdioMode = process.env.NODE_ENV === "cli" || process.argv.includes("--stdio");
15 | 
16 |   const config = getServerConfig(isStdioMode);
17 | 
18 |   const server = new FigmaMcpServer(config.figmaApiKey);
19 | 
20 |   if (isStdioMode) {
21 |     const transport = new StdioServerTransport();
22 |     await server.connect(transport);
23 |   } else {
24 |     console.log(`Initializing Figma MCP Server in HTTP mode on port ${config.port}...`);
25 |     await server.startHttpServer(config.port);
26 |   }
27 | }
28 | 
29 | startServer().catch((error) => {
30 |   console.error("Failed to start server:", error);
31 |   process.exit(1);
32 | });
33 | 
```

--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------

```markdown
 1 | ## Description
 2 | 
 3 | <!-- Describe your changes in detail -->
 4 | 
 5 | ## Related Issue
 6 | 
 7 | <!-- Link to the issue this PR addresses (if applicable) -->
 8 | <!-- Fixes #123 -->
 9 | 
10 | ## Type of Change
11 | 
12 | <!-- Mark the relevant option with an "x" -->
13 | 
14 | - [ ] Bug fix (non-breaking change that fixes an issue)
15 | - [ ] New feature (non-breaking change that adds functionality)
16 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
17 | - [ ] Documentation update
18 | - [ ] Refactoring (no functional changes)
19 | 
20 | ## Checklist
21 | 
22 | - [ ] My code follows the project's style guidelines
23 | - [ ] I have performed a self-review of my code
24 | - [ ] I have added/updated comments for hard-to-understand areas
25 | - [ ] I have updated the documentation (if applicable)
26 | - [ ] My changes generate no new warnings
27 | - [ ] I have run `pnpm lint` and `pnpm build` successfully
28 | - [ ] I have tested my changes locally
29 | 
30 | ## Screenshots (if applicable)
31 | 
32 | <!-- Add screenshots to help explain your changes -->
33 | 
34 | ## Additional Notes
35 | 
36 | <!-- Any additional information that reviewers should know -->
37 | 
```

--------------------------------------------------------------------------------
/src/core/style.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { type Node as FigmaDocumentNode } from "@figma/rest-api-spec";
 2 | import type { SimplifiedFill } from "~/types/index.js";
 3 | import { generateCSSShorthand } from "~/utils/css.js";
 4 | import { isVisible } from "~/utils/validation.js";
 5 | import { parsePaint } from "~/utils/color.js";
 6 | import { hasValue, isStrokeWeights } from "~/utils/validation.js";
 7 | export type SimplifiedStroke = {
 8 |   colors: SimplifiedFill[];
 9 |   strokeWeight?: string;
10 |   strokeDashes?: number[];
11 |   strokeWeights?: string;
12 | };
13 | export function buildSimplifiedStrokes(n: FigmaDocumentNode): SimplifiedStroke {
14 |   const strokes: SimplifiedStroke = { colors: [] };
15 |   if (hasValue("strokes", n) && Array.isArray(n.strokes) && n.strokes.length) {
16 |     strokes.colors = n.strokes.filter(isVisible).map(parsePaint);
17 |   }
18 | 
19 |   if (hasValue("strokeWeight", n) && typeof n.strokeWeight === "number" && n.strokeWeight > 0) {
20 |     strokes.strokeWeight = `${n.strokeWeight}px`;
21 |   }
22 | 
23 |   if (hasValue("strokeDashes", n) && Array.isArray(n.strokeDashes) && n.strokeDashes.length) {
24 |     strokes.strokeDashes = n.strokeDashes;
25 |   }
26 | 
27 |   if (hasValue("individualStrokeWeights", n, isStrokeWeights)) {
28 |     strokes.strokeWeight = generateCSSShorthand(n.individualStrokeWeights);
29 |   }
30 | 
31 |   return strokes;
32 | }
33 | 
```

--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------

```javascript
 1 | import eslint from "@eslint/js";
 2 | import tseslint from "typescript-eslint";
 3 | import prettierConfig from "eslint-config-prettier";
 4 | 
 5 | export default tseslint.config(
 6 |   eslint.configs.recommended,
 7 |   ...tseslint.configs.recommended,
 8 |   prettierConfig,
 9 |   {
10 |     ignores: ["dist/**", "node_modules/**", "*.config.js"],
11 |   },
12 |   {
13 |     languageOptions: {
14 |       ecmaVersion: 2022,
15 |       sourceType: "module",
16 |       parserOptions: {
17 |         project: "./tsconfig.json",
18 |       },
19 |     },
20 |     rules: {
21 |       // TypeScript specific
22 |       "@typescript-eslint/explicit-function-return-type": "off",
23 |       "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
24 |       "@typescript-eslint/no-explicit-any": "warn",
25 |       "@typescript-eslint/no-non-null-assertion": "warn",
26 |       "@typescript-eslint/consistent-type-imports": [
27 |         "error",
28 |         { prefer: "type-imports", fixStyle: "inline-type-imports" },
29 |       ],
30 | 
31 |       // General
32 |       "no-console": ["warn", { allow: ["warn", "error"] }],
33 |       "prefer-const": "error",
34 |       "no-var": "error",
35 |       eqeqeq: ["error", "always"],
36 |     },
37 |   },
38 |   // Test files - allow console.log and non-null assertions
39 |   {
40 |     files: ["tests/**/*.ts"],
41 |     rules: {
42 |       "no-console": "off",
43 |       "@typescript-eslint/no-non-null-assertion": "off",
44 |     },
45 |   },
46 | );
47 | 
```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: Feature Request
 2 | description: Suggest a new feature or improvement
 3 | labels: ["enhancement"]
 4 | 
 5 | body:
 6 |   - type: markdown
 7 |     attributes:
 8 |       value: |
 9 |         Thanks for suggesting a feature! Please fill out the form below.
10 | 
11 |   - type: textarea
12 |     id: problem
13 |     attributes:
14 |       label: Problem Statement
15 |       description: Is your feature request related to a problem? Please describe.
16 |       placeholder: "I'm always frustrated when..."
17 |     validations:
18 |       required: true
19 | 
20 |   - type: textarea
21 |     id: solution
22 |     attributes:
23 |       label: Proposed Solution
24 |       description: Describe the solution you'd like to see.
25 |       placeholder: "I would like to be able to..."
26 |     validations:
27 |       required: true
28 | 
29 |   - type: textarea
30 |     id: alternatives
31 |     attributes:
32 |       label: Alternatives Considered
33 |       description: Describe any alternative solutions or features you've considered.
34 |       placeholder: "I've also thought about..."
35 | 
36 |   - type: dropdown
37 |     id: priority
38 |     attributes:
39 |       label: Priority
40 |       description: How important is this feature to you?
41 |       options:
42 |         - Nice to have
43 |         - Important
44 |         - Critical
45 |     validations:
46 |       required: true
47 | 
48 |   - type: checkboxes
49 |     id: contribution
50 |     attributes:
51 |       label: Contribution
52 |       description: Are you willing to contribute to this feature?
53 |       options:
54 |         - label: I'm willing to submit a PR for this feature
55 | 
```

--------------------------------------------------------------------------------
/src/algorithms/layout/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Layout Detection Algorithm Module
 3 |  *
 4 |  * Exports spatial analysis utilities for detecting layout patterns
 5 |  * in Figma designs, including row/column grouping and containment analysis.
 6 |  *
 7 |  * @module algorithms/layout
 8 |  */
 9 | 
10 | // Spatial analysis utilities
11 | export {
12 |   RectUtils,
13 |   SpatialProjectionAnalyzer,
14 |   NodeRelationship,
15 |   type Rect,
16 |   type ProjectionLine,
17 | } from "./spatial.js";
18 | 
19 | // Layout optimizer
20 | export { LayoutOptimizer } from "./optimizer.js";
21 | 
22 | // Layout detection algorithm
23 | export {
24 |   // Types
25 |   type BoundingBox,
26 |   type ElementRect,
27 |   type LayoutGroup,
28 |   type LayoutAnalysisResult,
29 |   type LayoutNode,
30 |   type GridAnalysisResult,
31 |   type OverlapType,
32 |   type OverlapDetectionResult,
33 |   // Bounding box utilities
34 |   extractBoundingBox,
35 |   toElementRect,
36 |   calculateBounds,
37 |   // Overlap detection
38 |   isOverlappingY,
39 |   isOverlappingX,
40 |   isFullyOverlapping,
41 |   calculateIoU,
42 |   classifyOverlap,
43 |   detectOverlappingElements,
44 |   // Background element detection
45 |   detectBackgroundElement,
46 |   type BackgroundDetectionResult,
47 |   // Grouping
48 |   groupIntoRows,
49 |   groupIntoColumns,
50 |   findOverlappingElements,
51 |   // Gap analysis
52 |   calculateGaps,
53 |   analyzeGaps,
54 |   roundToCommonValue,
55 |   // Alignment
56 |   areValuesAligned,
57 |   analyzeAlignment,
58 |   toJustifyContent,
59 |   toAlignItems,
60 |   // Layout detection
61 |   detectLayoutDirection,
62 |   analyzeLayout,
63 |   buildLayoutTree,
64 |   generateLayoutReport,
65 |   // Grid detection
66 |   clusterValues,
67 |   detectGridLayout,
68 |   // Homogeneity analysis
69 |   analyzeHomogeneity,
70 |   filterHomogeneousForGrid,
71 |   type HomogeneityResult,
72 | } from "./detector.js";
73 | 
```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: CI
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [main, master]
 6 |   pull_request:
 7 |     branches: [main, master]
 8 | 
 9 | jobs:
10 |   build:
11 |     runs-on: ubuntu-latest
12 | 
13 |     strategy:
14 |       matrix:
15 |         node-version: [18.x, 20.x, 22.x]
16 | 
17 |     steps:
18 |       - name: Checkout repository
19 |         uses: actions/checkout@v4
20 | 
21 |       - name: Install pnpm
22 |         uses: pnpm/action-setup@v4
23 |         with:
24 |           version: 9
25 | 
26 |       - name: Setup Node.js ${{ matrix.node-version }}
27 |         uses: actions/setup-node@v4
28 |         with:
29 |           node-version: ${{ matrix.node-version }}
30 |           cache: "pnpm"
31 | 
32 |       - name: Install dependencies
33 |         run: pnpm install --frozen-lockfile
34 | 
35 |       - name: Type check
36 |         run: pnpm type-check
37 | 
38 |       - name: Lint
39 |         run: pnpm lint
40 | 
41 |       - name: Build
42 |         run: pnpm build
43 | 
44 |   release:
45 |     needs: build
46 |     runs-on: ubuntu-latest
47 |     if: github.event_name == 'push' && github.ref == 'refs/heads/main'
48 | 
49 |     steps:
50 |       - name: Checkout repository
51 |         uses: actions/checkout@v4
52 | 
53 |       - name: Install pnpm
54 |         uses: pnpm/action-setup@v4
55 |         with:
56 |           version: 9
57 | 
58 |       - name: Setup Node.js
59 |         uses: actions/setup-node@v4
60 |         with:
61 |           node-version: 20.x
62 |           cache: "pnpm"
63 |           registry-url: "https://registry.npmjs.org"
64 | 
65 |       - name: Install dependencies
66 |         run: pnpm install --frozen-lockfile
67 | 
68 |       - name: Build
69 |         run: pnpm build
70 | 
71 |       # Uncomment to enable auto-publish on main branch
72 |       # - name: Publish to npm
73 |       #   run: npm publish --access public
74 |       #   env:
75 |       #     NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
76 | 
```

--------------------------------------------------------------------------------
/tests/utils/preview.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Generate simplified output and open viewer for comparison
 3 |  *
 4 |  * Usage: pnpm preview
 5 |  */
 6 | 
 7 | import * as fs from "fs";
 8 | import * as path from "path";
 9 | import { fileURLToPath } from "url";
10 | import { exec } from "child_process";
11 | import { parseFigmaResponse } from "../../src/services/simplify-node-response.js";
12 | 
13 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
14 | const fixturesDir = path.join(__dirname, "../fixtures");
15 | 
16 | const originalPath = path.join(fixturesDir, "figma-data/real-node-data.json");
17 | const outputPath = path.join(__dirname, "simplified-with-css.json");
18 | const viewerPath = path.join(__dirname, "viewer.html");
19 | 
20 | async function main() {
21 |   console.log("📖 Reading original Figma data...");
22 |   const originalData = JSON.parse(fs.readFileSync(originalPath, "utf-8"));
23 | 
24 |   console.log("⚙️  Running simplification...");
25 |   const simplified = parseFigmaResponse(originalData);
26 | 
27 |   console.log("💾 Saving output...");
28 |   fs.writeFileSync(outputPath, JSON.stringify(simplified, null, 2));
29 | 
30 |   const originalSize = fs.statSync(originalPath).size;
31 |   const simplifiedSize = fs.statSync(outputPath).size;
32 |   const compressionRate = ((1 - simplifiedSize / originalSize) * 100).toFixed(1);
33 | 
34 |   console.log("");
35 |   console.log("📊 Results:");
36 |   console.log(`   Original:   ${(originalSize / 1024).toFixed(1)} KB`);
37 |   console.log(`   Simplified: ${(simplifiedSize / 1024).toFixed(1)} KB`);
38 |   console.log(`   Compression: ${compressionRate}%`);
39 |   console.log("");
40 | 
41 |   // Open viewer in browser
42 |   console.log("🌐 Opening viewer...");
43 |   const openCommand =
44 |     process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
45 | 
46 |   exec(`${openCommand} "${viewerPath}"`, (error) => {
47 |     if (error) {
48 |       console.log(`   Viewer path: file://${viewerPath}`);
49 |       console.log("   (Please open manually in browser)");
50 |     } else {
51 |       console.log("   Viewer opened in browser");
52 |     }
53 |   });
54 | }
55 | 
56 | main().catch(console.error);
57 | 
```

--------------------------------------------------------------------------------
/src/utils/file.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // ==================== Name Processing ====================
 2 | 
 3 | /**
 4 |  * Clean illegal characters from name (for file names)
 5 |  *
 6 |  * @param name Original name
 7 |  * @param separator Separator, defaults to '_'
 8 |  * @returns Sanitized name
 9 |  */
10 | export function sanitizeName(name: string, separator: string = "_"): string {
11 |   return name
12 |     .replace(/[/\\?%*:|"<>]/g, separator) // Replace illegal filesystem characters
13 |     .replace(/\s+/g, separator) // Replace whitespace characters
14 |     .replace(new RegExp(`${separator}+`, "g"), separator) // Merge consecutive separators
15 |     .toLowerCase();
16 | }
17 | 
18 | /**
19 |  * Clean name for ID generation (only keep alphanumeric and hyphens)
20 |  *
21 |  * @param name Original name
22 |  * @returns Sanitized name
23 |  */
24 | export function sanitizeNameForId(name: string): string {
25 |   return name
26 |     .replace(/\s+/g, "-")
27 |     .replace(/[^a-zA-Z0-9-]/g, "")
28 |     .toLowerCase();
29 | }
30 | 
31 | /**
32 |  * Generate file name based on node name
33 |  */
34 | export function generateFileName(name: string, format: string): string {
35 |   const sanitizedName = sanitizeName(name, "_");
36 |   const lowerFormat = format.toLowerCase();
37 | 
38 |   // If the name already includes the extension, keep the original name
39 |   if (sanitizedName.includes(`.${lowerFormat}`)) {
40 |     return sanitizedName;
41 |   }
42 | 
43 |   return `${sanitizedName}.${lowerFormat}`;
44 | }
45 | 
46 | // ==================== Export Format Detection ====================
47 | 
48 | /**
49 |  * Node interface for format detection
50 |  */
51 | export interface FormatDetectionNode {
52 |   type?: string;
53 |   exportSettings?: { format?: string[] };
54 |   cssStyles?: { backgroundImage?: string };
55 |   exportInfo?: { format?: string };
56 |   children?: FormatDetectionNode[];
57 | }
58 | 
59 | /**
60 |  * Choose appropriate export format based on node characteristics
61 |  *
62 |  * @param node Node object
63 |  * @param isSVGNode SVG detection function
64 |  * @returns Recommended export format
65 |  */
66 | export function suggestExportFormat(
67 |   node: FormatDetectionNode,
68 |   isSVGNode: (node: FormatDetectionNode) => boolean,
69 | ): "PNG" | "JPG" | "SVG" {
70 |   if (isSVGNode(node)) {
71 |     return "SVG";
72 |   }
73 |   return "PNG";
74 | }
75 | 
```

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

```typescript
  1 | /**
  2 |  * Figma API Type Definitions
  3 |  *
  4 |  * Types related to Figma API interactions, including node types,
  5 |  * export formats, and API response structures.
  6 |  *
  7 |  * @module types/figma
  8 |  */
  9 | 
 10 | // ==================== Node Types ====================
 11 | 
 12 | /** Figma node types */
 13 | export type FigmaNodeType =
 14 |   | "DOCUMENT"
 15 |   | "CANVAS"
 16 |   | "FRAME"
 17 |   | "GROUP"
 18 |   | "TEXT"
 19 |   | "VECTOR"
 20 |   | "RECTANGLE"
 21 |   | "ELLIPSE"
 22 |   | "LINE"
 23 |   | "POLYGON"
 24 |   | "STAR"
 25 |   | "BOOLEAN_OPERATION"
 26 |   | "REGULAR_POLYGON"
 27 |   | "INSTANCE"
 28 |   | "COMPONENT"
 29 |   | string;
 30 | 
 31 | /**
 32 |  * Image resource reference
 33 |  */
 34 | export interface ImageResource {
 35 |   /** Image reference ID for downloading */
 36 |   imageRef: string;
 37 | }
 38 | 
 39 | // ==================== Export Types ====================
 40 | 
 41 | /**
 42 |  * Export format options
 43 |  */
 44 | export type ExportFormat = "PNG" | "JPG" | "SVG";
 45 | 
 46 | /**
 47 |  * Export information for image nodes
 48 |  */
 49 | export interface ExportInfo {
 50 |   /** Export type (single image or image group) */
 51 |   type: "IMAGE" | "IMAGE_GROUP";
 52 |   /** Recommended export format */
 53 |   format: ExportFormat;
 54 |   /** Node ID for API calls (optional, defaults to node.id) */
 55 |   nodeId?: string;
 56 |   /** Suggested file name */
 57 |   fileName?: string;
 58 | }
 59 | 
 60 | // ==================== API Types ====================
 61 | 
 62 | /**
 63 |  * Figma API error
 64 |  */
 65 | export interface FigmaError {
 66 |   status: number;
 67 |   err: string;
 68 |   rateLimitInfo?: RateLimitInfo;
 69 | }
 70 | 
 71 | /**
 72 |  * Rate limit information from Figma API
 73 |  */
 74 | export interface RateLimitInfo {
 75 |   /** Remaining requests */
 76 |   remaining: number | null;
 77 |   /** Reset time in seconds */
 78 |   resetAfter: number | null;
 79 |   /** Retry wait time in seconds */
 80 |   retryAfter: number | null;
 81 | }
 82 | 
 83 | /**
 84 |  * Image download parameters
 85 |  */
 86 | export interface FetchImageParams {
 87 |   /** Figma node ID */
 88 |   nodeId: string;
 89 |   /** Local file name */
 90 |   fileName: string;
 91 |   /** File format */
 92 |   fileType: "png" | "svg";
 93 | }
 94 | 
 95 | /**
 96 |  * Image fill download parameters
 97 |  */
 98 | export interface FetchImageFillParams {
 99 |   /** Node ID */
100 |   nodeId: string;
101 |   /** Local file name */
102 |   fileName: string;
103 |   /** Image reference ID */
104 |   imageRef: string;
105 | }
106 | 
```

--------------------------------------------------------------------------------
/src/core/effects.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import {
 2 |   type DropShadowEffect,
 3 |   type InnerShadowEffect,
 4 |   type BlurEffect,
 5 |   type Node as FigmaDocumentNode,
 6 | } from "@figma/rest-api-spec";
 7 | import { formatRGBAColor } from "~/utils/color.js";
 8 | import { hasValue } from "~/utils/validation.js";
 9 | 
10 | export type SimplifiedEffects = {
11 |   boxShadow?: string;
12 |   filter?: string;
13 |   backdropFilter?: string;
14 | };
15 | 
16 | export function buildSimplifiedEffects(n: FigmaDocumentNode): SimplifiedEffects {
17 |   if (!hasValue("effects", n)) return {};
18 |   const effects = n.effects.filter((e) => e.visible);
19 | 
20 |   // Handle drop and inner shadows (both go into CSS box-shadow)
21 |   const dropShadows = effects
22 |     .filter((e): e is DropShadowEffect => e.type === "DROP_SHADOW")
23 |     .map(simplifyDropShadow);
24 | 
25 |   const innerShadows = effects
26 |     .filter((e): e is InnerShadowEffect => e.type === "INNER_SHADOW")
27 |     .map(simplifyInnerShadow);
28 | 
29 |   const boxShadow = [...dropShadows, ...innerShadows].join(", ");
30 | 
31 |   // Handle blur effects - separate by CSS property
32 |   // Layer blurs use the CSS 'filter' property
33 |   const filterBlurValues = effects
34 |     .filter((e): e is BlurEffect => e.type === "LAYER_BLUR")
35 |     .map(simplifyBlur)
36 |     .join(" ");
37 | 
38 |   // Background blurs use the CSS 'backdrop-filter' property
39 |   const backdropFilterValues = effects
40 |     .filter((e): e is BlurEffect => e.type === "BACKGROUND_BLUR")
41 |     .map(simplifyBlur)
42 |     .join(" ");
43 | 
44 |   const result: SimplifiedEffects = {};
45 |   if (boxShadow) result.boxShadow = boxShadow;
46 |   if (filterBlurValues) result.filter = filterBlurValues;
47 |   if (backdropFilterValues) result.backdropFilter = backdropFilterValues;
48 | 
49 |   return result;
50 | }
51 | 
52 | function simplifyDropShadow(effect: DropShadowEffect) {
53 |   return `${effect.offset.x}px ${effect.offset.y}px ${effect.radius}px ${effect.spread ?? 0}px ${formatRGBAColor(effect.color)}`;
54 | }
55 | 
56 | function simplifyInnerShadow(effect: InnerShadowEffect) {
57 |   return `inset ${effect.offset.x}px ${effect.offset.y}px ${effect.radius}px ${effect.spread ?? 0}px ${formatRGBAColor(effect.color)}`;
58 | }
59 | 
60 | function simplifyBlur(effect: BlurEffect) {
61 |   return `blur(${effect.radius}px)`;
62 | }
63 | 
```

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

```typescript
 1 | import { config } from "dotenv";
 2 | import yargs from "yargs";
 3 | import { hideBin } from "yargs/helpers";
 4 | 
 5 | // Load environment variables from .env file
 6 | config();
 7 | 
 8 | interface ServerConfig {
 9 |   figmaApiKey: string;
10 |   port: number;
11 |   configSources: {
12 |     figmaApiKey: "cli" | "env";
13 |     port: "cli" | "env" | "default";
14 |   };
15 | }
16 | 
17 | function maskApiKey(key: string): string {
18 |   if (key.length <= 4) return "****";
19 |   return `****${key.slice(-4)}`;
20 | }
21 | 
22 | interface CliArgs {
23 |   "figma-api-key"?: string;
24 |   port?: number;
25 | }
26 | 
27 | export function getServerConfig(isStdioMode: boolean): ServerConfig {
28 |   // Parse command line arguments
29 |   const argv = yargs(hideBin(process.argv))
30 |     .options({
31 |       "figma-api-key": {
32 |         type: "string",
33 |         description: "Figma API key",
34 |       },
35 |       port: {
36 |         type: "number",
37 |         description: "Port to run the server on",
38 |       },
39 |     })
40 |     .help()
41 |     .version("0.1.12")
42 |     .parseSync() as CliArgs;
43 | 
44 |   const config: ServerConfig = {
45 |     figmaApiKey: "",
46 |     port: 3333,
47 |     configSources: {
48 |       figmaApiKey: "env",
49 |       port: "default",
50 |     },
51 |   };
52 | 
53 |   // Handle FIGMA_API_KEY
54 |   if (argv["figma-api-key"]) {
55 |     config.figmaApiKey = argv["figma-api-key"];
56 |     config.configSources.figmaApiKey = "cli";
57 |   } else if (process.env.FIGMA_API_KEY) {
58 |     config.figmaApiKey = process.env.FIGMA_API_KEY;
59 |     config.configSources.figmaApiKey = "env";
60 |   }
61 | 
62 |   // Handle PORT
63 |   if (argv.port) {
64 |     config.port = argv.port;
65 |     config.configSources.port = "cli";
66 |   } else if (process.env.PORT) {
67 |     config.port = parseInt(process.env.PORT, 10);
68 |     config.configSources.port = "env";
69 |   }
70 | 
71 |   // Validate configuration
72 |   if (!config.figmaApiKey) {
73 |     console.error("FIGMA_API_KEY is required (via CLI argument --figma-api-key or .env file)");
74 |     process.exit(1);
75 |   }
76 | 
77 |   // Log configuration sources
78 |   if (!isStdioMode) {
79 |     console.log("\nConfiguration:");
80 |     console.log(
81 |       `- FIGMA_API_KEY: ${maskApiKey(config.figmaApiKey)} (source: ${config.configSources.figmaApiKey})`,
82 |     );
83 |     console.log(`- PORT: ${config.port} (source: ${config.configSources.port})`);
84 |     console.log(); // Empty line for better readability
85 |   }
86 | 
87 |   return config;
88 | }
89 | 
```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------

```yaml
  1 | name: Bug Report
  2 | description: Report a bug or unexpected behavior
  3 | labels: ["bug", "triage"]
  4 | 
  5 | body:
  6 |   - type: markdown
  7 |     attributes:
  8 |       value: |
  9 |         Thanks for taking the time to report a bug! Please fill out the form below.
 10 | 
 11 |   - type: textarea
 12 |     id: description
 13 |     attributes:
 14 |       label: Bug Description
 15 |       description: A clear and concise description of what the bug is.
 16 |       placeholder: Describe the bug...
 17 |     validations:
 18 |       required: true
 19 | 
 20 |   - type: textarea
 21 |     id: reproduction
 22 |     attributes:
 23 |       label: Steps to Reproduce
 24 |       description: Steps to reproduce the behavior.
 25 |       placeholder: |
 26 |         1. Configure MCP server with...
 27 |         2. Open Figma file...
 28 |         3. Run command...
 29 |         4. See error...
 30 |     validations:
 31 |       required: true
 32 | 
 33 |   - type: textarea
 34 |     id: expected
 35 |     attributes:
 36 |       label: Expected Behavior
 37 |       description: What did you expect to happen?
 38 |       placeholder: Describe what you expected...
 39 |     validations:
 40 |       required: true
 41 | 
 42 |   - type: textarea
 43 |     id: actual
 44 |     attributes:
 45 |       label: Actual Behavior
 46 |       description: What actually happened?
 47 |       placeholder: Describe what actually happened...
 48 |     validations:
 49 |       required: true
 50 | 
 51 |   - type: input
 52 |     id: version
 53 |     attributes:
 54 |       label: Package Version
 55 |       description: What version of figma-mcp-server are you using?
 56 |       placeholder: "1.0.0"
 57 |     validations:
 58 |       required: true
 59 | 
 60 |   - type: dropdown
 61 |     id: client
 62 |     attributes:
 63 |       label: AI Client
 64 |       description: Which AI client are you using?
 65 |       options:
 66 |         - Cursor
 67 |         - Windsurf
 68 |         - Cline
 69 |         - Claude Desktop
 70 |         - Other
 71 |     validations:
 72 |       required: true
 73 | 
 74 |   - type: input
 75 |     id: node-version
 76 |     attributes:
 77 |       label: Node.js Version
 78 |       description: What version of Node.js are you using? (run `node -v`)
 79 |       placeholder: "v20.0.0"
 80 |     validations:
 81 |       required: true
 82 | 
 83 |   - type: dropdown
 84 |     id: os
 85 |     attributes:
 86 |       label: Operating System
 87 |       options:
 88 |         - macOS
 89 |         - Windows
 90 |         - Linux
 91 |     validations:
 92 |       required: true
 93 | 
 94 |   - type: textarea
 95 |     id: logs
 96 |     attributes:
 97 |       label: Error Logs
 98 |       description: If applicable, paste any error logs here.
 99 |       render: shell
100 | 
101 |   - type: textarea
102 |     id: additional
103 |     attributes:
104 |       label: Additional Context
105 |       description: Any other context about the problem.
106 | 
```

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

```markdown
 1 | # Changelog
 2 | 
 3 | All notable changes to this project will be documented in this file.
 4 | 
 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 7 | 
 8 | ## [1.1.0] - 2025-01-06
 9 | 
10 | ### Added
11 | 
12 | - **Grid Layout Detection** - Automatically detect and convert grid-like arrangements to CSS Grid
13 | - **Background Element Merging** - Smart merge of background layers with padding inference
14 | - **Multi-layer Cache System** - LRU memory cache (L1) + disk cache (L2) for 24h data persistence
15 | - **MCP Resources** - Lightweight resource endpoints (`figma://file`, `/styles`, `/components`, `/assets`) for token-efficient metadata browsing
16 | - **MCP Prompts** - Professional `design_to_code` prompt for guided AI code generation workflow
17 | - **Comprehensive Test Suite** - 272 tests covering layout optimization, icon detection, parser, and resources (Vitest)
18 | 
19 | ### Changed
20 | 
21 | - **Improved Flexbox Detection** - Enhanced stack detection with better gap/padding inference
22 | - **Icon Detection Optimization** - Single-pass tree traversal for better performance
23 | - **Modular Architecture** - Reorganized codebase (`transformers` → `core/algorithms`) for better maintainability
24 | - **Bilingual Documentation** - Complete English and Chinese docs for all algorithms and architecture
25 | 
26 | ### Fixed
27 | 
28 | - **Gradient Alpha Channel** - Preserve alpha values in gradient color stops
29 | - **Non-grid Element Positioning** - Correct position handling for elements outside grid containers
30 | - **Security Dependencies** - Updated dependencies to resolve vulnerabilities
31 | 
32 | ## [1.0.1] - 2024-12-05
33 | 
34 | ### Added
35 | 
36 | - Smart layout detection algorithm (Flexbox inference from absolute positioning)
37 | - Icon layer merge algorithm (reduces fragmented exports by 96%)
38 | - CSS generation with optimized output
39 | - HTML preview generation from Figma JSON
40 | - Comprehensive documentation for algorithms
41 | 
42 | ### Changed
43 | 
44 | - Optimized data structures for AI consumption
45 | - Reduced output size by ~87% through intelligent simplification
46 | - Improved node processing with better type handling
47 | 
48 | ### Fixed
49 | 
50 | - Round all px values to integers
51 | - Proper handling of gradient and image fills
52 | - Border style extraction improvements
53 | 
54 | ## [1.0.0] - 2024-12-01
55 | 
56 | ### Added
57 | 
58 | - Initial release
59 | - MCP server implementation for Figma integration
60 | - `get_figma_data` tool for fetching design data
61 | - `download_figma_images` tool for image export
62 | - Support for stdio and HTTP/SSE modes
63 | - Basic CSS style generation
64 | - Figma API integration with caching
65 | 
66 | [1.1.0]: https://github.com/1yhy/Figma-Context-MCP/compare/v1.0.1...v1.1.0
67 | [1.0.1]: https://github.com/1yhy/Figma-Context-MCP/compare/v1.0.0...v1.0.1
68 | [1.0.0]: https://github.com/1yhy/Figma-Context-MCP/releases/tag/v1.0.0
69 | 
```

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

```json
 1 | {
 2 |   "name": "@yhy2001/figma-mcp-server",
 3 |   "version": "1.1.0",
 4 |   "description": "MCP server for Figma design integration with AI coding tools",
 5 |   "type": "module",
 6 |   "main": "dist/index.js",
 7 |   "bin": {
 8 |     "figma-mcp": "dist/index.js"
 9 |   },
10 |   "files": [
11 |     "dist",
12 |     "README.md",
13 |     "README.zh-CN.md"
14 |   ],
15 |   "scripts": {
16 |     "dev": "cross-env NODE_ENV=development tsup --watch",
17 |     "build": "tsup",
18 |     "start": "node dist/index.js",
19 |     "start:cli": "cross-env NODE_ENV=cli node dist/index.js",
20 |     "start:http": "node dist/index.js",
21 |     "dev:cli": "cross-env NODE_ENV=development tsup --watch -- --stdio",
22 |     "type-check": "tsc --noEmit",
23 |     "lint": "eslint src/",
24 |     "lint:fix": "eslint src/ --fix",
25 |     "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
26 |     "format:check": "prettier --check \"src/**/*.ts\"",
27 |     "test": "vitest run",
28 |     "test:watch": "vitest",
29 |     "test:coverage": "vitest run --coverage",
30 |     "test:ui": "vitest --ui",
31 |     "test:unit": "vitest run tests/unit",
32 |     "test:integration": "vitest run tests/integration",
33 |     "preview": "tsx tests/utils/preview.ts",
34 |     "inspect": "pnpx @modelcontextprotocol/inspector",
35 |     "mcp-test": "pnpm start -- --stdio",
36 |     "prepublishOnly": "pnpm type-check && pnpm lint && pnpm test && pnpm build",
37 |     "pub:release": "pnpm build && npm publish --access public",
38 |     "publish:local": "pnpm build && npm pack",
39 |     "prepare": "husky || true"
40 |   },
41 |   "engines": {
42 |     "node": ">=18.0.0"
43 |   },
44 |   "repository": {
45 |     "type": "git",
46 |     "url": "git+https://github.com/1yhy/Figma-Context-MCP.git"
47 |   },
48 |   "homepage": "https://github.com/1yhy/Figma-Context-MCP#readme",
49 |   "bugs": {
50 |     "url": "https://github.com/1yhy/Figma-Context-MCP/issues"
51 |   },
52 |   "keywords": [
53 |     "figma",
54 |     "mcp",
55 |     "model-context-protocol",
56 |     "typescript",
57 |     "ai",
58 |     "design",
59 |     "cursor",
60 |     "windsurf",
61 |     "cline",
62 |     "design-to-code"
63 |   ],
64 |   "author": "1yhy",
65 |   "license": "MIT",
66 |   "dependencies": {
67 |     "@modelcontextprotocol/sdk": "^1.24.3",
68 |     "cross-env": "^7.0.3",
69 |     "dotenv": "^16.4.7",
70 |     "express": "^4.21.2",
71 |     "remeda": "^2.20.1",
72 |     "yargs": "^17.7.2",
73 |     "zod": "^4.1.13"
74 |   },
75 |   "devDependencies": {
76 |     "@commitlint/cli": "^20.2.0",
77 |     "@commitlint/config-conventional": "^19.0.0",
78 |     "@eslint/js": "^9.39.1",
79 |     "@figma/rest-api-spec": "^0.24.0",
80 |     "@types/express": "^5.0.0",
81 |     "@types/node": "^24.10.1",
82 |     "@types/yargs": "^17.0.33",
83 |     "@typescript-eslint/eslint-plugin": "^8.48.1",
84 |     "@typescript-eslint/parser": "^8.48.1",
85 |     "@vitest/coverage-v8": "^4.0.15",
86 |     "eslint": "^9.39.1",
87 |     "eslint-config-prettier": "^10.0.1",
88 |     "husky": "^9.0.0",
89 |     "lint-staged": "^15.0.0",
90 |     "prettier": "^3.5.0",
91 |     "tsup": "^8.5.1",
92 |     "tsx": "^4.21.0",
93 |     "typescript": "^5.7.3",
94 |     "typescript-eslint": "^8.48.1",
95 |     "vitest": "^4.0.15"
96 |   }
97 | }
98 | 
```

--------------------------------------------------------------------------------
/src/utils/color.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Color Conversion Utilities
  3 |  *
  4 |  * Functions for converting between Figma color formats and CSS color values.
  5 |  *
  6 |  * @module utils/color
  7 |  */
  8 | 
  9 | import type { Paint, RGBA } from "@figma/rest-api-spec";
 10 | import type { CSSHexColor, CSSRGBAColor, SimplifiedFill } from "~/types/index.js";
 11 | 
 12 | // ==================== Type Definitions ====================
 13 | 
 14 | export interface ColorValue {
 15 |   hex: CSSHexColor;
 16 |   opacity: number;
 17 | }
 18 | 
 19 | // ==================== Color Conversion ====================
 20 | 
 21 | /**
 22 |  * Convert hex color and opacity to rgba format
 23 |  */
 24 | export function hexToRgba(hex: string, opacity: number = 1): string {
 25 |   hex = hex.replace("#", "");
 26 | 
 27 |   if (hex.length === 3) {
 28 |     hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
 29 |   }
 30 | 
 31 |   const r = parseInt(hex.substring(0, 2), 16);
 32 |   const g = parseInt(hex.substring(2, 4), 16);
 33 |   const b = parseInt(hex.substring(4, 6), 16);
 34 |   const validOpacity = Math.min(Math.max(opacity, 0), 1);
 35 | 
 36 |   return `rgba(${r}, ${g}, ${b}, ${validOpacity})`;
 37 | }
 38 | 
 39 | /**
 40 |  * Convert Figma RGBA color to { hex, opacity }
 41 |  */
 42 | export function convertColor(color: RGBA, opacity = 1): ColorValue {
 43 |   const r = Math.round(color.r * 255);
 44 |   const g = Math.round(color.g * 255);
 45 |   const b = Math.round(color.b * 255);
 46 |   const a = Math.round(opacity * color.a * 100) / 100;
 47 | 
 48 |   const hex = ("#" +
 49 |     ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()) as CSSHexColor;
 50 | 
 51 |   return { hex, opacity: a };
 52 | }
 53 | 
 54 | /**
 55 |  * Convert Figma RGBA to CSS rgba() format
 56 |  */
 57 | export function formatRGBAColor(color: RGBA, opacity = 1): CSSRGBAColor {
 58 |   const r = Math.round(color.r * 255);
 59 |   const g = Math.round(color.g * 255);
 60 |   const b = Math.round(color.b * 255);
 61 |   const a = Math.round(opacity * color.a * 100) / 100;
 62 | 
 63 |   return `rgba(${r}, ${g}, ${b}, ${a})`;
 64 | }
 65 | 
 66 | // ==================== Fill Parsing ====================
 67 | 
 68 | /**
 69 |  * Convert Figma Paint to simplified fill format
 70 |  */
 71 | export function parsePaint(raw: Paint): SimplifiedFill {
 72 |   if (raw.type === "IMAGE") {
 73 |     const imagePaint = raw as { type: "IMAGE"; imageRef?: string; scaleMode?: string };
 74 |     return {
 75 |       type: "IMAGE",
 76 |       imageRef: imagePaint.imageRef,
 77 |       scaleMode: imagePaint.scaleMode,
 78 |     };
 79 |   }
 80 | 
 81 |   if (raw.type === "SOLID") {
 82 |     const { hex, opacity } = convertColor(raw.color!, raw.opacity);
 83 |     if (opacity === 1) {
 84 |       return hex;
 85 |     }
 86 |     return formatRGBAColor(raw.color!, opacity);
 87 |   }
 88 | 
 89 |   if (
 90 |     raw.type === "GRADIENT_LINEAR" ||
 91 |     raw.type === "GRADIENT_RADIAL" ||
 92 |     raw.type === "GRADIENT_ANGULAR" ||
 93 |     raw.type === "GRADIENT_DIAMOND"
 94 |   ) {
 95 |     const gradientPaint = raw as {
 96 |       type: typeof raw.type;
 97 |       gradientHandlePositions?: Array<{ x: number; y: number }>;
 98 |       gradientStops?: Array<{ position: number; color: RGBA }>;
 99 |     };
100 |     return {
101 |       type: raw.type,
102 |       gradientHandlePositions: gradientPaint.gradientHandlePositions,
103 |       gradientStops: gradientPaint.gradientStops?.map(({ position, color }) => ({
104 |         position,
105 |         color: convertColor(color).hex,
106 |       })),
107 |     };
108 |   }
109 | 
110 |   throw new Error(`Unknown paint type: ${raw.type}`);
111 | }
112 | 
```

--------------------------------------------------------------------------------
/src/services/cache/types.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Cache System Type Definitions
  3 |  *
  4 |  * @module services/cache/types
  5 |  */
  6 | 
  7 | // ==================== Configuration ====================
  8 | 
  9 | /**
 10 |  * Memory cache configuration
 11 |  */
 12 | export interface MemoryCacheConfig {
 13 |   /** Maximum number of node cache items (default: 100) */
 14 |   maxNodeItems: number;
 15 |   /** Maximum number of image cache items (default: 50) */
 16 |   maxImageItems: number;
 17 |   /** Node cache TTL in milliseconds (default: 5 minutes) */
 18 |   nodeTTL: number;
 19 |   /** Image cache TTL in milliseconds (default: 10 minutes) */
 20 |   imageTTL: number;
 21 | }
 22 | 
 23 | /**
 24 |  * Disk cache configuration
 25 |  */
 26 | export interface DiskCacheConfig {
 27 |   /** Cache directory path */
 28 |   cacheDir: string;
 29 |   /** Maximum disk cache size in bytes (default: 500MB) */
 30 |   maxSize: number;
 31 |   /** Cache TTL in milliseconds (default: 24 hours) */
 32 |   ttl: number;
 33 | }
 34 | 
 35 | /**
 36 |  * Complete cache configuration
 37 |  */
 38 | export interface CacheConfig {
 39 |   /** Whether caching is enabled */
 40 |   enabled: boolean;
 41 |   /** Memory cache configuration */
 42 |   memory: MemoryCacheConfig;
 43 |   /** Disk cache configuration */
 44 |   disk: DiskCacheConfig;
 45 | }
 46 | 
 47 | // ==================== Cache Entries ====================
 48 | 
 49 | /**
 50 |  * Cache entry metadata
 51 |  */
 52 | export interface CacheEntryMeta {
 53 |   /** Cache key */
 54 |   key: string;
 55 |   /** Creation timestamp */
 56 |   createdAt: number;
 57 |   /** Expiration timestamp */
 58 |   expiresAt: number;
 59 |   /** Figma file key */
 60 |   fileKey: string;
 61 |   /** Figma node ID (optional) */
 62 |   nodeId?: string;
 63 |   /** Figma file version (lastModified) */
 64 |   version?: string;
 65 |   /** Query depth */
 66 |   depth?: number;
 67 |   /** Data size in bytes */
 68 |   size?: number;
 69 | }
 70 | 
 71 | /**
 72 |  * Node cache entry
 73 |  */
 74 | export interface NodeCacheEntry {
 75 |   /** Cached data */
 76 |   data: unknown;
 77 |   /** Figma file key */
 78 |   fileKey: string;
 79 |   /** Figma node ID */
 80 |   nodeId?: string;
 81 |   /** Figma file version */
 82 |   version?: string;
 83 |   /** Query depth */
 84 |   depth?: number;
 85 | }
 86 | 
 87 | /**
 88 |  * Image cache entry
 89 |  */
 90 | export interface ImageCacheEntry {
 91 |   /** Local file path */
 92 |   path: string;
 93 |   /** Figma file key */
 94 |   fileKey: string;
 95 |   /** Figma node ID */
 96 |   nodeId: string;
 97 |   /** Image format */
 98 |   format: string;
 99 |   /** File size in bytes */
100 |   size?: number;
101 | }
102 | 
103 | // ==================== Statistics ====================
104 | 
105 | /**
106 |  * Memory cache statistics
107 |  */
108 | export interface MemoryCacheStats {
109 |   /** Cache hits */
110 |   hits: number;
111 |   /** Cache misses */
112 |   misses: number;
113 |   /** Current item count */
114 |   size: number;
115 |   /** Maximum item count */
116 |   maxSize: number;
117 |   /** Hit rate (0-1) */
118 |   hitRate: number;
119 |   /** Eviction count */
120 |   evictions: number;
121 | }
122 | 
123 | /**
124 |  * Disk cache statistics
125 |  */
126 | export interface DiskCacheStats {
127 |   /** Cache hits */
128 |   hits: number;
129 |   /** Cache misses */
130 |   misses: number;
131 |   /** Total size in bytes */
132 |   totalSize: number;
133 |   /** Maximum size in bytes */
134 |   maxSize: number;
135 |   /** Node data file count */
136 |   nodeFileCount: number;
137 |   /** Image file count */
138 |   imageFileCount: number;
139 | }
140 | 
141 | /**
142 |  * Combined cache statistics
143 |  */
144 | export interface CacheStatistics {
145 |   /** Whether cache is enabled */
146 |   enabled: boolean;
147 |   /** Memory cache stats */
148 |   memory: MemoryCacheStats;
149 |   /** Disk cache stats */
150 |   disk: DiskCacheStats;
151 | }
152 | 
153 | // ==================== Default Configurations ====================
154 | 
155 | /**
156 |  * Default memory cache configuration
157 |  */
158 | export const DEFAULT_MEMORY_CONFIG: MemoryCacheConfig = {
159 |   maxNodeItems: 100,
160 |   maxImageItems: 50,
161 |   nodeTTL: 5 * 60 * 1000, // 5 minutes
162 |   imageTTL: 10 * 60 * 1000, // 10 minutes
163 | };
164 | 
165 | /**
166 |  * Default disk cache configuration
167 |  */
168 | export const DEFAULT_DISK_CONFIG: Partial<DiskCacheConfig> = {
169 |   maxSize: 500 * 1024 * 1024, // 500MB
170 |   ttl: 24 * 60 * 60 * 1000, // 24 hours
171 | };
172 | 
173 | /**
174 |  * Default complete cache configuration
175 |  */
176 | export const DEFAULT_CACHE_CONFIG: Omit<CacheConfig, "disk"> & { disk: Partial<DiskCacheConfig> } =
177 |   {
178 |     enabled: true,
179 |     memory: DEFAULT_MEMORY_CONFIG,
180 |     disk: DEFAULT_DISK_CONFIG,
181 |   };
182 | 
```

--------------------------------------------------------------------------------
/scripts/optimize-figma-json.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env npx tsx
  2 | /**
  3 |  * Figma JSON Optimizer Script
  4 |  *
  5 |  * Usage:
  6 |  *   npx tsx scripts/optimize-figma-json.ts <input-file> [output-file]
  7 |  *
  8 |  * Examples:
  9 |  *   npx tsx scripts/optimize-figma-json.ts tests/fixtures/figma-data/node-108-517.json
 10 |  *   npx tsx scripts/optimize-figma-json.ts input.json output-optimized.json
 11 |  *
 12 |  * If output-file is not specified:
 13 |  *   - For files in figma-data/, outputs to expected/<name>-optimized.json
 14 |  *   - Otherwise, outputs to <input-name>-optimized.json in the same directory
 15 |  */
 16 | 
 17 | import fs from "fs";
 18 | import path from "path";
 19 | import { parseFigmaResponse } from "../src/core/parser.js";
 20 | 
 21 | // Parse arguments
 22 | const args = process.argv.slice(2);
 23 | 
 24 | if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
 25 |   console.log(`
 26 | Figma JSON Optimizer
 27 | 
 28 | Usage:
 29 |   npx tsx scripts/optimize-figma-json.ts <input-file> [output-file]
 30 | 
 31 | Examples:
 32 |   npx tsx scripts/optimize-figma-json.ts tests/fixtures/figma-data/node-108-517.json
 33 |   npx tsx scripts/optimize-figma-json.ts input.json output-optimized.json
 34 | 
 35 | Options:
 36 |   --help, -h     Show this help message
 37 |   --stats        Show detailed statistics only (no file output)
 38 |   --quiet, -q    Minimal output
 39 | `);
 40 |   process.exit(0);
 41 | }
 42 | 
 43 | const inputFile = args[0];
 44 | const showStatsOnly = args.includes("--stats");
 45 | const quiet = args.includes("--quiet") || args.includes("-q");
 46 | 
 47 | // Determine output file
 48 | let outputFile = args.find((arg) => !arg.startsWith("-") && arg !== inputFile);
 49 | 
 50 | if (!outputFile && !showStatsOnly) {
 51 |   const inputDir = path.dirname(inputFile);
 52 |   const inputName = path.basename(inputFile, ".json");
 53 | 
 54 |   // If input is in figma-data, output to expected
 55 |   if (inputDir.includes("figma-data")) {
 56 |     const expectedDir = inputDir.replace("figma-data", "expected");
 57 |     outputFile = path.join(expectedDir, `${inputName}-optimized.json`);
 58 |   } else {
 59 |     outputFile = path.join(inputDir, `${inputName}-optimized.json`);
 60 |   }
 61 | }
 62 | 
 63 | // Check input file exists
 64 | if (!fs.existsSync(inputFile)) {
 65 |   console.error(`Error: Input file not found: ${inputFile}`);
 66 |   process.exit(1);
 67 | }
 68 | 
 69 | // Read and optimize
 70 | if (!quiet) {
 71 |   console.log(`\nOptimizing: ${inputFile}`);
 72 | }
 73 | 
 74 | const startTime = Date.now();
 75 | const rawData = JSON.parse(fs.readFileSync(inputFile, "utf8"));
 76 | const optimized = parseFigmaResponse(rawData);
 77 | const elapsed = Date.now() - startTime;
 78 | 
 79 | // Calculate statistics
 80 | const json = JSON.stringify(optimized, null, 2);
 81 | const originalSize = fs.statSync(inputFile).size;
 82 | const optimizedSize = Buffer.byteLength(json);
 83 | const compression = ((1 - optimizedSize / originalSize) * 100).toFixed(1);
 84 | 
 85 | const absoluteCount = (json.match(/"position":\s*"absolute"/g) || []).length;
 86 | const gridCount = (json.match(/"display":\s*"grid"/g) || []).length;
 87 | const flexCount = (json.match(/"display":\s*"flex"/g) || []).length;
 88 | const flexRowCount = (json.match(/"flexDirection":\s*"row"/g) || []).length;
 89 | const flexColumnCount = (json.match(/"flexDirection":\s*"column"/g) || []).length;
 90 | 
 91 | // Get root node info
 92 | let rootInfo = "";
 93 | if (optimized.nodes && optimized.nodes.length > 0) {
 94 |   const root = optimized.nodes[0];
 95 |   rootInfo = `${root.name} (${root.cssStyles?.width} × ${root.cssStyles?.height})`;
 96 | }
 97 | 
 98 | // Output results
 99 | if (!quiet) {
100 |   console.log(`\n${"─".repeat(50)}`);
101 |   console.log(`Root: ${rootInfo}`);
102 |   console.log(`${"─".repeat(50)}`);
103 |   console.log(`\nLayout Statistics:`);
104 |   console.log(`  position:absolute  ${absoluteCount}`);
105 |   console.log(`  display:grid       ${gridCount}`);
106 |   console.log(`  display:flex       ${flexCount} (row: ${flexRowCount}, column: ${flexColumnCount})`);
107 |   console.log(`\nCompression:`);
108 |   console.log(`  Original:  ${(originalSize / 1024).toFixed(1)} KB`);
109 |   console.log(`  Optimized: ${(optimizedSize / 1024).toFixed(1)} KB`);
110 |   console.log(`  Reduced:   ${compression}%`);
111 |   console.log(`\nTime: ${elapsed}ms`);
112 | }
113 | 
114 | // Save output
115 | if (!showStatsOnly && outputFile) {
116 |   // Ensure output directory exists
117 |   const outputDir = path.dirname(outputFile);
118 |   if (!fs.existsSync(outputDir)) {
119 |     fs.mkdirSync(outputDir, { recursive: true });
120 |   }
121 | 
122 |   fs.writeFileSync(outputFile, json);
123 | 
124 |   if (!quiet) {
125 |     console.log(`\nSaved: ${outputFile}`);
126 |   } else {
127 |     console.log(outputFile);
128 |   }
129 | }
130 | 
131 | if (!quiet) {
132 |   console.log("");
133 | }
134 | 
```

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

```typescript
  1 | /**
  2 |  * Validation Utilities
  3 |  *
  4 |  * Type guards, validators, and visibility checks for Figma nodes.
  5 |  *
  6 |  * @module utils/validation
  7 |  */
  8 | 
  9 | import type {
 10 |   Rectangle,
 11 |   HasLayoutTrait,
 12 |   StrokeWeights,
 13 |   HasFramePropertiesTrait,
 14 | } from "@figma/rest-api-spec";
 15 | import { isTruthy } from "remeda";
 16 | import type { CSSHexColor, CSSRGBAColor } from "~/types/index.js";
 17 | 
 18 | export { isTruthy };
 19 | 
 20 | // ==================== Visibility Types ====================
 21 | 
 22 | /** Properties for visibility checking */
 23 | export interface VisibilityProperties {
 24 |   visible?: boolean;
 25 |   opacity?: number;
 26 |   absoluteBoundingBox?: { x: number; y: number; width: number; height: number };
 27 |   absoluteRenderBounds?: { x: number; y: number; width: number; height: number } | null;
 28 | }
 29 | 
 30 | /** Properties for parent container clipping check */
 31 | export interface ParentClipProperties {
 32 |   clipsContent?: boolean;
 33 |   absoluteBoundingBox?: { x: number; y: number; width: number; height: number };
 34 | }
 35 | 
 36 | // ==================== Visibility Checks ====================
 37 | 
 38 | /**
 39 |  * Check if an element is visible
 40 |  */
 41 | export function isVisible(element: VisibilityProperties): boolean {
 42 |   if (element.visible === false) {
 43 |     return false;
 44 |   }
 45 | 
 46 |   if (element.opacity === 0) {
 47 |     return false;
 48 |   }
 49 | 
 50 |   if (element.absoluteRenderBounds === null) {
 51 |     return false;
 52 |   }
 53 | 
 54 |   return true;
 55 | }
 56 | 
 57 | /**
 58 |  * Check if an element is visible within its parent container (considering clipping)
 59 |  */
 60 | export function isVisibleInParent(
 61 |   element: VisibilityProperties,
 62 |   parent: ParentClipProperties,
 63 | ): boolean {
 64 |   if (!isVisible(element)) {
 65 |     return false;
 66 |   }
 67 | 
 68 |   if (
 69 |     parent &&
 70 |     parent.clipsContent === true &&
 71 |     element.absoluteBoundingBox &&
 72 |     parent.absoluteBoundingBox
 73 |   ) {
 74 |     const elementBox = element.absoluteBoundingBox;
 75 |     const parentBox = parent.absoluteBoundingBox;
 76 | 
 77 |     const outsideParent =
 78 |       elementBox.x >= parentBox.x + parentBox.width ||
 79 |       elementBox.x + elementBox.width <= parentBox.x ||
 80 |       elementBox.y >= parentBox.y + parentBox.height ||
 81 |       elementBox.y + elementBox.height <= parentBox.y;
 82 | 
 83 |     if (outsideParent) {
 84 |       return false;
 85 |     }
 86 |   }
 87 | 
 88 |   return true;
 89 | }
 90 | 
 91 | // ==================== Object Processing ====================
 92 | 
 93 | /**
 94 |  * Remove empty arrays and empty objects from an object
 95 |  */
 96 | export function removeEmptyKeys<T>(input: T): T {
 97 |   if (typeof input !== "object" || input === null) {
 98 |     return input;
 99 |   }
100 | 
101 |   if (Array.isArray(input)) {
102 |     return input.map((item) => removeEmptyKeys(item)) as T;
103 |   }
104 | 
105 |   const result = {} as T;
106 |   for (const key in input) {
107 |     if (Object.prototype.hasOwnProperty.call(input, key)) {
108 |       const value = input[key];
109 |       const cleanedValue = removeEmptyKeys(value);
110 | 
111 |       if (
112 |         cleanedValue !== undefined &&
113 |         !(Array.isArray(cleanedValue) && cleanedValue.length === 0) &&
114 |         !(
115 |           typeof cleanedValue === "object" &&
116 |           cleanedValue !== null &&
117 |           Object.keys(cleanedValue).length === 0
118 |         )
119 |       ) {
120 |         result[key] = cleanedValue;
121 |       }
122 |     }
123 |   }
124 | 
125 |   return result;
126 | }
127 | 
128 | // ==================== Type Guards ====================
129 | 
130 | export function hasValue<K extends PropertyKey, T>(
131 |   key: K,
132 |   obj: unknown,
133 |   typeGuard?: (val: unknown) => val is T,
134 | ): obj is Record<K, T> {
135 |   const isObject = typeof obj === "object" && obj !== null;
136 |   if (!isObject || !(key in obj)) return false;
137 |   const val = (obj as Record<K, unknown>)[key];
138 |   return typeGuard ? typeGuard(val) : val !== undefined;
139 | }
140 | 
141 | export function isFrame(val: unknown): val is HasFramePropertiesTrait {
142 |   return (
143 |     typeof val === "object" &&
144 |     !!val &&
145 |     "clipsContent" in val &&
146 |     typeof val.clipsContent === "boolean"
147 |   );
148 | }
149 | 
150 | export function isLayout(val: unknown): val is HasLayoutTrait {
151 |   return (
152 |     typeof val === "object" &&
153 |     !!val &&
154 |     "absoluteBoundingBox" in val &&
155 |     typeof val.absoluteBoundingBox === "object" &&
156 |     !!val.absoluteBoundingBox &&
157 |     "x" in val.absoluteBoundingBox &&
158 |     "y" in val.absoluteBoundingBox &&
159 |     "width" in val.absoluteBoundingBox &&
160 |     "height" in val.absoluteBoundingBox
161 |   );
162 | }
163 | 
164 | export function isStrokeWeights(val: unknown): val is StrokeWeights {
165 |   return (
166 |     typeof val === "object" &&
167 |     val !== null &&
168 |     "top" in val &&
169 |     "right" in val &&
170 |     "bottom" in val &&
171 |     "left" in val
172 |   );
173 | }
174 | 
175 | export function isRectangle<T, K extends string>(
176 |   key: K,
177 |   obj: T,
178 | ): obj is T & { [P in K]: Rectangle } {
179 |   const recordObj = obj as Record<K, unknown>;
180 |   return (
181 |     typeof obj === "object" &&
182 |     !!obj &&
183 |     key in recordObj &&
184 |     typeof recordObj[key] === "object" &&
185 |     !!recordObj[key] &&
186 |     "x" in recordObj[key] &&
187 |     "y" in recordObj[key] &&
188 |     "width" in recordObj[key] &&
189 |     "height" in recordObj[key]
190 |   );
191 | }
192 | 
193 | export function isRectangleCornerRadii(val: unknown): val is number[] {
194 |   return Array.isArray(val) && val.length === 4 && val.every((v) => typeof v === "number");
195 | }
196 | 
197 | export function isCSSColorValue(val: unknown): val is CSSRGBAColor | CSSHexColor {
198 |   return typeof val === "string" && (val.startsWith("#") || val.startsWith("rgba"));
199 | }
200 | 
```

--------------------------------------------------------------------------------
/src/types/simplified.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Simplified Output Type Definitions
  3 |  *
  4 |  * Types for the MCP simplified output format, including CSS styles,
  5 |  * simplified node structures, and algorithm configurations.
  6 |  *
  7 |  * @module types/simplified
  8 |  */
  9 | 
 10 | import type { ExportInfo } from "./figma.js";
 11 | 
 12 | // ==================== CSS Types ====================
 13 | 
 14 | /** CSS hex color format */
 15 | export type CSSHexColor = `#${string}`;
 16 | 
 17 | /** CSS rgba color format */
 18 | export type CSSRGBAColor = `rgba(${number}, ${number}, ${number}, ${number})`;
 19 | 
 20 | /**
 21 |  * CSS style object containing all supported CSS properties
 22 |  */
 23 | export type CSSStyle = {
 24 |   // Text styles
 25 |   fontFamily?: string;
 26 |   fontSize?: string;
 27 |   fontWeight?: string | number;
 28 |   textAlign?: string;
 29 |   verticalAlign?: string;
 30 |   lineHeight?: string;
 31 | 
 32 |   // Colors and backgrounds
 33 |   color?: string;
 34 |   backgroundColor?: string;
 35 |   background?: string;
 36 |   backgroundImage?: string;
 37 | 
 38 |   // Layout
 39 |   width?: string;
 40 |   height?: string;
 41 |   margin?: string;
 42 |   padding?: string;
 43 |   position?: string;
 44 |   top?: string;
 45 |   right?: string;
 46 |   bottom?: string;
 47 |   left?: string;
 48 |   display?: string;
 49 |   flexDirection?: string;
 50 |   justifyContent?: string;
 51 |   alignItems?: string;
 52 |   gap?: string;
 53 | 
 54 |   // Grid layout
 55 |   gridTemplateColumns?: string;
 56 |   gridTemplateRows?: string;
 57 |   rowGap?: string;
 58 |   columnGap?: string;
 59 |   justifyItems?: string;
 60 |   gridColumn?: string;
 61 |   gridRow?: string;
 62 | 
 63 |   // Flex item
 64 |   flexGrow?: string | number;
 65 |   flexShrink?: string | number;
 66 |   flexBasis?: string;
 67 |   flex?: string;
 68 |   alignSelf?: string;
 69 |   order?: string | number;
 70 | 
 71 |   // Borders and radius
 72 |   border?: string;
 73 |   borderRadius?: string;
 74 |   borderWidth?: string;
 75 |   borderStyle?: string;
 76 |   borderColor?: string;
 77 |   borderImage?: string;
 78 |   borderImageSlice?: string;
 79 | 
 80 |   // Effects
 81 |   boxShadow?: string;
 82 |   filter?: string;
 83 |   backdropFilter?: string;
 84 |   opacity?: string;
 85 | 
 86 |   // Webkit specific
 87 |   webkitBackgroundClip?: string;
 88 |   webkitTextFillColor?: string;
 89 |   backgroundClip?: string;
 90 | 
 91 |   // Allow additional properties
 92 |   [key: string]: string | number | undefined;
 93 | };
 94 | 
 95 | /**
 96 |  * Text style properties (legacy, for backward compatibility)
 97 |  */
 98 | export type TextStyle = Partial<{
 99 |   fontFamily: string;
100 |   fontWeight: number;
101 |   fontSize: number;
102 |   textAlignHorizontal: string;
103 |   textAlignVertical: string;
104 |   lineHeightPx: number;
105 | }>;
106 | 
107 | // ==================== Simplified Node Types ====================
108 | 
109 | /**
110 |  * Fill type for simplified nodes
111 |  */
112 | export type SimplifiedFill =
113 |   | CSSHexColor
114 |   | CSSRGBAColor
115 |   | SimplifiedSolidFill
116 |   | SimplifiedGradientFill
117 |   | SimplifiedImageFill;
118 | 
119 | /** Solid fill with explicit type */
120 | export interface SimplifiedSolidFill {
121 |   type: "SOLID";
122 |   color: string;
123 |   opacity?: number;
124 | }
125 | 
126 | /** Gradient fill */
127 | export interface SimplifiedGradientFill {
128 |   type: "GRADIENT_LINEAR" | "GRADIENT_RADIAL" | "GRADIENT_ANGULAR" | "GRADIENT_DIAMOND";
129 |   gradientHandlePositions?: Array<{ x: number; y: number }>;
130 |   gradientStops?: Array<{
131 |     position: number;
132 |     color: string;
133 |   }>;
134 | }
135 | 
136 | /** Image fill */
137 | export interface SimplifiedImageFill {
138 |   type: "IMAGE";
139 |   imageRef?: string;
140 |   scaleMode?: string;
141 | }
142 | 
143 | /**
144 |  * Simplified node structure
145 |  * This is the main output type for the MCP response
146 |  */
147 | export interface SimplifiedNode {
148 |   /** Node ID */
149 |   id: string;
150 |   /** Node name */
151 |   name: string;
152 |   /** Node type (FRAME, TEXT, VECTOR, etc.) */
153 |   type: string;
154 |   /** Text content (for TEXT nodes) */
155 |   text?: string;
156 |   /** Legacy text style (for backward compatibility) */
157 |   style?: TextStyle;
158 |   /** CSS styles */
159 |   cssStyles?: CSSStyle;
160 |   /** Fill information */
161 |   fills?: SimplifiedFill[];
162 |   /** Export information (for image nodes) */
163 |   exportInfo?: ExportInfo;
164 |   /** Child nodes */
165 |   children?: SimplifiedNode[];
166 |   /** Internal: absolute X coordinate */
167 |   _absoluteX?: number;
168 |   /** Internal: absolute Y coordinate */
169 |   _absoluteY?: number;
170 | }
171 | 
172 | /**
173 |  * Simplified design output
174 |  * Top-level structure returned by the MCP
175 |  */
176 | export interface SimplifiedDesign {
177 |   /** Design file name */
178 |   name: string;
179 |   /** Last modified timestamp */
180 |   lastModified: string;
181 |   /** File version */
182 |   version?: string;
183 |   /** Thumbnail URL */
184 |   thumbnailUrl: string;
185 |   /** Root nodes */
186 |   nodes: SimplifiedNode[];
187 | }
188 | 
189 | // ==================== Algorithm Types ====================
190 | 
191 | /**
192 |  * Icon detection result
193 |  */
194 | export interface IconDetectionResult {
195 |   nodeId: string;
196 |   nodeName: string;
197 |   shouldMerge: boolean;
198 |   exportFormat: "SVG" | "PNG";
199 |   reason: string;
200 |   size?: { width: number; height: number };
201 |   childCount?: number;
202 | }
203 | 
204 | /**
205 |  * Icon detection configuration
206 |  */
207 | export interface IconDetectionConfig {
208 |   /** Maximum icon size in pixels */
209 |   maxIconSize: number;
210 |   /** Minimum icon size in pixels */
211 |   minIconSize: number;
212 |   /** Minimum ratio of mergeable types */
213 |   mergeableRatio: number;
214 |   /** Maximum nesting depth */
215 |   maxDepth: number;
216 |   /** Maximum child count */
217 |   maxChildren: number;
218 |   /** Maximum size to respect exportSettings */
219 |   respectExportSettingsMaxSize: number;
220 | }
221 | 
222 | /**
223 |  * Layout detection result
224 |  */
225 | export interface LayoutInfo {
226 |   type: "flex" | "absolute" | "grid";
227 |   direction?: "row" | "column";
228 |   gap?: number;
229 |   justifyContent?: string;
230 |   alignItems?: string;
231 |   /** Grid-specific: row gap */
232 |   rowGap?: number;
233 |   /** Grid-specific: column gap */
234 |   columnGap?: number;
235 |   /** Grid-specific: template columns (e.g., "100px 200px 100px") */
236 |   gridTemplateColumns?: string;
237 |   /** Grid-specific: template rows (e.g., "auto auto") */
238 |   gridTemplateRows?: string;
239 |   /** Grid confidence score (0-1) */
240 |   confidence?: number;
241 | }
242 | 
```

--------------------------------------------------------------------------------
/src/utils/css.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * CSS Utilities
  3 |  *
  4 |  * CSS output optimization and generation utilities.
  5 |  * Used to reduce output size and improve readability.
  6 |  *
  7 |  * @module utils/css
  8 |  */
  9 | 
 10 | // ==================== CSS Shorthand Generation ====================
 11 | 
 12 | /**
 13 |  * Generate CSS shorthand properties (such as padding, margin, border-radius)
 14 |  *
 15 |  * @example
 16 |  * generateCSSShorthand({ top: 10, right: 10, bottom: 10, left: 10 }) // "10px"
 17 |  * generateCSSShorthand({ top: 10, right: 20, bottom: 10, left: 20 }) // "10px 20px"
 18 |  */
 19 | export function generateCSSShorthand(
 20 |   values: {
 21 |     top: number;
 22 |     right: number;
 23 |     bottom: number;
 24 |     left: number;
 25 |   },
 26 |   options: {
 27 |     ignoreZero?: boolean;
 28 |     suffix?: string;
 29 |   } = {},
 30 | ): string | undefined {
 31 |   const { ignoreZero = true, suffix = "px" } = options;
 32 |   const { top, right, bottom, left } = values;
 33 | 
 34 |   if (ignoreZero && top === 0 && right === 0 && bottom === 0 && left === 0) {
 35 |     return undefined;
 36 |   }
 37 | 
 38 |   if (top === right && right === bottom && bottom === left) {
 39 |     return `${top}${suffix}`;
 40 |   }
 41 | 
 42 |   if (right === left) {
 43 |     if (top === bottom) {
 44 |       return `${top}${suffix} ${right}${suffix}`;
 45 |     }
 46 |     return `${top}${suffix} ${right}${suffix} ${bottom}${suffix}`;
 47 |   }
 48 | 
 49 |   return `${top}${suffix} ${right}${suffix} ${bottom}${suffix} ${left}${suffix}`;
 50 | }
 51 | 
 52 | // ==================== Numeric Precision Optimization ====================
 53 | 
 54 | /**
 55 |  * Round a number to specified precision
 56 |  * @param value Original number
 57 |  * @param precision Number of decimal places, default 0 (integer)
 58 |  */
 59 | export function roundValue(value: number, precision: number = 0): number {
 60 |   if (precision === 0) {
 61 |     return Math.round(value);
 62 |   }
 63 |   const multiplier = Math.pow(10, precision);
 64 |   return Math.round(value * multiplier) / multiplier;
 65 | }
 66 | 
 67 | /**
 68 |  * Format px value, rounded to integer
 69 |  * @param value Pixel value
 70 |  */
 71 | export function formatPxValue(value: number): string {
 72 |   return `${Math.round(value)}px`;
 73 | }
 74 | 
 75 | /**
 76 |  * Format numeric value, used for gap and other properties, rounded to integer
 77 |  * @param value Numeric value
 78 |  */
 79 | export function formatNumericValue(value: number): string {
 80 |   return `${Math.round(value)}px`;
 81 | }
 82 | 
 83 | // ==================== Browser Defaults ====================
 84 | 
 85 | /**
 86 |  * Browser/Tailwind default values
 87 |  * These values can be omitted from output
 88 |  */
 89 | export const BROWSER_DEFAULTS: Record<string, string | number | undefined> = {
 90 |   // Text defaults
 91 |   textAlign: "left",
 92 |   verticalAlign: "top",
 93 |   fontWeight: 400,
 94 | 
 95 |   // Flex defaults
 96 |   flexDirection: "row",
 97 |   justifyContent: "flex-start",
 98 |   alignItems: "stretch",
 99 | 
100 |   // Position defaults (if all elements are absolute, can be omitted)
101 |   // position: 'static',  // Not omitting for now, as we explicitly use absolute
102 | 
103 |   // Other
104 |   opacity: "1",
105 |   borderStyle: "none",
106 | };
107 | 
108 | /**
109 |  * Check if a value is the default value
110 |  */
111 | export function isDefaultValue(key: string, value: string | number | undefined): boolean {
112 |   if (value === undefined) return true;
113 | 
114 |   const defaultValue = BROWSER_DEFAULTS[key];
115 |   if (defaultValue === undefined) return false;
116 | 
117 |   // Handle number and string comparison
118 |   if (typeof defaultValue === "number" && typeof value === "number") {
119 |     return defaultValue === value;
120 |   }
121 | 
122 |   return String(defaultValue) === String(value);
123 | }
124 | 
125 | /**
126 |  * Omit default style values
127 |  * @param styles CSS style object
128 |  * @returns Optimized style object
129 |  */
130 | export function omitDefaultStyles<T extends Record<string, unknown>>(styles: T): Partial<T> {
131 |   const result: Partial<T> = {};
132 | 
133 |   for (const [key, value] of Object.entries(styles)) {
134 |     // Skip undefined
135 |     if (value === undefined) continue;
136 | 
137 |     // Skip default values
138 |     if (isDefaultValue(key, value as string | number)) continue;
139 | 
140 |     // Keep non-default values
141 |     (result as Record<string, unknown>)[key] = value;
142 |   }
143 | 
144 |   return result;
145 | }
146 | 
147 | // ==================== Gap Analysis ====================
148 | 
149 | /**
150 |  * Analyze gap consistency
151 |  * @param gaps Array of gaps
152 |  * @param tolerancePercent Tolerance percentage, default 20%
153 |  */
154 | export function analyzeGapConsistency(
155 |   gaps: number[],
156 |   tolerancePercent: number = 20,
157 | ): {
158 |   isConsistent: boolean;
159 |   averageGap: number;
160 |   roundedGap: number;
161 |   variance: number;
162 | } {
163 |   if (gaps.length === 0) {
164 |     return { isConsistent: true, averageGap: 0, roundedGap: 0, variance: 0 };
165 |   }
166 | 
167 |   if (gaps.length === 1) {
168 |     const rounded = roundValue(gaps[0]);
169 |     return { isConsistent: true, averageGap: gaps[0], roundedGap: rounded, variance: 0 };
170 |   }
171 | 
172 |   // Calculate average
173 |   const avg = gaps.reduce((a, b) => a + b, 0) / gaps.length;
174 | 
175 |   // Calculate variance
176 |   const variance = gaps.reduce((sum, gap) => sum + Math.pow(gap - avg, 2), 0) / gaps.length;
177 |   const stdDev = Math.sqrt(variance);
178 | 
179 |   // Determine consistency: standard deviation less than specified percentage of average
180 |   const tolerance = avg * (tolerancePercent / 100);
181 |   const isConsistent = stdDev <= tolerance;
182 | 
183 |   // Round to integer
184 |   const roundedGap = roundValue(avg);
185 | 
186 |   return { isConsistent, averageGap: avg, roundedGap, variance };
187 | }
188 | 
189 | /**
190 |  * Round gap to common values
191 |  * Common values: 0, 2, 4, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 64
192 |  */
193 | export function roundToCommonGap(gap: number): number {
194 |   const COMMON_GAPS = [0, 2, 4, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96, 128];
195 | 
196 |   // Find closest common value
197 |   let closest = COMMON_GAPS[0];
198 |   let minDiff = Math.abs(gap - closest);
199 | 
200 |   for (const commonGap of COMMON_GAPS) {
201 |     const diff = Math.abs(gap - commonGap);
202 |     if (diff < minDiff) {
203 |       minDiff = diff;
204 |       closest = commonGap;
205 |     }
206 |   }
207 | 
208 |   // If difference is too large (over 4px), use rounded value
209 |   if (minDiff > 4) {
210 |     return roundValue(gap);
211 |   }
212 | 
213 |   return closest;
214 | }
215 | 
216 | // ==================== Export Info Optimization ====================
217 | 
218 | /**
219 |  * Optimize exportInfo, omit nodeId if it's the same as node id
220 |  */
221 | export function optimizeExportInfo(
222 |   nodeId: string,
223 |   exportInfo: { type: string; format: string; nodeId?: string; fileName?: string },
224 | ): { type: string; format: string; nodeId?: string; fileName?: string } {
225 |   const result = { ...exportInfo };
226 | 
227 |   // If nodeId is the same as node id, omit it
228 |   if (result.nodeId === nodeId) {
229 |     delete result.nodeId;
230 |   }
231 | 
232 |   return result;
233 | }
234 | 
```

--------------------------------------------------------------------------------
/src/services/cache/lru-cache.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * LRU (Least Recently Used) Memory Cache
  3 |  *
  4 |  * A generic in-memory cache with LRU eviction policy.
  5 |  * Used as a fast cache layer before disk cache.
  6 |  *
  7 |  * Features:
  8 |  * - O(1) get/set operations
  9 |  * - Automatic eviction of least recently used items
 10 |  * - Optional TTL (time-to-live) per item
 11 |  * - Size limiting by item count
 12 |  * - Statistics tracking
 13 |  *
 14 |  * @module services/cache/lru-cache
 15 |  */
 16 | 
 17 | import type { NodeCacheEntry } from "./types.js";
 18 | 
 19 | export interface LRUCacheConfig {
 20 |   /** Maximum number of items in cache */
 21 |   maxSize: number;
 22 |   /** Default TTL in milliseconds (0 = no expiration) */
 23 |   defaultTTL: number;
 24 | }
 25 | 
 26 | interface CacheEntry<T> {
 27 |   value: T;
 28 |   createdAt: number;
 29 |   expiresAt: number | null;
 30 |   size?: number;
 31 | }
 32 | 
 33 | export interface CacheStats {
 34 |   hits: number;
 35 |   misses: number;
 36 |   evictions: number;
 37 |   size: number;
 38 |   maxSize: number;
 39 | }
 40 | 
 41 | const DEFAULT_CONFIG: LRUCacheConfig = {
 42 |   maxSize: 100,
 43 |   defaultTTL: 0, // No expiration by default
 44 | };
 45 | 
 46 | /**
 47 |  * Generic LRU Cache implementation using Map
 48 |  * Map maintains insertion order, making it ideal for LRU
 49 |  */
 50 | export class LRUCache<T> {
 51 |   private cache: Map<string, CacheEntry<T>>;
 52 |   private config: LRUCacheConfig;
 53 |   private stats: CacheStats;
 54 | 
 55 |   constructor(config: Partial<LRUCacheConfig> = {}) {
 56 |     this.config = { ...DEFAULT_CONFIG, ...config };
 57 |     this.cache = new Map();
 58 |     this.stats = {
 59 |       hits: 0,
 60 |       misses: 0,
 61 |       evictions: 0,
 62 |       size: 0,
 63 |       maxSize: this.config.maxSize,
 64 |     };
 65 |   }
 66 | 
 67 |   /**
 68 |    * Get an item from cache
 69 |    * Returns null if not found or expired
 70 |    */
 71 |   get(key: string): T | null {
 72 |     const entry = this.cache.get(key);
 73 | 
 74 |     if (!entry) {
 75 |       this.stats.misses++;
 76 |       return null;
 77 |     }
 78 | 
 79 |     // Check expiration
 80 |     if (entry.expiresAt && Date.now() > entry.expiresAt) {
 81 |       this.delete(key);
 82 |       this.stats.misses++;
 83 |       return null;
 84 |     }
 85 | 
 86 |     // Move to end (most recently used)
 87 |     this.cache.delete(key);
 88 |     this.cache.set(key, entry);
 89 | 
 90 |     this.stats.hits++;
 91 |     return entry.value;
 92 |   }
 93 | 
 94 |   /**
 95 |    * Set an item in cache
 96 |    * @param key Cache key
 97 |    * @param value Value to cache
 98 |    * @param ttl Optional TTL in milliseconds (overrides default)
 99 |    */
100 |   set(key: string, value: T, ttl?: number): void {
101 |     // If key exists, delete it first (to update position)
102 |     if (this.cache.has(key)) {
103 |       this.cache.delete(key);
104 |     } else {
105 |       // Evict if at capacity
106 |       while (this.cache.size >= this.config.maxSize) {
107 |         this.evictOldest();
108 |       }
109 |     }
110 | 
111 |     const effectiveTTL = ttl ?? this.config.defaultTTL;
112 |     const entry: CacheEntry<T> = {
113 |       value,
114 |       createdAt: Date.now(),
115 |       expiresAt: effectiveTTL > 0 ? Date.now() + effectiveTTL : null,
116 |     };
117 | 
118 |     this.cache.set(key, entry);
119 |     this.stats.size = this.cache.size;
120 |   }
121 | 
122 |   /**
123 |    * Check if key exists (without updating access time)
124 |    */
125 |   has(key: string): boolean {
126 |     const entry = this.cache.get(key);
127 |     if (!entry) return false;
128 | 
129 |     // Check expiration
130 |     if (entry.expiresAt && Date.now() > entry.expiresAt) {
131 |       this.delete(key);
132 |       return false;
133 |     }
134 | 
135 |     return true;
136 |   }
137 | 
138 |   /**
139 |    * Delete an item from cache
140 |    */
141 |   delete(key: string): boolean {
142 |     const existed = this.cache.delete(key);
143 |     if (existed) {
144 |       this.stats.size = this.cache.size;
145 |     }
146 |     return existed;
147 |   }
148 | 
149 |   /**
150 |    * Clear all items from cache
151 |    */
152 |   clear(): void {
153 |     this.cache.clear();
154 |     this.stats.size = 0;
155 |   }
156 | 
157 |   /**
158 |    * Get all keys in cache (most recent last)
159 |    */
160 |   keys(): string[] {
161 |     return Array.from(this.cache.keys());
162 |   }
163 | 
164 |   /**
165 |    * Get cache size
166 |    */
167 |   get size(): number {
168 |     return this.cache.size;
169 |   }
170 | 
171 |   /**
172 |    * Get cache statistics
173 |    */
174 |   getStats(): CacheStats {
175 |     return { ...this.stats };
176 |   }
177 | 
178 |   /**
179 |    * Reset statistics
180 |    */
181 |   resetStats(): void {
182 |     this.stats.hits = 0;
183 |     this.stats.misses = 0;
184 |     this.stats.evictions = 0;
185 |   }
186 | 
187 |   /**
188 |    * Get hit rate (0-1)
189 |    */
190 |   getHitRate(): number {
191 |     const total = this.stats.hits + this.stats.misses;
192 |     return total === 0 ? 0 : this.stats.hits / total;
193 |   }
194 | 
195 |   /**
196 |    * Clean expired entries
197 |    */
198 |   cleanExpired(): number {
199 |     const now = Date.now();
200 |     let cleaned = 0;
201 | 
202 |     for (const [key, entry] of this.cache.entries()) {
203 |       if (entry.expiresAt && now > entry.expiresAt) {
204 |         this.cache.delete(key);
205 |         cleaned++;
206 |       }
207 |     }
208 | 
209 |     this.stats.size = this.cache.size;
210 |     return cleaned;
211 |   }
212 | 
213 |   /**
214 |    * Evict the oldest (least recently used) item
215 |    */
216 |   private evictOldest(): void {
217 |     const oldestKey = this.cache.keys().next().value;
218 |     if (oldestKey !== undefined) {
219 |       this.cache.delete(oldestKey);
220 |       this.stats.evictions++;
221 |       this.stats.size = this.cache.size;
222 |     }
223 |   }
224 | 
225 |   /**
226 |    * Peek at an item without updating access time
227 |    */
228 |   peek(key: string): T | null {
229 |     const entry = this.cache.get(key);
230 |     if (!entry) return null;
231 | 
232 |     if (entry.expiresAt && Date.now() > entry.expiresAt) {
233 |       return null;
234 |     }
235 | 
236 |     return entry.value;
237 |   }
238 | 
239 |   /**
240 |    * Update config (applies to new entries only)
241 |    */
242 |   updateConfig(config: Partial<LRUCacheConfig>): void {
243 |     this.config = { ...this.config, ...config };
244 |     this.stats.maxSize = this.config.maxSize;
245 | 
246 |     // Evict if new maxSize is smaller
247 |     while (this.cache.size > this.config.maxSize) {
248 |       this.evictOldest();
249 |     }
250 |   }
251 | }
252 | 
253 | /**
254 |  * Specialized LRU cache for Figma node data
255 |  * Includes version-aware caching
256 |  */
257 | export class NodeLRUCache extends LRUCache<NodeCacheEntry> {
258 |   /**
259 |    * Generate cache key for node data
260 |    */
261 |   static generateKey(fileKey: string, nodeId?: string, depth?: number): string {
262 |     const parts = [fileKey];
263 |     if (nodeId) parts.push(`n:${nodeId}`);
264 |     if (depth !== undefined) parts.push(`d:${depth}`);
265 |     return parts.join(":");
266 |   }
267 | 
268 |   /**
269 |    * Get node data with version check
270 |    */
271 |   getNode(fileKey: string, nodeId?: string, depth?: number, version?: string): unknown | null {
272 |     const key = NodeLRUCache.generateKey(fileKey, nodeId, depth);
273 |     const entry = this.get(key);
274 | 
275 |     if (!entry) return null;
276 | 
277 |     // Version mismatch - cache is stale
278 |     if (version && entry.version && entry.version !== version) {
279 |       this.delete(key);
280 |       return null;
281 |     }
282 | 
283 |     return entry.data;
284 |   }
285 | 
286 |   /**
287 |    * Set node data with metadata
288 |    */
289 |   setNode(
290 |     data: unknown,
291 |     fileKey: string,
292 |     nodeId?: string,
293 |     depth?: number,
294 |     version?: string,
295 |     ttl?: number,
296 |   ): void {
297 |     const key = NodeLRUCache.generateKey(fileKey, nodeId, depth);
298 |     const entry: NodeCacheEntry = {
299 |       data,
300 |       fileKey,
301 |       nodeId,
302 |       version,
303 |       depth,
304 |     };
305 |     this.set(key, entry, ttl);
306 |   }
307 | 
308 |   /**
309 |    * Invalidate all cache entries for a file
310 |    */
311 |   invalidateFile(fileKey: string): number {
312 |     let invalidated = 0;
313 |     for (const key of this.keys()) {
314 |       if (key.startsWith(fileKey)) {
315 |         this.delete(key);
316 |         invalidated++;
317 |       }
318 |     }
319 |     return invalidated;
320 |   }
321 | 
322 |   /**
323 |    * Invalidate cache entries for a specific node and its descendants
324 |    */
325 |   invalidateNode(fileKey: string, nodeId: string): number {
326 |     let invalidated = 0;
327 |     const prefix = NodeLRUCache.generateKey(fileKey, nodeId);
328 |     for (const key of this.keys()) {
329 |       if (key.startsWith(prefix)) {
330 |         this.delete(key);
331 |         invalidated++;
332 |       }
333 |     }
334 |     return invalidated;
335 |   }
336 | }
337 | 
```

--------------------------------------------------------------------------------
/src/services/cache/cache-manager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Unified Cache Manager
  3 |  *
  4 |  * Manages multi-layer caching with L1 (memory) and L2 (disk) layers.
  5 |  *
  6 |  * Cache hierarchy:
  7 |  * - L1: In-memory LRU cache (fast, limited size)
  8 |  * - L2: Disk-based cache (persistent, larger capacity)
  9 |  *
 10 |  * @module services/cache/cache-manager
 11 |  */
 12 | 
 13 | import type { CacheConfig, CacheStatistics } from "./types.js";
 14 | import { DEFAULT_MEMORY_CONFIG } from "./types.js";
 15 | import { NodeLRUCache } from "./lru-cache.js";
 16 | import { DiskCache } from "./disk-cache.js";
 17 | import os from "os";
 18 | import path from "path";
 19 | 
 20 | /**
 21 |  * Default cache configuration
 22 |  */
 23 | const DEFAULT_CONFIG: CacheConfig = {
 24 |   enabled: true,
 25 |   memory: DEFAULT_MEMORY_CONFIG,
 26 |   disk: {
 27 |     cacheDir: path.join(os.homedir(), ".figma-mcp-cache"),
 28 |     maxSize: 500 * 1024 * 1024, // 500MB
 29 |     ttl: 24 * 60 * 60 * 1000, // 24 hours
 30 |   },
 31 | };
 32 | 
 33 | /**
 34 |  * Unified cache manager with multi-layer caching
 35 |  */
 36 | export class CacheManager {
 37 |   private config: CacheConfig;
 38 |   private memoryCache: NodeLRUCache;
 39 |   private diskCache: DiskCache | null;
 40 | 
 41 |   constructor(config: Partial<CacheConfig> = {}) {
 42 |     this.config = this.mergeConfig(DEFAULT_CONFIG, config);
 43 | 
 44 |     // Skip initialization if disabled
 45 |     if (!this.config.enabled) {
 46 |       this.memoryCache = new NodeLRUCache({ maxSize: 0, defaultTTL: 0 });
 47 |       this.diskCache = null;
 48 |       return;
 49 |     }
 50 | 
 51 |     // Initialize L1: Memory cache
 52 |     this.memoryCache = new NodeLRUCache({
 53 |       maxSize: this.config.memory.maxNodeItems,
 54 |       defaultTTL: this.config.memory.nodeTTL,
 55 |     });
 56 | 
 57 |     // Initialize L2: Disk cache
 58 |     this.diskCache = new DiskCache(this.config.disk);
 59 |   }
 60 | 
 61 |   /**
 62 |    * Deep merge configuration
 63 |    */
 64 |   private mergeConfig(defaults: CacheConfig, overrides: Partial<CacheConfig>): CacheConfig {
 65 |     return {
 66 |       enabled: overrides.enabled ?? defaults.enabled,
 67 |       memory: {
 68 |         ...defaults.memory,
 69 |         ...overrides.memory,
 70 |       },
 71 |       disk: {
 72 |         ...defaults.disk,
 73 |         ...overrides.disk,
 74 |       },
 75 |     };
 76 |   }
 77 | 
 78 |   // ==================== Node Data Operations ====================
 79 | 
 80 |   /**
 81 |    * Get node data with multi-layer cache lookup
 82 |    *
 83 |    * Flow: L1 (memory) -> L2 (disk) -> null (cache miss)
 84 |    */
 85 |   async getNodeData<T>(
 86 |     fileKey: string,
 87 |     nodeId?: string,
 88 |     depth?: number,
 89 |     version?: string,
 90 |   ): Promise<T | null> {
 91 |     if (!this.config.enabled) return null;
 92 | 
 93 |     // L1: Check memory cache
 94 |     const memoryData = this.memoryCache.getNode(fileKey, nodeId, depth, version);
 95 |     if (memoryData !== null) {
 96 |       return memoryData as T;
 97 |     }
 98 | 
 99 |     // L2: Check disk cache
100 |     if (this.diskCache) {
101 |       const diskData = await this.diskCache.get<T>(fileKey, nodeId, depth, version);
102 |       if (diskData !== null) {
103 |         // Backfill L1 cache
104 |         this.memoryCache.setNode(diskData, fileKey, nodeId, depth, version);
105 |         return diskData;
106 |       }
107 |     }
108 | 
109 |     return null;
110 |   }
111 | 
112 |   /**
113 |    * Set node data in both cache layers
114 |    */
115 |   async setNodeData<T>(
116 |     data: T,
117 |     fileKey: string,
118 |     nodeId?: string,
119 |     depth?: number,
120 |     version?: string,
121 |   ): Promise<void> {
122 |     if (!this.config.enabled) return;
123 | 
124 |     // Write to L1 (memory)
125 |     this.memoryCache.setNode(data, fileKey, nodeId, depth, version);
126 | 
127 |     // Write to L2 (disk)
128 |     if (this.diskCache) {
129 |       await this.diskCache.set(data, fileKey, nodeId, depth, version);
130 |     }
131 |   }
132 | 
133 |   /**
134 |    * Check if node data exists in cache
135 |    */
136 |   async hasNodeData(fileKey: string, nodeId?: string, depth?: number): Promise<boolean> {
137 |     if (!this.config.enabled) return false;
138 | 
139 |     const key = NodeLRUCache.generateKey(fileKey, nodeId, depth);
140 |     if (this.memoryCache.has(key)) {
141 |       return true;
142 |     }
143 | 
144 |     if (this.diskCache) {
145 |       return this.diskCache.has(fileKey, nodeId, depth);
146 |     }
147 | 
148 |     return false;
149 |   }
150 | 
151 |   // ==================== Image Operations ====================
152 | 
153 |   /**
154 |    * Check if image is cached
155 |    */
156 |   async hasImage(fileKey: string, nodeId: string, format: string): Promise<string | null> {
157 |     if (!this.config.enabled || !this.diskCache) return null;
158 |     return this.diskCache.hasImage(fileKey, nodeId, format);
159 |   }
160 | 
161 |   /**
162 |    * Cache image file
163 |    */
164 |   async cacheImage(
165 |     sourcePath: string,
166 |     fileKey: string,
167 |     nodeId: string,
168 |     format: string,
169 |   ): Promise<string> {
170 |     if (!this.config.enabled || !this.diskCache) return sourcePath;
171 |     return this.diskCache.cacheImage(sourcePath, fileKey, nodeId, format);
172 |   }
173 | 
174 |   /**
175 |    * Copy image from cache to target path
176 |    */
177 |   async copyImageFromCache(
178 |     fileKey: string,
179 |     nodeId: string,
180 |     format: string,
181 |     targetPath: string,
182 |   ): Promise<boolean> {
183 |     if (!this.config.enabled || !this.diskCache) return false;
184 |     return this.diskCache.copyImageFromCache(fileKey, nodeId, format, targetPath);
185 |   }
186 | 
187 |   // ==================== Invalidation Operations ====================
188 | 
189 |   /**
190 |    * Invalidate all cache entries for a file
191 |    */
192 |   async invalidateFile(fileKey: string): Promise<{ memory: number; disk: number }> {
193 |     const memoryInvalidated = this.memoryCache.invalidateFile(fileKey);
194 |     const diskInvalidated = this.diskCache ? await this.diskCache.invalidateFile(fileKey) : 0;
195 | 
196 |     return { memory: memoryInvalidated, disk: diskInvalidated };
197 |   }
198 | 
199 |   /**
200 |    * Invalidate cache for a specific node
201 |    */
202 |   async invalidateNode(fileKey: string, nodeId: string): Promise<{ memory: number; disk: number }> {
203 |     const memoryInvalidated = this.memoryCache.invalidateNode(fileKey, nodeId);
204 |     const diskInvalidated = this.diskCache
205 |       ? (await this.diskCache.delete(fileKey, nodeId))
206 |         ? 1
207 |         : 0
208 |       : 0;
209 | 
210 |     return { memory: memoryInvalidated, disk: diskInvalidated };
211 |   }
212 | 
213 |   // ==================== Maintenance Operations ====================
214 | 
215 |   /**
216 |    * Clean expired cache entries from all layers
217 |    */
218 |   async cleanExpired(): Promise<{ memory: number; disk: number }> {
219 |     const memoryCleaned = this.memoryCache.cleanExpired();
220 |     const diskCleaned = this.diskCache ? await this.diskCache.cleanExpired() : 0;
221 | 
222 |     return { memory: memoryCleaned, disk: diskCleaned };
223 |   }
224 | 
225 |   /**
226 |    * Clear all cache
227 |    */
228 |   async clearAll(): Promise<void> {
229 |     this.memoryCache.clear();
230 |     if (this.diskCache) {
231 |       await this.diskCache.clearAll();
232 |     }
233 |   }
234 | 
235 |   /**
236 |    * Get combined cache statistics
237 |    */
238 |   async getStats(): Promise<CacheStatistics> {
239 |     const memoryStats = this.memoryCache.getStats();
240 | 
241 |     if (!this.diskCache) {
242 |       return {
243 |         enabled: this.config.enabled,
244 |         memory: {
245 |           hits: memoryStats.hits,
246 |           misses: memoryStats.misses,
247 |           size: memoryStats.size,
248 |           maxSize: memoryStats.maxSize,
249 |           hitRate: this.memoryCache.getHitRate(),
250 |           evictions: memoryStats.evictions,
251 |         },
252 |         disk: {
253 |           hits: 0,
254 |           misses: 0,
255 |           totalSize: 0,
256 |           maxSize: this.config.disk.maxSize,
257 |           nodeFileCount: 0,
258 |           imageFileCount: 0,
259 |         },
260 |       };
261 |     }
262 | 
263 |     const diskStats = await this.diskCache.getStats();
264 | 
265 |     return {
266 |       enabled: this.config.enabled,
267 |       memory: {
268 |         hits: memoryStats.hits,
269 |         misses: memoryStats.misses,
270 |         size: memoryStats.size,
271 |         maxSize: memoryStats.maxSize,
272 |         hitRate: this.memoryCache.getHitRate(),
273 |         evictions: memoryStats.evictions,
274 |       },
275 |       disk: diskStats,
276 |     };
277 |   }
278 | 
279 |   /**
280 |    * Get cache directory path
281 |    */
282 |   getCacheDir(): string {
283 |     return this.config.disk.cacheDir;
284 |   }
285 | 
286 |   /**
287 |    * Check if caching is enabled
288 |    */
289 |   isEnabled(): boolean {
290 |     return this.config.enabled;
291 |   }
292 | 
293 |   /**
294 |    * Reset statistics
295 |    */
296 |   resetStats(): void {
297 |     this.memoryCache.resetStats();
298 |   }
299 | }
300 | 
301 | // Export singleton instance
302 | export const cacheManager = new CacheManager();
303 | 
```

--------------------------------------------------------------------------------
/src/core/layout.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { isFrame, isLayout, isRectangle } from "~/utils/validation.js";
  2 | import type {
  3 |   Node as FigmaDocumentNode,
  4 |   HasFramePropertiesTrait,
  5 |   HasLayoutTrait,
  6 | } from "@figma/rest-api-spec";
  7 | import { generateCSSShorthand } from "~/utils/css.js";
  8 | 
  9 | export interface SimplifiedLayout {
 10 |   mode: "none" | "row" | "column";
 11 |   justifyContent?: "flex-start" | "flex-end" | "center" | "space-between" | "baseline" | "stretch";
 12 |   alignItems?: "flex-start" | "flex-end" | "center" | "space-between" | "baseline" | "stretch";
 13 |   alignSelf?: "flex-start" | "flex-end" | "center" | "stretch";
 14 |   wrap?: boolean;
 15 |   gap?: string;
 16 |   locationRelativeToParent?: {
 17 |     x: number;
 18 |     y: number;
 19 |   };
 20 |   dimensions?: {
 21 |     width?: number;
 22 |     height?: number;
 23 |     aspectRatio?: number;
 24 |   };
 25 |   padding?: string;
 26 |   sizing?: {
 27 |     horizontal?: "fixed" | "fill" | "hug";
 28 |     vertical?: "fixed" | "fill" | "hug";
 29 |   };
 30 |   overflowScroll?: ("x" | "y")[];
 31 |   position?: "absolute";
 32 | }
 33 | 
 34 | // Convert Figma's layout config into a more typical flex-like schema
 35 | export function buildSimplifiedLayout(
 36 |   n: FigmaDocumentNode,
 37 |   parent?: FigmaDocumentNode,
 38 | ): SimplifiedLayout {
 39 |   const frameValues = buildSimplifiedFrameValues(n);
 40 |   const layoutValues = buildSimplifiedLayoutValues(n, parent, frameValues.mode) || {};
 41 | 
 42 |   return { ...frameValues, ...layoutValues };
 43 | }
 44 | 
 45 | /**
 46 |  * Convert Figma's primaryAxisAlignItems to CSS justifyContent
 47 |  * Primary axis: horizontal for row, vertical for column
 48 |  */
 49 | function convertJustifyContent(
 50 |   axisAlign?: HasFramePropertiesTrait["primaryAxisAlignItems"],
 51 |   stretch?: {
 52 |     children: FigmaDocumentNode[];
 53 |     mode: "row" | "column";
 54 |   },
 55 | ) {
 56 |   // Check if all children fill the main axis (stretch behavior)
 57 |   if (stretch) {
 58 |     const { children, mode } = stretch;
 59 |     const shouldStretch =
 60 |       children.length > 0 &&
 61 |       children.every((c) => {
 62 |         if ("layoutPositioning" in c && c.layoutPositioning === "ABSOLUTE") return true;
 63 |         // Primary axis: horizontal for row, vertical for column
 64 |         if (mode === "row") {
 65 |           return "layoutSizingHorizontal" in c && c.layoutSizingHorizontal === "FILL";
 66 |         } else {
 67 |           return "layoutSizingVertical" in c && c.layoutSizingVertical === "FILL";
 68 |         }
 69 |       });
 70 | 
 71 |     if (shouldStretch) return "stretch";
 72 |   }
 73 | 
 74 |   switch (axisAlign) {
 75 |     case "MIN":
 76 |       // MIN, AKA flex-start, is the default alignment
 77 |       return undefined;
 78 |     case "MAX":
 79 |       return "flex-end";
 80 |     case "CENTER":
 81 |       return "center";
 82 |     case "SPACE_BETWEEN":
 83 |       return "space-between";
 84 |     default:
 85 |       return undefined;
 86 |   }
 87 | }
 88 | 
 89 | /**
 90 |  * Convert Figma's counterAxisAlignItems to CSS alignItems
 91 |  * Counter axis: vertical for row, horizontal for column
 92 |  * Note: SPACE_BETWEEN is not valid for counter axis in Figma
 93 |  */
 94 | function convertAlignItems(
 95 |   axisAlign?: HasFramePropertiesTrait["counterAxisAlignItems"],
 96 |   stretch?: {
 97 |     children: FigmaDocumentNode[];
 98 |     mode: "row" | "column";
 99 |   },
100 | ) {
101 |   // Check if all children fill the cross axis (stretch behavior)
102 |   if (stretch) {
103 |     const { children, mode } = stretch;
104 |     const shouldStretch =
105 |       children.length > 0 &&
106 |       children.every((c) => {
107 |         if ("layoutPositioning" in c && c.layoutPositioning === "ABSOLUTE") return true;
108 |         // Counter axis: vertical for row, horizontal for column
109 |         if (mode === "row") {
110 |           return "layoutSizingVertical" in c && c.layoutSizingVertical === "FILL";
111 |         } else {
112 |           return "layoutSizingHorizontal" in c && c.layoutSizingHorizontal === "FILL";
113 |         }
114 |       });
115 | 
116 |     if (shouldStretch) return "stretch";
117 |   }
118 | 
119 |   switch (axisAlign) {
120 |     case "MIN":
121 |       // MIN, AKA flex-start, is the default alignment
122 |       return undefined;
123 |     case "MAX":
124 |       return "flex-end";
125 |     case "CENTER":
126 |       return "center";
127 |     case "BASELINE":
128 |       return "baseline";
129 |     default:
130 |       return undefined;
131 |   }
132 | }
133 | 
134 | function convertSelfAlign(align?: HasLayoutTrait["layoutAlign"]) {
135 |   switch (align) {
136 |     case "MIN":
137 |       // MIN, AKA flex-start, is the default alignment
138 |       return undefined;
139 |     case "MAX":
140 |       return "flex-end";
141 |     case "CENTER":
142 |       return "center";
143 |     case "STRETCH":
144 |       return "stretch";
145 |     default:
146 |       return undefined;
147 |   }
148 | }
149 | 
150 | // interpret sizing
151 | function convertSizing(
152 |   s?: HasLayoutTrait["layoutSizingHorizontal"] | HasLayoutTrait["layoutSizingVertical"],
153 | ) {
154 |   if (s === "FIXED") return "fixed";
155 |   if (s === "FILL") return "fill";
156 |   if (s === "HUG") return "hug";
157 |   return undefined;
158 | }
159 | 
160 | function buildSimplifiedFrameValues(n: FigmaDocumentNode): SimplifiedLayout | { mode: "none" } {
161 |   if (!isFrame(n)) {
162 |     return { mode: "none" };
163 |   }
164 | 
165 |   const frameValues: SimplifiedLayout = {
166 |     mode:
167 |       !n.layoutMode || n.layoutMode === "NONE"
168 |         ? "none"
169 |         : n.layoutMode === "HORIZONTAL"
170 |           ? "row"
171 |           : "column",
172 |   };
173 | 
174 |   const overflowScroll: SimplifiedLayout["overflowScroll"] = [];
175 |   if (n.overflowDirection?.includes("HORIZONTAL")) overflowScroll.push("x");
176 |   if (n.overflowDirection?.includes("VERTICAL")) overflowScroll.push("y");
177 |   if (overflowScroll.length > 0) frameValues.overflowScroll = overflowScroll;
178 | 
179 |   if (frameValues.mode === "none") {
180 |     return frameValues;
181 |   }
182 | 
183 |   // Convert Figma alignment to CSS flex properties
184 |   frameValues.justifyContent = convertJustifyContent(n.primaryAxisAlignItems ?? "MIN", {
185 |     children: n.children,
186 |     mode: frameValues.mode,
187 |   });
188 |   frameValues.alignItems = convertAlignItems(n.counterAxisAlignItems ?? "MIN", {
189 |     children: n.children,
190 |     mode: frameValues.mode,
191 |   });
192 |   frameValues.alignSelf = convertSelfAlign(n.layoutAlign);
193 | 
194 |   // Only include wrap if it's set to WRAP, since flex layouts don't default to wrapping
195 |   frameValues.wrap = n.layoutWrap === "WRAP" ? true : undefined;
196 |   frameValues.gap = n.itemSpacing ? `${n.itemSpacing ?? 0}px` : undefined;
197 |   // gather padding
198 |   if (n.paddingTop || n.paddingBottom || n.paddingLeft || n.paddingRight) {
199 |     frameValues.padding = generateCSSShorthand({
200 |       top: n.paddingTop ?? 0,
201 |       right: n.paddingRight ?? 0,
202 |       bottom: n.paddingBottom ?? 0,
203 |       left: n.paddingLeft ?? 0,
204 |     });
205 |   }
206 | 
207 |   return frameValues;
208 | }
209 | 
210 | function buildSimplifiedLayoutValues(
211 |   n: FigmaDocumentNode,
212 |   parent: FigmaDocumentNode | undefined,
213 |   mode: "row" | "column" | "none",
214 | ): SimplifiedLayout | undefined {
215 |   if (!isLayout(n)) return undefined;
216 | 
217 |   const layoutValues: SimplifiedLayout = { mode };
218 | 
219 |   layoutValues.sizing = {
220 |     horizontal: convertSizing(n.layoutSizingHorizontal),
221 |     vertical: convertSizing(n.layoutSizingVertical),
222 |   };
223 | 
224 |   // Only include positioning-related properties if parent layout isn't flex or if the node is absolute
225 |   if (isFrame(parent) && (parent?.layoutMode === "NONE" || n.layoutPositioning === "ABSOLUTE")) {
226 |     if (n.layoutPositioning === "ABSOLUTE") {
227 |       layoutValues.position = "absolute";
228 |     }
229 |     if (n.absoluteBoundingBox && parent.absoluteBoundingBox) {
230 |       layoutValues.locationRelativeToParent = {
231 |         x: n.absoluteBoundingBox.x - (parent?.absoluteBoundingBox?.x ?? n.absoluteBoundingBox.x),
232 |         y: n.absoluteBoundingBox.y - (parent?.absoluteBoundingBox?.y ?? n.absoluteBoundingBox.y),
233 |       };
234 |     }
235 |     return layoutValues;
236 |   }
237 | 
238 |   // Handle dimensions based on layout growth and alignment
239 |   if (isRectangle("absoluteBoundingBox", n) && isRectangle("absoluteBoundingBox", parent)) {
240 |     const dimensions: { width?: number; height?: number; aspectRatio?: number } = {};
241 | 
242 |     // Only include dimensions that aren't meant to stretch
243 |     if (mode === "row") {
244 |       if (!n.layoutGrow && n.layoutSizingHorizontal === "FIXED")
245 |         dimensions.width = n.absoluteBoundingBox.width;
246 |       if (n.layoutAlign !== "STRETCH" && n.layoutSizingVertical === "FIXED")
247 |         dimensions.height = n.absoluteBoundingBox.height;
248 |     } else if (mode === "column") {
249 |       // column
250 |       if (n.layoutAlign !== "STRETCH" && n.layoutSizingHorizontal === "FIXED")
251 |         dimensions.width = n.absoluteBoundingBox.width;
252 |       if (!n.layoutGrow && n.layoutSizingVertical === "FIXED")
253 |         dimensions.height = n.absoluteBoundingBox.height;
254 | 
255 |       if (n.preserveRatio) {
256 |         dimensions.aspectRatio = n.absoluteBoundingBox?.width / n.absoluteBoundingBox?.height;
257 |       }
258 |     }
259 | 
260 |     if (Object.keys(dimensions).length > 0) {
261 |       layoutValues.dimensions = dimensions;
262 |     }
263 |   }
264 | 
265 |   return layoutValues;
266 | }
267 | 
```

--------------------------------------------------------------------------------
/tests/unit/algorithms/icon.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Icon Detection Algorithm Unit Tests
  3 |  *
  4 |  * Tests the icon detection algorithm for identifying and merging
  5 |  * vector layers that should be exported as single icons.
  6 |  */
  7 | 
  8 | import { describe, it, expect, beforeAll } from "vitest";
  9 | import * as fs from "fs";
 10 | import * as path from "path";
 11 | import { fileURLToPath } from "url";
 12 | import {
 13 |   detectIcon,
 14 |   analyzeNodeTree,
 15 |   DEFAULT_CONFIG,
 16 |   type FigmaNode,
 17 |   type IconDetectionResult,
 18 | } from "~/algorithms/icon/index.js";
 19 | 
 20 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
 21 | const fixturesPath = path.join(__dirname, "../../fixtures");
 22 | 
 23 | // Load test fixture
 24 | function loadTestData(): FigmaNode {
 25 |   const dataPath = path.join(fixturesPath, "figma-data/real-node-data.json");
 26 |   const rawData = JSON.parse(fs.readFileSync(dataPath, "utf-8"));
 27 |   const nodeKey = Object.keys(rawData.nodes)[0];
 28 |   return rawData.nodes[nodeKey].document;
 29 | }
 30 | 
 31 | // Count total icons detected
 32 | function countIcons(results: IconDetectionResult[]): number {
 33 |   return results.filter((r) => r.shouldMerge).length;
 34 | }
 35 | 
 36 | describe("Icon Detection Algorithm", () => {
 37 |   let testData: FigmaNode;
 38 | 
 39 |   beforeAll(() => {
 40 |     testData = loadTestData();
 41 |   });
 42 | 
 43 |   describe("Configuration", () => {
 44 |     it("should have sensible default configuration", () => {
 45 |       expect(DEFAULT_CONFIG.maxIconSize).toBe(300);
 46 |       expect(DEFAULT_CONFIG.minIconSize).toBe(8);
 47 |       expect(DEFAULT_CONFIG.mergeableRatio).toBe(0.6);
 48 |       expect(DEFAULT_CONFIG.maxDepth).toBe(5);
 49 |     });
 50 |   });
 51 | 
 52 |   describe("Size Constraints", () => {
 53 |     it("should reject nodes that are too large", () => {
 54 |       const largeNode: FigmaNode = {
 55 |         id: "large-1",
 56 |         name: "Large Node",
 57 |         type: "GROUP",
 58 |         absoluteBoundingBox: { x: 0, y: 0, width: 500, height: 500 },
 59 |         children: [{ id: "child-1", name: "Vector", type: "VECTOR" }],
 60 |       };
 61 | 
 62 |       const result = detectIcon(largeNode, DEFAULT_CONFIG);
 63 |       expect(result.shouldMerge).toBe(false);
 64 |       expect(result.reason).toContain("large");
 65 |     });
 66 | 
 67 |     it("should reject nodes that are too small", () => {
 68 |       const smallNode: FigmaNode = {
 69 |         id: "small-1",
 70 |         name: "Small Node",
 71 |         type: "GROUP",
 72 |         absoluteBoundingBox: { x: 0, y: 0, width: 4, height: 4 },
 73 |         children: [{ id: "child-1", name: "Vector", type: "VECTOR" }],
 74 |       };
 75 | 
 76 |       const result = detectIcon(smallNode, DEFAULT_CONFIG);
 77 |       expect(result.shouldMerge).toBe(false);
 78 |     });
 79 |   });
 80 | 
 81 |   describe("Node Type Detection", () => {
 82 |     it("should detect vector-only groups as icons", () => {
 83 |       const vectorGroup: FigmaNode = {
 84 |         id: "icon-1",
 85 |         name: "Search Icon",
 86 |         type: "GROUP",
 87 |         absoluteBoundingBox: { x: 0, y: 0, width: 24, height: 24 },
 88 |         children: [
 89 |           { id: "v1", name: "Circle", type: "ELLIPSE" },
 90 |           { id: "v2", name: "Line", type: "LINE" },
 91 |         ],
 92 |       };
 93 | 
 94 |       const result = detectIcon(vectorGroup, DEFAULT_CONFIG);
 95 |       expect(result.shouldMerge).toBe(true);
 96 |       expect(result.exportFormat).toBe("SVG");
 97 |     });
 98 | 
 99 |     it("should reject groups containing TEXT nodes", () => {
100 |       const textGroup: FigmaNode = {
101 |         id: "text-group",
102 |         name: "Button with Text",
103 |         type: "GROUP",
104 |         absoluteBoundingBox: { x: 0, y: 0, width: 100, height: 40 },
105 |         children: [
106 |           { id: "bg", name: "Background", type: "RECTANGLE" },
107 |           { id: "label", name: "Label", type: "TEXT" },
108 |         ],
109 |       };
110 | 
111 |       const result = detectIcon(textGroup, DEFAULT_CONFIG);
112 |       expect(result.shouldMerge).toBe(false);
113 |       expect(result.reason).toContain("TEXT");
114 |     });
115 |   });
116 | 
117 |   describe("Export Format Selection", () => {
118 |     it("should choose SVG for pure vector icons", () => {
119 |       const vectorIcon: FigmaNode = {
120 |         id: "svg-icon",
121 |         name: "Star",
122 |         type: "GROUP",
123 |         absoluteBoundingBox: { x: 0, y: 0, width: 24, height: 24 },
124 |         children: [{ id: "star", name: "Star", type: "STAR" }],
125 |       };
126 | 
127 |       const result = detectIcon(vectorIcon, DEFAULT_CONFIG);
128 |       expect(result.shouldMerge).toBe(true);
129 |       expect(result.exportFormat).toBe("SVG");
130 |     });
131 | 
132 |     it("should choose PNG for icons with complex effects", () => {
133 |       const effectIcon: FigmaNode = {
134 |         id: "effect-icon",
135 |         name: "Shadow Icon",
136 |         type: "GROUP",
137 |         absoluteBoundingBox: { x: 0, y: 0, width: 24, height: 24 },
138 |         effects: [{ type: "DROP_SHADOW", visible: true }],
139 |         children: [{ id: "shape", name: "Shape", type: "RECTANGLE" }],
140 |       };
141 | 
142 |       const result = detectIcon(effectIcon, DEFAULT_CONFIG);
143 |       if (result.shouldMerge) {
144 |         expect(result.exportFormat).toBe("PNG");
145 |       }
146 |     });
147 | 
148 |     it("should respect designer-specified export settings", () => {
149 |       const exportNode: FigmaNode = {
150 |         id: "export-icon",
151 |         name: "Custom Export",
152 |         type: "GROUP",
153 |         absoluteBoundingBox: { x: 0, y: 0, width: 32, height: 32 },
154 |         exportSettings: [{ format: "PNG", suffix: "", constraint: { type: "SCALE", value: 2 } }],
155 |         children: [{ id: "v1", name: "Vector", type: "VECTOR" }],
156 |       };
157 | 
158 |       const result = detectIcon(exportNode, DEFAULT_CONFIG);
159 |       expect(result.shouldMerge).toBe(true);
160 |       expect(result.exportFormat).toBe("PNG");
161 |     });
162 |   });
163 | 
164 |   describe("Mergeable Types", () => {
165 |     const mergeableTypes = [
166 |       "VECTOR",
167 |       "ELLIPSE",
168 |       "RECTANGLE",
169 |       "STAR",
170 |       "POLYGON",
171 |       "LINE",
172 |       "BOOLEAN_OPERATION",
173 |     ];
174 | 
175 |     mergeableTypes.forEach((type) => {
176 |       it(`should recognize ${type} as mergeable`, () => {
177 |         const node: FigmaNode = {
178 |           id: `${type.toLowerCase()}-icon`,
179 |           name: `${type} Icon`,
180 |           type: "GROUP",
181 |           absoluteBoundingBox: { x: 0, y: 0, width: 24, height: 24 },
182 |           children: [{ id: "child", name: type, type: type }],
183 |         };
184 | 
185 |         const result = detectIcon(node, DEFAULT_CONFIG);
186 |         expect(result.shouldMerge).toBe(true);
187 |       });
188 |     });
189 |   });
190 | 
191 |   describe("Real Figma Data", () => {
192 |     it("should load and parse real Figma data", () => {
193 |       expect(testData).toBeDefined();
194 |       expect(testData.type).toBe("GROUP");
195 |     });
196 | 
197 |     it("should analyze entire node tree", () => {
198 |       const result = analyzeNodeTree(testData, DEFAULT_CONFIG);
199 |       expect(result).toHaveProperty("processedTree");
200 |       expect(result).toHaveProperty("exportableIcons");
201 |       expect(result).toHaveProperty("summary");
202 |       expect(Array.isArray(result.exportableIcons)).toBe(true);
203 |     });
204 | 
205 |     it("should detect appropriate number of icons", () => {
206 |       const result = analyzeNodeTree(testData, DEFAULT_CONFIG);
207 |       const iconCount = countIcons(result.exportableIcons);
208 | 
209 |       // Should detect some icons but not too many (avoid fragmentation)
210 |       expect(iconCount).toBeGreaterThanOrEqual(0);
211 |       expect(iconCount).toBeLessThan(10); // Should be merged, not fragmented
212 |     });
213 | 
214 |     it("should not mark root node as icon", () => {
215 |       const result = detectIcon(testData, DEFAULT_CONFIG);
216 |       expect(result.shouldMerge).toBe(false);
217 |     });
218 |   });
219 | 
220 |   describe("Edge Cases", () => {
221 |     it("should handle nodes without children", () => {
222 |       const leafNode: FigmaNode = {
223 |         id: "leaf",
224 |         name: "Single Vector",
225 |         type: "VECTOR",
226 |         absoluteBoundingBox: { x: 0, y: 0, width: 24, height: 24 },
227 |       };
228 | 
229 |       const result = detectIcon(leafNode, DEFAULT_CONFIG);
230 |       expect(result).toBeDefined();
231 |     });
232 | 
233 |     it("should handle nodes without bounding box", () => {
234 |       const noBoundsNode: FigmaNode = {
235 |         id: "no-bounds",
236 |         name: "No Bounds",
237 |         type: "GROUP",
238 |         children: [],
239 |       };
240 | 
241 |       const result = detectIcon(noBoundsNode, DEFAULT_CONFIG);
242 |       expect(result.shouldMerge).toBe(false);
243 |     });
244 | 
245 |     it("should handle deeply nested structures", () => {
246 |       const deepNode: FigmaNode = {
247 |         id: "deep",
248 |         name: "Deep",
249 |         type: "GROUP",
250 |         absoluteBoundingBox: { x: 0, y: 0, width: 24, height: 24 },
251 |         children: [
252 |           {
253 |             id: "level1",
254 |             name: "Level 1",
255 |             type: "GROUP",
256 |             children: [
257 |               {
258 |                 id: "level2",
259 |                 name: "Level 2",
260 |                 type: "GROUP",
261 |                 children: [{ id: "vector", name: "Vector", type: "VECTOR" }],
262 |               },
263 |             ],
264 |           },
265 |         ],
266 |       };
267 | 
268 |       const result = detectIcon(deepNode, DEFAULT_CONFIG);
269 |       expect(result).toBeDefined();
270 |     });
271 |   });
272 | });
273 | 
```

--------------------------------------------------------------------------------
/tests/integration/parser.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Parser Integration Tests
  3 |  *
  4 |  * Tests the complete parsing pipeline from raw Figma API response
  5 |  * to simplified node structure.
  6 |  */
  7 | 
  8 | import { describe, it, expect, beforeAll } from "vitest";
  9 | import * as fs from "fs";
 10 | import * as path from "path";
 11 | import { fileURLToPath } from "url";
 12 | import { parseFigmaResponse } from "~/core/parser.js";
 13 | 
 14 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
 15 | const fixturesPath = path.join(__dirname, "../fixtures");
 16 | 
 17 | // Load fixtures
 18 | function loadRawData(): unknown {
 19 |   const dataPath = path.join(fixturesPath, "figma-data/real-node-data.json");
 20 |   return JSON.parse(fs.readFileSync(dataPath, "utf-8"));
 21 | }
 22 | 
 23 | function loadExpectedOutput(): unknown {
 24 |   const dataPath = path.join(fixturesPath, "expected/real-node-data-optimized.json");
 25 |   return JSON.parse(fs.readFileSync(dataPath, "utf-8"));
 26 | }
 27 | 
 28 | describe("Figma Response Parser", () => {
 29 |   let rawData: unknown;
 30 |   let expectedOutput: ReturnType<typeof loadExpectedOutput>;
 31 | 
 32 |   beforeAll(() => {
 33 |     rawData = loadRawData();
 34 |     expectedOutput = loadExpectedOutput();
 35 |   });
 36 | 
 37 |   describe("Basic Parsing", () => {
 38 |     it("should parse raw Figma response", () => {
 39 |       const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);
 40 | 
 41 |       expect(result).toBeDefined();
 42 |       expect(result.name).toBeDefined();
 43 |       expect(result.nodes).toBeDefined();
 44 |       expect(Array.isArray(result.nodes)).toBe(true);
 45 |     });
 46 | 
 47 |     it("should extract file metadata", () => {
 48 |       const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);
 49 | 
 50 |       expect(result.name).toBe("Vigilkids产品站");
 51 |       expect(result.lastModified).toBeDefined();
 52 |     });
 53 |   });
 54 | 
 55 |   describe("Node Structure", () => {
 56 |     it("should preserve node hierarchy", () => {
 57 |       const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);
 58 | 
 59 |       expect(result.nodes.length).toBeGreaterThan(0);
 60 | 
 61 |       const rootNode = result.nodes[0];
 62 |       expect(rootNode.id).toBeDefined();
 63 |       expect(rootNode.name).toBeDefined();
 64 |       expect(rootNode.type).toBeDefined();
 65 |     });
 66 | 
 67 |     it("should generate CSS styles", () => {
 68 |       const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);
 69 | 
 70 |       const rootNode = result.nodes[0];
 71 |       expect(rootNode.cssStyles).toBeDefined();
 72 |       expect(rootNode.cssStyles?.width).toBeDefined();
 73 |       expect(rootNode.cssStyles?.height).toBeDefined();
 74 |     });
 75 |   });
 76 | 
 77 |   describe("Data Compression", () => {
 78 |     it("should significantly reduce data size", () => {
 79 |       const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);
 80 | 
 81 |       const originalSize = Buffer.byteLength(JSON.stringify(rawData));
 82 |       const simplifiedSize = Buffer.byteLength(JSON.stringify(result));
 83 |       const compressionRate = ((originalSize - simplifiedSize) / originalSize) * 100;
 84 | 
 85 |       // Should achieve at least 70% compression
 86 |       expect(compressionRate).toBeGreaterThan(70);
 87 |     });
 88 |   });
 89 | 
 90 |   describe("CSS Style Generation", () => {
 91 |     it("should convert colors to hex format", () => {
 92 |       const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);
 93 | 
 94 |       // Find a node with background color
 95 |       const findNodeWithBg = (
 96 |         nodes: Array<{ cssStyles?: Record<string, unknown>; children?: unknown[] }>,
 97 |       ): Record<string, unknown> | null => {
 98 |         for (const node of nodes) {
 99 |           if (node.cssStyles?.backgroundColor) {
100 |             return node.cssStyles;
101 |           }
102 |           if (node.children) {
103 |             const found = findNodeWithBg(
104 |               node.children as Array<{ cssStyles?: Record<string, unknown>; children?: unknown[] }>,
105 |             );
106 |             if (found) return found;
107 |           }
108 |         }
109 |         return null;
110 |       };
111 | 
112 |       const styles = findNodeWithBg(result.nodes);
113 |       if (styles?.backgroundColor) {
114 |         expect(styles.backgroundColor).toMatch(/^#[A-Fa-f0-9]{6}$/);
115 |       }
116 |     });
117 | 
118 |     it("should round pixel values to integers", () => {
119 |       const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);
120 | 
121 |       const rootNode = result.nodes[0];
122 |       const width = rootNode.cssStyles?.width as string;
123 |       const height = rootNode.cssStyles?.height as string;
124 | 
125 |       // Should be integer pixel values
126 |       expect(width).toMatch(/^\d+px$/);
127 |       expect(height).toMatch(/^\d+px$/);
128 |     });
129 |   });
130 | 
131 |   describe("Layout Detection Integration", () => {
132 |     it("should detect flex layouts in appropriate nodes", () => {
133 |       const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);
134 | 
135 |       // Find nodes with flex properties
136 |       const findFlexNode = (
137 |         nodes: Array<{ cssStyles?: Record<string, unknown>; children?: unknown[] }>,
138 |       ): Record<string, unknown> | null => {
139 |         for (const node of nodes) {
140 |           if (node.cssStyles?.display === "flex") {
141 |             return node.cssStyles;
142 |           }
143 |           if (node.children) {
144 |             const found = findFlexNode(
145 |               node.children as Array<{ cssStyles?: Record<string, unknown>; children?: unknown[] }>,
146 |             );
147 |             if (found) return found;
148 |           }
149 |         }
150 |         return null;
151 |       };
152 | 
153 |       const flexStyles = findFlexNode(result.nodes);
154 |       if (flexStyles) {
155 |         expect(flexStyles.display).toBe("flex");
156 |         expect(flexStyles.flexDirection).toBeDefined();
157 |       }
158 |     });
159 |   });
160 | 
161 |   describe("Icon Detection Integration", () => {
162 |     it("should mark icon nodes with exportInfo", () => {
163 |       const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);
164 | 
165 |       // Find nodes with export info
166 |       const findExportNode = (
167 |         nodes: Array<{ exportInfo?: unknown; children?: unknown[] }>,
168 |       ): unknown | null => {
169 |         for (const node of nodes) {
170 |           if (node.exportInfo) {
171 |             return node.exportInfo;
172 |           }
173 |           if (node.children) {
174 |             const found = findExportNode(
175 |               node.children as Array<{ exportInfo?: unknown; children?: unknown[] }>,
176 |             );
177 |             if (found) return found;
178 |           }
179 |         }
180 |         return null;
181 |       };
182 | 
183 |       const exportInfo = findExportNode(result.nodes);
184 |       if (exportInfo) {
185 |         expect(exportInfo).toHaveProperty("type");
186 |         expect(exportInfo).toHaveProperty("format");
187 |         expect(exportInfo).toHaveProperty("fileName");
188 |       }
189 |     });
190 |   });
191 | 
192 |   describe("Text Node Processing", () => {
193 |     it("should extract text content", () => {
194 |       const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);
195 | 
196 |       // Find text nodes
197 |       const findTextNode = (
198 |         nodes: Array<{ type?: string; text?: string; children?: unknown[] }>,
199 |       ): { text?: string } | null => {
200 |         for (const node of nodes) {
201 |           if (node.type === "TEXT" && node.text) {
202 |             return node;
203 |           }
204 |           if (node.children) {
205 |             const found = findTextNode(
206 |               node.children as Array<{ type?: string; text?: string; children?: unknown[] }>,
207 |             );
208 |             if (found) return found;
209 |           }
210 |         }
211 |         return null;
212 |       };
213 | 
214 |       const textNode = findTextNode(result.nodes);
215 |       if (textNode) {
216 |         expect(textNode.text).toBeDefined();
217 |         expect(typeof textNode.text).toBe("string");
218 |       }
219 |     });
220 | 
221 |     it("should include font styles for text nodes", () => {
222 |       const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);
223 | 
224 |       const findTextStyles = (
225 |         nodes: Array<{ type?: string; cssStyles?: Record<string, unknown>; children?: unknown[] }>,
226 |       ): Record<string, unknown> | null => {
227 |         for (const node of nodes) {
228 |           if (node.type === "TEXT" && node.cssStyles) {
229 |             return node.cssStyles;
230 |           }
231 |           if (node.children) {
232 |             const found = findTextStyles(
233 |               node.children as Array<{
234 |                 type?: string;
235 |                 cssStyles?: Record<string, unknown>;
236 |                 children?: unknown[];
237 |               }>,
238 |             );
239 |             if (found) return found;
240 |           }
241 |         }
242 |         return null;
243 |       };
244 | 
245 |       const textStyles = findTextStyles(result.nodes);
246 |       if (textStyles) {
247 |         expect(textStyles.fontFamily).toBeDefined();
248 |         expect(textStyles.fontSize).toBeDefined();
249 |       }
250 |     });
251 |   });
252 | 
253 |   describe("Output Stability", () => {
254 |     it("should produce consistent output structure", () => {
255 |       const result = parseFigmaResponse(rawData as Parameters<typeof parseFigmaResponse>[0]);
256 | 
257 |       // Compare key structure with expected output
258 |       expect(Object.keys(result)).toEqual(Object.keys(expectedOutput as object));
259 |       expect(result.nodes.length).toBe((expectedOutput as { nodes: unknown[] }).nodes.length);
260 |     });
261 |   });
262 | });
263 | 
```

--------------------------------------------------------------------------------
/src/resources/figma-resources.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Figma Resources - Expose Figma data as MCP Resources
  3 |  * Resources are lightweight, on-demand data sources that save tokens
  4 |  */
  5 | 
  6 | import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
  7 | import type { FigmaService } from "../services/figma.js";
  8 | import type { SimplifiedNode } from "../types/index.js";
  9 | 
 10 | // ==================== Types ====================
 11 | 
 12 | export interface FileMetadata {
 13 |   name: string;
 14 |   lastModified: string;
 15 |   version: string;
 16 |   pages: Array<{ id: string; name: string; childCount: number }>;
 17 |   thumbnailUrl?: string;
 18 | }
 19 | 
 20 | export interface StyleTokens {
 21 |   colors: Array<{ name: string; value: string; hex: string }>;
 22 |   typography: Array<{
 23 |     name: string;
 24 |     fontFamily: string;
 25 |     fontSize: number;
 26 |     fontWeight: number;
 27 |     lineHeight?: number;
 28 |   }>;
 29 |   effects: Array<{ name: string; type: string; value: string }>;
 30 | }
 31 | 
 32 | export interface ComponentSummary {
 33 |   id: string;
 34 |   name: string;
 35 |   description?: string;
 36 |   type: "COMPONENT" | "COMPONENT_SET";
 37 |   variants?: string[];
 38 | }
 39 | 
 40 | // ==================== Resource Handlers ====================
 41 | 
 42 | /**
 43 |  * Extract file metadata (lightweight, ~200 tokens)
 44 |  */
 45 | export async function getFileMetadata(
 46 |   figmaService: FigmaService,
 47 |   fileKey: string,
 48 | ): Promise<FileMetadata> {
 49 |   const file = await figmaService.getFile(fileKey, 1); // depth=1 for minimal data
 50 | 
 51 |   const pages = file.nodes
 52 |     .filter((node) => node.type === "CANVAS")
 53 |     .map((page) => ({
 54 |       id: page.id,
 55 |       name: page.name,
 56 |       childCount: page.children?.length ?? 0,
 57 |     }));
 58 | 
 59 |   return {
 60 |     name: file.name,
 61 |     lastModified: file.lastModified,
 62 |     version: file.version ?? "",
 63 |     pages,
 64 |   };
 65 | }
 66 | 
 67 | /**
 68 |  * Extract style tokens from file (colors, typography, effects) (~500 tokens)
 69 |  */
 70 | export async function getStyleTokens(
 71 |   figmaService: FigmaService,
 72 |   fileKey: string,
 73 | ): Promise<StyleTokens> {
 74 |   const file = await figmaService.getFile(fileKey, 3); // Need some depth for styles
 75 | 
 76 |   const colors: StyleTokens["colors"] = [];
 77 |   const typography: StyleTokens["typography"] = [];
 78 |   const effects: StyleTokens["effects"] = [];
 79 |   const seenColors = new Set<string>();
 80 |   const seenFonts = new Set<string>();
 81 | 
 82 |   function extractFromNode(node: SimplifiedNode) {
 83 |     // Extract colors from fills
 84 |     if (node.cssStyles) {
 85 |       const bgColor = node.cssStyles.background || node.cssStyles.backgroundColor;
 86 |       if (bgColor && !seenColors.has(bgColor)) {
 87 |         seenColors.add(bgColor);
 88 |         colors.push({
 89 |           name: node.name || "unnamed",
 90 |           value: bgColor,
 91 |           hex: bgColor,
 92 |         });
 93 |       }
 94 | 
 95 |       const textColor = node.cssStyles.color;
 96 |       if (textColor && !seenColors.has(textColor)) {
 97 |         seenColors.add(textColor);
 98 |         colors.push({
 99 |           name: `${node.name || "text"}-color`,
100 |           value: textColor,
101 |           hex: textColor,
102 |         });
103 |       }
104 | 
105 |       // Extract typography
106 |       if (node.cssStyles.fontFamily && node.cssStyles.fontSize) {
107 |         const fontKey = `${node.cssStyles.fontFamily}-${node.cssStyles.fontSize}-${node.cssStyles.fontWeight || 400}`;
108 |         if (!seenFonts.has(fontKey)) {
109 |           seenFonts.add(fontKey);
110 |           typography.push({
111 |             name: node.name || "text",
112 |             fontFamily: node.cssStyles.fontFamily,
113 |             fontSize: parseFloat(String(node.cssStyles.fontSize)) || 14,
114 |             fontWeight: parseFloat(String(node.cssStyles.fontWeight)) || 400,
115 |             lineHeight: node.cssStyles.lineHeight
116 |               ? parseFloat(String(node.cssStyles.lineHeight))
117 |               : undefined,
118 |           });
119 |         }
120 |       }
121 | 
122 |       // Extract effects (shadows, blur)
123 |       if (node.cssStyles.boxShadow) {
124 |         effects.push({
125 |           name: `${node.name || "element"}-shadow`,
126 |           type: "shadow",
127 |           value: String(node.cssStyles.boxShadow),
128 |         });
129 |       }
130 |     }
131 | 
132 |     // Recurse into children
133 |     if (node.children) {
134 |       for (const child of node.children) {
135 |         extractFromNode(child);
136 |       }
137 |     }
138 |   }
139 | 
140 |   for (const node of file.nodes) {
141 |     extractFromNode(node);
142 |   }
143 | 
144 |   // Limit results to avoid token bloat
145 |   return {
146 |     colors: colors.slice(0, 20),
147 |     typography: typography.slice(0, 10),
148 |     effects: effects.slice(0, 10),
149 |   };
150 | }
151 | 
152 | /**
153 |  * Extract component list (~300 tokens)
154 |  */
155 | export async function getComponentList(
156 |   figmaService: FigmaService,
157 |   fileKey: string,
158 | ): Promise<ComponentSummary[]> {
159 |   const file = await figmaService.getFile(fileKey, 5); // Need depth for components
160 |   const components: ComponentSummary[] = [];
161 | 
162 |   function findComponents(node: SimplifiedNode) {
163 |     if (node.type === "COMPONENT" || node.type === "COMPONENT_SET") {
164 |       components.push({
165 |         id: node.id,
166 |         name: node.name,
167 |         type: node.type as "COMPONENT" | "COMPONENT_SET",
168 |         variants:
169 |           node.type === "COMPONENT_SET" ? node.children?.map((c) => c.name).slice(0, 5) : undefined,
170 |       });
171 |     }
172 | 
173 |     if (node.children) {
174 |       for (const child of node.children) {
175 |         findComponents(child);
176 |       }
177 |     }
178 |   }
179 | 
180 |   for (const node of file.nodes) {
181 |     findComponents(node);
182 |   }
183 | 
184 |   return components.slice(0, 50); // Limit to 50 components
185 | }
186 | 
187 | /**
188 |  * Extract images/assets list (~400 tokens)
189 |  */
190 | export async function getAssetList(
191 |   figmaService: FigmaService,
192 |   fileKey: string,
193 | ): Promise<
194 |   Array<{
195 |     nodeId: string;
196 |     name: string;
197 |     type: "vector" | "image" | "icon";
198 |     exportFormats: string[];
199 |     imageRef?: string;
200 |   }>
201 | > {
202 |   const file = await figmaService.getFile(fileKey, 5);
203 |   const assets: Array<{
204 |     nodeId: string;
205 |     name: string;
206 |     type: "vector" | "image" | "icon";
207 |     exportFormats: string[];
208 |     imageRef?: string;
209 |   }> = [];
210 | 
211 |   function findAssets(node: SimplifiedNode) {
212 |     // Check for exportable assets (exportInfo is a single object, not array)
213 |     if (node.exportInfo) {
214 |       const isIcon =
215 |         node.type === "VECTOR" ||
216 |         node.type === "BOOLEAN_OPERATION" ||
217 |         node.exportInfo.type === "IMAGE";
218 | 
219 |       assets.push({
220 |         nodeId: node.id,
221 |         name: node.name,
222 |         type: isIcon ? "icon" : "vector",
223 |         exportFormats: [node.exportInfo.format],
224 |       });
225 |     }
226 | 
227 |     // Check for image fills in fills array
228 |     const imageFill = node.fills?.find(
229 |       (fill): fill is { type: "IMAGE"; imageRef?: string } =>
230 |         typeof fill === "object" && "type" in fill && fill.type === "IMAGE",
231 |     );
232 |     if (imageFill?.imageRef) {
233 |       assets.push({
234 |         nodeId: node.id,
235 |         name: node.name,
236 |         type: "image",
237 |         exportFormats: ["png", "jpg"],
238 |         imageRef: imageFill.imageRef,
239 |       });
240 |     }
241 | 
242 |     if (node.children) {
243 |       for (const child of node.children) {
244 |         findAssets(child);
245 |       }
246 |     }
247 |   }
248 | 
249 |   for (const node of file.nodes) {
250 |     findAssets(node);
251 |   }
252 | 
253 |   return assets.slice(0, 100); // Limit to 100 assets
254 | }
255 | 
256 | // ==================== Resource Templates ====================
257 | 
258 | /**
259 |  * Create resource template for file metadata
260 |  */
261 | export function createFileMetadataTemplate(): ResourceTemplate {
262 |   return new ResourceTemplate("figma://file/{fileKey}", {
263 |     list: undefined, // Can't list all files without user's file list
264 |     complete: {
265 |       fileKey: async () => [], // Could be enhanced with recent files
266 |     },
267 |   });
268 | }
269 | 
270 | /**
271 |  * Create resource template for style tokens
272 |  */
273 | export function createStylesTemplate(): ResourceTemplate {
274 |   return new ResourceTemplate("figma://file/{fileKey}/styles", {
275 |     list: undefined,
276 |     complete: {
277 |       fileKey: async () => [],
278 |     },
279 |   });
280 | }
281 | 
282 | /**
283 |  * Create resource template for components
284 |  */
285 | export function createComponentsTemplate(): ResourceTemplate {
286 |   return new ResourceTemplate("figma://file/{fileKey}/components", {
287 |     list: undefined,
288 |     complete: {
289 |       fileKey: async () => [],
290 |     },
291 |   });
292 | }
293 | 
294 | /**
295 |  * Create resource template for assets
296 |  */
297 | export function createAssetsTemplate(): ResourceTemplate {
298 |   return new ResourceTemplate("figma://file/{fileKey}/assets", {
299 |     list: undefined,
300 |     complete: {
301 |       fileKey: async () => [],
302 |     },
303 |   });
304 | }
305 | 
306 | // ==================== Help Content ====================
307 | 
308 | export const FIGMA_MCP_HELP = `# Figma MCP Server - Resource Guide
309 | 
310 | ## Available Resources
311 | 
312 | ### File Metadata
313 | \`figma://file/{fileKey}\`
314 | Returns: File name, pages, last modified date
315 | Token cost: ~200
316 | 
317 | ### Design Tokens (Styles)
318 | \`figma://file/{fileKey}/styles\`
319 | Returns: Colors, typography, effects extracted from file
320 | Token cost: ~500
321 | 
322 | ### Component List
323 | \`figma://file/{fileKey}/components\`
324 | Returns: All components and component sets with variants
325 | Token cost: ~300
326 | 
327 | ### Asset List
328 | \`figma://file/{fileKey}/assets\`
329 | Returns: Exportable images, icons, vectors with node IDs
330 | Token cost: ~400
331 | 
332 | ## How to Get fileKey
333 | 
334 | From Figma URL: \`figma.com/file/{fileKey}/...\`
335 | Or: \`figma.com/design/{fileKey}/...\`
336 | 
337 | ## Example Usage
338 | 
339 | 1. Read file metadata: \`figma://file/abc123\`
340 | 2. Get color palette: \`figma://file/abc123/styles\`
341 | 3. List components: \`figma://file/abc123/components\`
342 | 4. Find assets to download: \`figma://file/abc123/assets\`
343 | 
344 | ## Tools vs Resources
345 | 
346 | - **Resources**: Read-only data, user-controlled, lightweight
347 | - **Tools**: Actions (download images), AI-controlled, heavier
348 | 
349 | Use Resources for exploration, Tools for execution.
350 | `;
351 | 
```
Page 1/8FirstPrevNextLast