#
tokens: 19816/50000 37/37 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .env.example
├── .gitignore
├── .vscode
│   └── settings.json
├── biome.jsonc
├── bun.build.script.ts
├── bun.lock
├── Dockerfile
├── docs
│   ├── banner.png
│   ├── bannerv3.png
│   ├── en
│   │   ├── FAQ.md
│   │   └── GettingStarted.md
│   ├── Info.md
│   └── zh
│       ├── FAQ.md
│       └── GettingStarted.md
├── glama.json
├── LICENSE
├── mcp.json
├── package.json
├── pnpm-lock.yaml
├── README-zh-CN.md
├── README.md
├── smithery.yaml
├── src
│   ├── cli.ts
│   ├── server
│   │   └── figma
│   │       ├── apis
│   │       │   ├── f2c.ts
│   │       │   └── figma.ts
│   │       ├── config.ts
│   │       ├── helpers
│   │       │   ├── downloader.ts
│   │       │   ├── figma.ts
│   │       │   └── index.ts
│   │       ├── index.ts
│   │       ├── notifications
│   │       │   ├── index.ts
│   │       │   └── logger.ts
│   │       ├── tools
│   │       │   ├── f2c.ts
│   │       │   ├── figma.ts
│   │       │   └── v03.ts
│   │       └── types
│   │           ├── f2c.ts
│   │           └── figma.ts
│   ├── stdio.ts
│   ├── streamable-http.ts
│   ├── test
│   │   ├── api.test.ts
│   │   └── e2e.test.ts
│   ├── transports
│   │   ├── stdio.ts
│   │   └── streamable-http
│   │       ├── http-server.ts
│   │       ├── index.ts
│   │       ├── with-session-steamable-http.ts
│   │       └── without-session-steamable-http.ts
│   └── utils
│       ├── fetch.ts
│       ├── index.ts
│       └── logger.ts
├── STARTKIT.md
├── tsconfig.json
└── user_rules.json
```

# Files

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
1 | FIGMA_API_KEY=your_figma_api_key_here
```

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

```
1 | node_modules
2 | dist/
3 | .env
4 | .demo/
5 | build/
6 | .comate/
7 | .kiro
8 | 
```

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

```markdown
 1 | # F2C MCP Server 
 2 | ![MCP Server](https://badge.mcpx.dev?type=server 'MCP Server')
 3 | [![smithery badge](https://smithery.ai/badge/@f2c-ai/f2c-mcp)](https://smithery.ai/server/@f2c-ai/f2c-mcp)
 4 | [![npm version][npm-version-src]][npm-version-href]
 5 | [![npm downloads][npm-downloads-src]][npm-downloads-href]
 6 | [![github][github-src]][github-href]
 7 | [![node][node-src]][node-href]
 8 | 
 9 | 
10 | [npm-version-src]: https://img.shields.io/npm/v/@f2c/mcp?style=flat&colorA=18181B&colorB=F0DB4F
11 | [npm-version-href]: https://npmjs.com/package/@f2c/mcp
12 | [npm-downloads-src]: https://img.shields.io/npm/dm/@f2c/mcp?style=flat&colorA=18181B&colorB=F0DB4F
13 | [npm-downloads-href]: https://npmjs.com/package/@f2c/mcp
14 | [github-src]: https://img.shields.io/badge/github-@f2c/mcp-blue?style=flat&colorA=18181B&colorB=F0DB4F
15 | [github-href]: https://github.com/f2c-ai/f2c-mcp
16 | [node-src]: https://img.shields.io/node/v/@f2c/mcp?style=flat&colorA=18181B&colorB=F0DB4F
17 | [node-href]: https://nodejs.org/en/about/previous-releases
18 | 
19 | > Due to [Figma REST API rate limits](https://developers.figma.com/docs/rest-api/rate-limits/), if you are affected, please switch to [@f2c/mcp-plugin](https://www.npmjs.com/package/@f2c/mcp-plugin) for normal operation.
20 | 
21 | English | [简体中文](./README-zh-CN.md)
22 | 
23 | A Model Context Protocol server for Figma Design to Code using [F2C](https://f2c.yy.com/).
24 | 
25 | <a href="https://glama.ai/mcp/servers/@f2c-ai/f2c-mcp">
26 |   <img width="380" height="200" src="https://glama.ai/mcp/servers/@f2c-ai/f2c-mcp/badge" alt="f2c-mcp-server MCP server" />
27 | </a>
28 | 
29 | ## Features
30 | <img alt="f2c" src="https://raw.githubusercontent.com/f2c-ai/f2c-mcp/main/docs/bannerv3.png" />
31 | 
32 | - 🎨 Pixel-Perfect HTML/CSS:F2C converts Figma designs to pixel-perfect HTML/CSS with precision.
33 | - ⚛️ Multi-Framework Support:F2C generates React, CSS Modules, and Tailwind CSS code for fast development.
34 | - 🧠 Figma Design Context:F2C integrates design context, ensuring compatibility with AI tools like Cursor.
35 | - 🔗 Figma File URL Parsing:F2C converts design nodes via Figma URLs, streamlining workflows.
36 | - 🖼️ Remote Image Localization:F2C automates downloading Figma images to local assets for efficiency.
37 | 
38 | ## How it works
39 | 1. [Configure the Server](docs/en/GettingStarted.md) in an MCP-supported IDE (e.g., Cursor, Trae).
40 | > recommended to use [Comate AI IDE](https://comate.baidu.com/zh/download/ai-ide) 
41 | 2. Open your chat in IDE (e.g. agent mode in Cursor).
42 | 3. Paste a link to a Figma Node (Right-click any node in the Figma Layer panel to copy it).
43 | 4. Enter your requirements in the chat, such as fetching node data, downloading images, converting to code, etc.
44 | 
45 | ## Configuration and Development
46 | 
47 | See [Configuration and Development](docs/en/GettingStarted.md)
48 | 
49 | ## Data Privacy Notice
50 | The logging tools integrated in this project are used solely for basic usage statistics and error log reporting. No sensitive information or user data is collected. All reported data is used exclusively to improve product quality and user experience.
51 | 
52 | ## FAQ
53 | See [FAQ](docs/en/FAQ.md)
54 | 
55 | ## Credits
56 | 
57 | Thanks to:
58 | 
59 | + [Framelink Figma MCP Server](https://github.com/GLips/Figma-Context-MCP) Give Cursor and other AI-powered coding tools access to your Figma files with this Model Context Protocol server.  
60 | + [Cursor Talk to Figma MCP](https://github.com/sonnylazuardi/cursor-talk-to-figma-mcp) Allowing Cursor to communicate with Figma for reading designs and modifying them programmatically.
61 | + [Figma MCP Server](https://github.com/MatthewDailey/figma-mcp) This server provides tools for viewing, commenting, and analyzing Figma designs directly through the ModelContextProtocol.
```

--------------------------------------------------------------------------------
/src/server/figma/helpers/index.ts:
--------------------------------------------------------------------------------

```typescript
1 | export * from './figma'
2 | export * from './downloader'
3 | 
```

--------------------------------------------------------------------------------
/glama.json:
--------------------------------------------------------------------------------

```json
1 | {
2 |   "$schema": "https://glama.ai/mcp/schemas/server.json",
3 |   "maintainers": ["ckken"]
4 | }
5 | 
```

--------------------------------------------------------------------------------
/src/stdio.ts:
--------------------------------------------------------------------------------

```typescript
1 | import {server} from 'src/server/figma'
2 | import {startServer} from 'src/transports/stdio'
3 | startServer(server)
4 | 
```

--------------------------------------------------------------------------------
/src/streamable-http.ts:
--------------------------------------------------------------------------------

```typescript
1 | import {startServer} from '@/transports/streamable-http'
2 | import {server} from 'src/server/figma'
3 | startServer(server, 3000)
4 | 
```

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

```typescript
1 | #!/usr/bin/env node
2 | 
3 | import {server} from 'src/server/figma'
4 | import {startServer} from 'src/transports/stdio'
5 | startServer(server)
6 | 
```

--------------------------------------------------------------------------------
/src/server/figma/notifications/logger.ts:
--------------------------------------------------------------------------------

```typescript
1 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
2 | 
3 | export const registerLoggerNotificatons = (server: McpServer) => {}
4 | 
```

--------------------------------------------------------------------------------
/mcp.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "mcpServers": {
 3 |     "f2c-mcp": {
 4 |       "command": "npx",
 5 |       "args": ["-y", "f2c-mcp"],
 6 |       "env": {
 7 |         "personalToken": ""
 8 |       }
 9 |     }
10 |   }
11 | }
12 | 
```

--------------------------------------------------------------------------------
/docs/en/FAQ.md:
--------------------------------------------------------------------------------

```markdown
 1 | # FAQ
 2 |  ```
 3 | Error: spawn npx ENOENT
 4 | ```
 5 | Solutions: Add PATH to mcpServer
 6 | ```
 7 | {
 8 |   "env": {
 9 |     "PATH": "/Users/xxx/.nvm/versions/node/v20.10.0/bin:/bin"
10 |   }
11 | }
12 | ```
```

--------------------------------------------------------------------------------
/src/server/figma/notifications/index.ts:
--------------------------------------------------------------------------------

```typescript
1 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
2 | import {registerLoggerNotificatons} from './logger'
3 | 
4 | export const registerNotificatons = (server: McpServer) => {
5 |   registerLoggerNotificatons(server)
6 | }
7 | 
```

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

```dockerfile
 1 | FROM oven/bun:latest
 2 | 
 3 | WORKDIR /app
 4 | 
 5 | # Copy package files
 6 | COPY package*.json ./
 7 | 
 8 | COPY bun.lock ./
 9 | # Install dependencies
10 | RUN bun install
11 | 
12 | # Copy application code
13 | COPY . .
14 | 
15 | # Build the application
16 | RUN bun run build
17 | 
18 | # Command will be provided by smithery.yaml
19 | CMD ["node", "dist/stdio.js"]
20 | 
```

--------------------------------------------------------------------------------
/bun.build.script.ts:
--------------------------------------------------------------------------------

```typescript
 1 | const script = process.env.npm_lifecycle_script || ''
 2 | const isDev = script.includes('--watch')
 3 | 
 4 | export const result = await Bun.build({
 5 |   entrypoints: ['src/stdio.ts', 'src/cli.ts', 'src/streamable-http.ts'],
 6 |   outdir: 'dist',
 7 |   format: 'cjs',
 8 |   target: 'node',
 9 |   sourcemap: 'linked',
10 |   minify: !isDev,
11 |   env: isDev ? 'inline' : 'disable',
12 | })
13 | 
```

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

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/build/project-config
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     properties: {}
 9 |   exampleConfig: {}
10 |   commandFunction:
11 |     # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
12 |     |-
13 |     (config) => ({ command: 'node', args: ['./dist/stdio.js'] })
14 | 
```

--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "biome.enabled": true,
 3 |   "eslint.enable": false,
 4 |   "prettier.enable": false,
 5 |   "editor.codeActionsOnSave": {
 6 |     "quickfix.biome": "explicit",
 7 |     "source.organizeImports.biome": "explicit"
 8 |   },
 9 |   "typescript.tsdk": "node_modules/typescript/lib",
10 |   "typescript.enablePromptUseWorkspaceTsdk": true,
11 |   "editor.defaultFormatter": "biomejs.biome",
12 |   "[typescript]": {
13 |     "editor.defaultFormatter": "biomejs.biome"
14 |   },
15 |   "[json]": {
16 |     "editor.defaultFormatter": "biomejs.biome"
17 |   }
18 | }
19 | 
```

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

```typescript
 1 | export interface GetFileParams {
 2 |   fileKey: string
 3 |   ids?: string
 4 |   version?: string
 5 |   depth?: number
 6 |   geometry?: 'paths'
 7 |   plugin_data?: string
 8 |   branch_data?: boolean
 9 |   personalToken?: string
10 | }
11 | 
12 | export interface GetImagesParams {
13 |   fileKey: string
14 |   ids: string
15 |   scale?: number
16 |   format?: 'jpg' | 'png' | 'svg' | 'pdf'
17 |   svg_include_id?: boolean
18 |   svg_simplify_stroke?: boolean
19 |   use_absolute_bounds?: boolean
20 |   version?: string
21 |   personalToken?: string
22 | }
23 | 
24 | export interface GetKeyParams {
25 |   fileKey: string
26 |   personalToken?: string
27 | }
28 | 
```

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

```typescript
 1 | export interface NodeToCodeWithF2C {
 2 |   personal_token: string
 3 |   format: string
 4 |   nodeIds: string
 5 |   fileKey: string
 6 | }
 7 | export interface NodeToCodeWithF2COptions {
 8 |   personalToken?: string
 9 |   localPath?: string
10 |   format?: string
11 |   ids: string
12 |   fileKey: string
13 |   imgFormat: 'png' | 'jpg' | 'svg'
14 |   scaleSize: number
15 |   ideInfo?: string
16 | }
17 | 
18 | export interface NodeToCodeAllFiles {
19 |   files: NodeToCodeFile[]
20 |   images: {
21 |     [key: string]: {id: string; name: string; fileExt: 'png' | 'jpg' | 'svg'; nodeType: string}
22 |   }
23 | }
24 | export interface NodeToCodeFile {
25 |   content: string
26 |   path: string
27 | }
28 | 
```

--------------------------------------------------------------------------------
/user_rules.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "outputRules": {
 3 |     "defaultOutputPath": ".demo",
 4 |     "fileTypes": {
 5 |       "html": {
 6 |         "path": ".demo",
 7 |         "extension": ".html"
 8 |       },
 9 |       "css": {
10 |         "path": ".demo",
11 |         "extension": ".css"
12 |       },
13 |       "assets": {
14 |         "path": ".demo",
15 |         "allowedTypes": ["png", "jpg", "svg", "gif"]
16 |       }
17 |     },
18 |     "rules": {
19 |       "createDirectory": true,
20 |       "overwrite": true,
21 |       "separateStyles": true,
22 |       "assetHandling": "copy",
23 |       "naming": {
24 |         "pattern": "${filename}-${timestamp}",
25 |         "lowercase": true
26 |       }
27 |     }
28 |   }
29 | }
30 | 
```

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

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

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

```typescript
 1 | import {getArgValue} from '@/utils/index'
 2 | import {LogLevel, createLogger} from '@/utils/logger'
 3 | 
 4 | const logger = createLogger('FigmaConfig', LogLevel.INFO)
 5 | 
 6 | class FigmaConfig {
 7 |   public serverName = 'F2C MCP'
 8 |   public serverVersion = process.env.FIGMA_VERSION || '0.0.1'
 9 |   private _personalToken = getArgValue('figma-api-key') || process.env.FIGMA_API_KEY || process.env.personalToken || ''
10 |   public get personalToken() {
11 |     return this._personalToken
12 |   }
13 |   public set personalToken(token: string) {
14 |     this._personalToken = token
15 |     logger.debug('personalToken', token)
16 |   }
17 | }
18 | 
19 | export default new FigmaConfig()
20 | 
```

--------------------------------------------------------------------------------
/src/server/figma/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
 2 | import config from 'src/server/figma/config'
 3 | import {registerNotificatons} from './notifications'
 4 | // import {registerF2cServer} from 'src/server/figma/tools/f2c'
 5 | // import {registerFigmaServer} from 'src/server/figma/tools/figma'
 6 | import {registerV03Server} from './tools/v03'
 7 | 
 8 | export const server = new McpServer(
 9 |   {
10 |     name: config.serverName,
11 |     version: config.serverVersion,
12 |   },
13 |   {
14 |     capabilities: {
15 |       logging: {},
16 |     },
17 |   },
18 | )
19 | 
20 | registerNotificatons(server)
21 | // registerFigmaServer(server)
22 | // registerF2cServer(server)
23 | registerV03Server(server)
24 | 
```

--------------------------------------------------------------------------------
/src/transports/stdio.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
 2 | import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'
 3 | import {createLogger} from 'src/utils/logger'
 4 | 
 5 | const logger = createLogger('StdioTransport')
 6 | 
 7 | export async function startServer(server: McpServer) {
 8 |   try {
 9 |     const transport = new StdioServerTransport()
10 |     await server.connect(transport)
11 |   } catch (e: any) {
12 |     logger.info(
13 |       JSON.stringify({
14 |         jsonrpc: '2.0',
15 |         id: null,
16 |         error: {
17 |           code: -32000,
18 |           message: `Server startup failed: ${e.message}`,
19 |         },
20 |       }),
21 |     )
22 |     process.exit(1)
23 |   }
24 | }
25 | 
```

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

```typescript
 1 | /**
 2 |  * Get the value of a specified parameter from command line arguments
 3 |  * Supports --param=value format
 4 |  * @param {string} paramName - Parameter name (without -- prefix)
 5 |  * @param {string|undefined} defaultValue - Default value, returned if parameter not found
 6 |  * @returns {string|undefined} Parameter value or default value
 7 |  */
 8 | export function getArgValue(paramName: string, defaultValue?: string): string | undefined {
 9 |   const args = process.argv
10 |   const paramPrefix = `--${paramName}=`
11 | 
12 |   for (const arg of args) {
13 |     // Check if argument starts with specified prefix
14 |     if (arg.startsWith(paramPrefix)) {
15 |       return arg.substring(paramPrefix.length)
16 |     }
17 |   }
18 | 
19 |   // If parameter not found, return default value
20 |   return defaultValue
21 | }
22 | 
```

--------------------------------------------------------------------------------
/src/server/figma/helpers/figma.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // Enhanced Figma URL parser supporting multiple formats
 2 | export function parseFigmaUrl(url: string) {
 3 |   try {
 4 |     const urlObj = new URL(url)
 5 |     const path = urlObj.pathname
 6 | 
 7 |     // Support both file/xxx and design/xxx formats
 8 |     const [, fileKey] = path.match(/(?:file|design)\/([^/]+)/) || []
 9 | 
10 |     // Support node-id parameter and hash format
11 |     const nodeIdMatch =
12 |       urlObj.searchParams.get('node-id') || url.match(/node-id=([^&]+)/) || url.match(/#([^:]+:[^:]+)/)
13 | 
14 |     const nodeId = nodeIdMatch ? (Array.isArray(nodeIdMatch) ? nodeIdMatch[1] : nodeIdMatch) : ''
15 | 
16 |     if (!fileKey) {
17 |       throw new Error('Invalid Figma link: fileKey not found')
18 |     }
19 | 
20 |     return {
21 |       fileKey,
22 |       nodeId: nodeId || '',
23 |     }
24 |   } catch (error) {
25 |     throw new Error('Invalid Figma link')
26 |   }
27 | }
28 | 
```

--------------------------------------------------------------------------------
/src/transports/streamable-http/index.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import {createLogger} from '@/utils/logger'
 2 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
 3 | const logger = createLogger('StreamableHttp')
 4 | type ServerTypeIns = 'all' | 'session' | 'no_session'
 5 | export async function startServer(server: McpServer, port = 3000, serverType: ServerTypeIns = 'all') {
 6 |   switch (serverType) {
 7 |     // don't support hot reload
 8 |     case 'all': {
 9 |       const {startHttpServer} = await import('./http-server.js')
10 |       return startHttpServer(port, server)
11 |     }
12 |     // don't support hot reload
13 |     case 'session': {
14 |       const {startServer: startWithSessionServer} = await import('./with-session-steamable-http.js')
15 |       return startWithSessionServer(server, port)
16 |     }
17 |     // support hot reload and dev
18 |     case 'no_session': {
19 |       const {startServer: startWithoutSessionServer} = await import('./without-session-steamable-http.js')
20 |       return startWithoutSessionServer(server, port)
21 |     }
22 |   }
23 | }
24 | 
```

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

```typescript
 1 | export enum LogLevel {
 2 |   DEBUG = 0,
 3 |   INFO = 1,
 4 |   WARN = 2,
 5 |   ERROR = 3,
 6 | }
 7 | // Determine if stdio or http mode by checking the run command
 8 | function detectTransportMode(): boolean {
 9 |   const args = process.argv.join(' ')
10 |   // If command line includes streamable-http.js or streamable-http.ts, it's HTTP mode
11 |   return args.includes('streamable-http.js') || args.includes('streamable-http.ts')
12 | }
13 | export const isHttp = detectTransportMode()
14 | export class Logger {
15 |   private context: string
16 |   private level: LogLevel
17 |   constructor(context: string, level: LogLevel = LogLevel.INFO) {
18 |     this.context = context
19 |     this.level = level
20 |   }
21 | 
22 |   setLevel(level: LogLevel): void {
23 |     this.level = level
24 |   }
25 |   log(...args: any[]): void {
26 |     if (isHttp) {
27 |       console.log(...args)
28 |     } else {
29 |       console.error(...args)
30 |     }
31 |   }
32 |   logWarn(...args: any[]): void {
33 |     if (isHttp) {
34 |       console.warn(...args)
35 |     } else {
36 |       console.error(...args)
37 |     }
38 |   }
39 |   debug(...args: any[]): void {
40 |     if (this.level <= LogLevel.DEBUG) {
41 |       this.log(`[DEBUG] [${this.context}]`, ...args) // 使用 console.error 而不是 console.log
42 |     }
43 |   }
44 | 
45 |   info(...args: any[]): void {
46 |     if (this.level <= LogLevel.INFO) {
47 |       this.log(`[INFO] [${this.context}]`, ...args)
48 |     }
49 |   }
50 | 
51 |   warn(...args: any[]): void {
52 |     if (this.level <= LogLevel.WARN) {
53 |       this.logWarn(`[WARN] [${this.context}]`, ...args)
54 |     }
55 |   }
56 | 
57 |   error(...args: any[]): void {
58 |     if (this.level <= LogLevel.ERROR) {
59 |       console.error(`[ERROR] [${this.context}]`, ...args)
60 |     }
61 |   }
62 | }
63 | 
64 | // Create default logger instance
65 | export const createLogger = (context: string, level: LogLevel = LogLevel.INFO) => new Logger(context, level)
66 | 
```

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

```json
 1 | {
 2 |   "name": "@f2c/mcp",
 3 |   "version": "0.4.9",
 4 |   "description": "",
 5 |   "repository": {
 6 |     "type": "git",
 7 |     "url": "git+https://github.com/f2c-ai/f2c-mcp",
 8 |     "directory": "."
 9 |   },
10 |   "publishConfig": {
11 |     "access": "public"
12 |   },
13 |   "keywords": ["f2c", "mcp"],
14 |   "files": ["dist"],
15 |   "main": "dist/stdio.js",
16 |   "types": "dist/stdio.d.ts",
17 |   "exports": {
18 |     ".": {
19 |       "import": {
20 |         "types": "./dist/stdio.d.mts",
21 |         "default": "./dist/stdio.mjs"
22 |       },
23 |       "require": {
24 |         "types": "./dist/stdio.d.ts",
25 |         "default": "./dist/stdio.js"
26 |       }
27 |     },
28 |     "./streamable-http": {
29 |       "import": {
30 |         "types": "./dist/streamable-http.d.mts",
31 |         "default": "./dist/streamable-http.mjs"
32 |       },
33 |       "require": {
34 |         "types": "./dist/streamable-http.d.ts",
35 |         "default": "./dist/streamable-http.js"
36 |       }
37 |     }
38 |   },
39 |   "bin": {
40 |     "f2c-mcp": "dist/cli.js"
41 |   },
42 |   "scripts": {
43 |     "build": "bun run bun.build.script.ts",
44 |     "dev": "bun --watch run bun.build.script.ts",
45 |     "http": "node ./dist/streamable-http.js",
46 |     "http:dev": "bun --env-file=.env --watch run src/streamable-http.ts",
47 |     "http:prod": "bun --env-file= run src/streamable-http.ts",
48 |     "inspector": "npx @modelcontextprotocol/inspector node ./dist/stdio.js",
49 |     "lint": "biome check . --fix",
50 |     "test": "bun test src/test/api.test.ts",
51 |     "e2e": "bun test src/test/e2e.test.ts"
52 |   },
53 |   "author": "ckken",
54 |   "maintainers": ["ckken"],
55 |   "license": "ISC",
56 |   "dependencies": {
57 |     "cors": "^2.8.5",
58 |     "express": "^5.1.0",
59 |     "node-fetch": "^3.3.2",
60 |     "zod": "^3.22.4",
61 |     "@f2c/data-reporter": "^0.0.20"
62 |   },
63 |   "devDependencies": {
64 |     "@biomejs/biome": "^1.9.4",
65 |     "@empjs/biome-config": "^0.7.2",
66 |     "@modelcontextprotocol/sdk": "1.17.2",
67 |     "@types/bun": "^1.2.13",
68 |     "@types/cors": "^2.8.18",
69 |     "@types/express": "^5.0.1",
70 |     "@types/node": "^22.15.0",
71 |     "@types/node-fetch": "^2.6.12",
72 |     "typescript": "^5.8.3"
73 |   },
74 |   "engines": {
75 |     "node": ">=16.0.0"
76 |   }
77 | }
78 | 
```

--------------------------------------------------------------------------------
/src/server/figma/apis/figma.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import type {GetFileParams, GetImagesParams, GetKeyParams} from '@/server/figma/types/figma'
 2 | import config from 'src/server/figma/config'
 3 | import compatFetch from 'src/utils/fetch'
 4 | import {createLogger} from 'src/utils/logger'
 5 | 
 6 | const logger = createLogger('FigmaRestApi')
 7 | class FigmaRestApi {
 8 |   protected figmaHost = `https://api.figma.com/v1`
 9 |   async files(o: GetFileParams) {
10 |     let url: string
11 |     if (o.ids) {
12 |       url = this.opToUrl(`${this.figmaHost}/files/${o.fileKey}/nodes`, o)
13 |     } else {
14 |       url = this.opToUrl(`${this.figmaHost}/files/${o.fileKey}`, o)
15 |     }
16 | 
17 |     return this.fetch(url)
18 |   }
19 |   async images(o: GetImagesParams) {
20 |     const url = this.opToUrl(`${this.figmaHost}/images/${o.fileKey}`, o)
21 |     return this.fetch(url)
22 |   }
23 |   // Returns download links for all images present in image fills
24 |   async imageFills(o: GetKeyParams) {
25 |     const url = this.opToUrl(`${this.figmaHost}/files/${o.fileKey}/images`, o)
26 |     return this.fetch(url)
27 |   }
28 |   // Returns the metadata for the file referred to by :key
29 |   async meta(o: GetKeyParams) {
30 |     const url = this.opToUrl(`${this.figmaHost}/files/${o.fileKey}/meta`, o)
31 |     return this.fetch(url)
32 |   }
33 |   async fetch(url: string, resType: 'json' | 'text' = 'json'): Promise<any> {
34 |     try {
35 |       const fetchOptions = {
36 |         method: 'GET',
37 |         headers: {
38 |           'X-FIGMA-TOKEN': config.personalToken,
39 |         },
40 |       }
41 |       const response = await compatFetch(url, fetchOptions)
42 |       // logger.debug('response', url, JSON.stringify(fetchOptions))
43 |       if (!response.ok) {
44 |         throw new Error(`HTTP error! status: ${response.status}`)
45 |       }
46 |       const data = resType === 'text' ? await response.text() : await response.json()
47 |       return data
48 |     } catch (error) {
49 |       logger.error('HTTP error', error)
50 |       throw error
51 |     }
52 |   }
53 |   private opToUrl(api: string, o: any = {}, filters = ['fileKey', 'personalToken']) {
54 |     if (Object.keys(o).length === 0) {
55 |       return api
56 |     }
57 |     if (o.personalToken) {
58 |       config.personalToken = o.personalToken
59 |     }
60 |     const url: any = new URL(api)
61 |     for (const [key, value] of Object.entries(o)) {
62 |       if (!filters.includes(key)) url.searchParams.append(key, value)
63 |     }
64 |     return url.toString()
65 |   }
66 | }
67 | export default new FigmaRestApi()
68 | 
```

--------------------------------------------------------------------------------
/src/server/figma/apis/f2c.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import config from 'src/server/figma/config'
 2 | import type {NodeToCodeAllFiles, NodeToCodeFile, NodeToCodeWithF2COptions} from 'src/server/figma/types/f2c'
 3 | import compatFetch from 'src/utils/fetch'
 4 | import {LogLevel, createLogger} from 'src/utils/logger'
 5 | 
 6 | const logger = createLogger('F2cApi', LogLevel.INFO)
 7 | class F2cApi {
 8 |   protected f2cHost = `https://f2c-figma-api.yy.com/api`
 9 |   //
10 |   async nodeToCode(o: NodeToCodeWithF2COptions): Promise<NodeToCodeFile[]> {
11 |     const op = {
12 |       fileKey: o.fileKey,
13 |       nodeIds: o.ids,
14 |       personal_token: o.personalToken || config.personalToken,
15 |       option: {
16 |         cssFramework: 'inlinecss',
17 |         imgFormat: o.imgFormat || 'png',
18 |         scaleSize: o.scaleSize || 2,
19 |       },
20 |       format: 'files',
21 |       // format: 'allFiles',
22 |     }
23 |     if (o.format === 'react-cssmodules') {
24 |       op.option.cssFramework = 'cssmodules'
25 |     } else if (o.format === 'react-tailwind') {
26 |       op.option.cssFramework = 'tailwindcss'
27 |     }
28 |     const url = this.opToUrl(`${this.f2cHost}/nodes`, op)
29 |     return this.fetch(url, 'json', o.ideInfo || 'other')
30 |   }
31 |   async fetch(url: string, resType: 'json' | 'text' = 'json', ideInfo: string): Promise<any> {
32 |     logger.debug('fetch', url, config.personalToken)
33 |     try {
34 |       const fetchOptions = {
35 |         method: 'GET',
36 |         headers: {
37 |           'F2c-Api-Platform': `mcp-${ideInfo}`,
38 | 
39 |         },
40 |       }
41 |       logger.debug('fetch', url)
42 |       const response = await compatFetch(url, fetchOptions)
43 |       if (!response.ok) {
44 |         throw new Error(`HTTP error! status: ${response.status}`)
45 |       }
46 |       const data = resType === 'text' ? await response.text() : await response.json()
47 |       return data
48 |     } catch (error) {
49 |       logger.error('HTTP error', error)
50 |       throw error
51 |     }
52 |   }
53 |   private opToUrl(api: string, o: any = {}) {
54 |     if (Object.keys(o).length === 0) {
55 |       return api
56 |     }
57 |     const url: any = new URL(api)
58 |     for (const [key, value] of Object.entries(o)) {
59 |       if (typeof value === 'object' && value !== null) {
60 |         for (const [nestedKey, nestedValue] of Object.entries(value)) {
61 |           url.searchParams.append(`${key}[${nestedKey}]`, nestedValue as string)
62 |         }
63 |       } else {
64 |         url.searchParams.append(key, value as string)
65 |       }
66 |     }
67 |     return url.toString()
68 |   }
69 | }
70 | export default new F2cApi()
71 | 
```

--------------------------------------------------------------------------------
/src/transports/streamable-http/with-session-steamable-http.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import {randomUUID} from 'node:crypto'
 2 | import {createLogger} from '@/utils/logger'
 3 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
 4 | import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js'
 5 | import {isInitializeRequest} from '@modelcontextprotocol/sdk/types.js'
 6 | import express from 'express'
 7 | 
 8 | const logger = createLogger('SessionStreamableHttp')
 9 | 
10 | const app = express()
11 | app.use(express.json())
12 | export const startServer = (server: McpServer, port = 3000) => {
13 |   const transports: {[sessionId: string]: StreamableHTTPServerTransport} = {}
14 |   app.post('/mcp', async (req, res) => {
15 |     // let acceptHeader = req.headers.accept as string
16 |     // if (acceptHeader === '*/*') {
17 |     //   acceptHeader = '*/*,application/json, text/event-stream'
18 |     //   req.headers.accept = acceptHeader
19 |     // }
20 |     res.setHeader('Content-Type', 'application/json')
21 |     const sessionId = req.headers['mcp-session-id'] as string | undefined
22 |     let transport: StreamableHTTPServerTransport
23 |     if (sessionId && transports[sessionId]) {
24 |       transport = transports[sessionId]
25 |     } else if (!sessionId && isInitializeRequest(req.body)) {
26 |       transport = new StreamableHTTPServerTransport({
27 |         sessionIdGenerator: () => randomUUID(),
28 |         enableJsonResponse: true,
29 |         onsessioninitialized: sessionId => {
30 |           transports[sessionId] = transport
31 |         },
32 |       })
33 |       transport.onclose = () => {
34 |         if (transport.sessionId) {
35 |           delete transports[transport.sessionId]
36 |         }
37 |       }
38 |       await server.connect(transport)
39 |     } else {
40 |       res.status(400).json({
41 |         jsonrpc: '2.0',
42 |         error: {
43 |           code: -32000,
44 |           message: 'Bad Request: No valid session ID provided',
45 |         },
46 |         id: null,
47 |       })
48 |       return
49 |     }
50 |     await transport.handleRequest(req, res, req.body)
51 |   })
52 |   const handleSessionRequest = async (req: express.Request, res: express.Response) => {
53 |     const sessionId = req.headers['mcp-session-id'] as string | undefined
54 |     if (!sessionId || !transports[sessionId]) {
55 |       res.status(400).send('Invalid or missing session ID')
56 |       return
57 |     }
58 |     const transport = transports[sessionId]
59 |     await transport.handleRequest(req, res)
60 |   }
61 |   app.get('/mcp', handleSessionRequest)
62 |   app.delete('/mcp', handleSessionRequest)
63 |   app.listen(port, () => {
64 |     logger.info(`MCP Session-based Streamable HTTP Server listening on port ${port}`)
65 |     logger.info(`Server address: http://localhost:${port}/mcp`)
66 |   })
67 | }
68 | 
```

--------------------------------------------------------------------------------
/src/transports/streamable-http/without-session-steamable-http.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import {createLogger} from '@/utils/logger'
 2 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
 3 | import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js'
 4 | import express from 'express'
 5 | 
 6 | const logger = createLogger('StatelessStreamableHttp')
 7 | 
 8 | const app = express()
 9 | app.use(
10 |   express.json({
11 |     type: ['application/json', 'application/*+json', '*/*'], // 扩展支持的 Content-Type
12 |   }),
13 | )
14 | const noAllowAcess = (req: any, res: any, next: any) => {
15 |   return res.writeHead(405).end(
16 |     JSON.stringify({
17 |       jsonrpc: '2.0',
18 |       error: {
19 |         code: -32000,
20 |         message: 'Method not allowed.',
21 |       },
22 |       id: null,
23 |     }),
24 |   )
25 | }
26 | // const polyfillRequest = (req: any, res: any) => {
27 | //   // 设置响应头
28 | //   res.setHeader('Content-Type', 'application/json')
29 | //   res.setHeader('Connection', 'keep-alive')
30 | //   res.setHeader('Keep-Alive', 'timeout=5')
31 | 
32 | //   // let acceptHeader = req.headers.accept as string
33 | //   // if (acceptHeader === '*/*') {
34 | //   //   acceptHeader = '*/*,application/json, text/event-stream'
35 | //   //   req.headers.accept = acceptHeader
36 | //   // }
37 | 
38 | //   // 确保请求的 Content-Type 存在
39 | //   if (!req.headers['content-type']) {
40 | //     req.headers['content-type'] = 'application/json'
41 | //   }
42 | // }
43 | export const startServer = (server: McpServer, port = 3000) => {
44 |   app.post('/mcp', async (req, res) => {
45 |     res.setHeader('Content-Type', 'application/json')
46 |     logger.info('Request body:', JSON.stringify(req.body))
47 |     // polyfillRequest(req, res)
48 | 
49 |     try {
50 |       const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
51 |         sessionIdGenerator: undefined,
52 |         enableJsonResponse: true,
53 |       })
54 | 
55 |       res.on('close', () => {
56 |         transport.close()
57 |         server.close()
58 |       })
59 | 
60 |       await server.connect(transport)
61 |       await transport.handleRequest(req, res, req.body)
62 |     } catch (error: any) {
63 |       logger.error('Error handling MCP request:', error)
64 |       logger.error('Error stack:', error.stack)
65 |       if (!res.headersSent) {
66 |         res.status(500).json({
67 |           jsonrpc: '2.0',
68 |           error: {
69 |             code: -32603,
70 |             message: 'Internal server error',
71 |             data: {
72 |               errorMessage: error.message,
73 |               errorName: error.name,
74 |             },
75 |           },
76 |           id: req.body?.id || null,
77 |         })
78 |       }
79 |     }
80 |   })
81 | 
82 |   app.get('/mcp', noAllowAcess)
83 |   app.delete('/mcp', noAllowAcess)
84 | 
85 |   app.listen(port, () => {
86 |     logger.info(`MCP Stateless Streamable HTTP server started, listening on port ${port}`)
87 |     logger.info(`Server address: http://localhost:${port}/mcp`)
88 |   })
89 | }
90 | 
```

--------------------------------------------------------------------------------
/docs/en/GettingStarted.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Getting Started
  2 | Usually, code editors and other AI clients use a configuration file to manage MCP servers.
  3 | 
  4 | You can add the following content to the configuration file to set up the `f2c-mcp` server.
  5 | 
  6 | > NOTE: You will need to create a Figma access token to use this server. Instructions on how to create a Figma API access token can be found [here](https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens).
  7 | 
  8 | ## No-Installation MCP Configuration (stdio)
  9 | 
 10 | ### MacOS / Linux
 11 | [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=f2c-mcp&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMm5weCUyMC15JTIwJTQwZjJjJTJGbWNwJTIyJTJDJTIyZW52JTIyJTNBJTdCJTIycGVyc29uYWxUb2tlbiUyMiUzQSUyMiUyMiU3RCU3RA%3D%3D)
 12 | ```json
 13 | {
 14 |   "mcpServers": {
 15 |     "f2c-mcp": {
 16 |       "command": "npx",
 17 |       "args": [
 18 |         "-y",
 19 |         "@f2c/mcp"
 20 |       ],
 21 |       "env": {
 22 |         "personalToken": ""
 23 |       }
 24 |     }
 25 |   }
 26 | }
 27 | ```
 28 | or
 29 | ```json
 30 | {
 31 |   "mcpServers": {
 32 |     "f2c-mcp": {
 33 |       "command": "npx",
 34 |       "args": [
 35 |         "-y",
 36 |         "@f2c/mcp",
 37 |         "--figma-api-key=YOUR-KEY"
 38 |       ],
 39 |     }
 40 |   }
 41 | }
 42 | ```
 43 | 
 44 | ### Windows (stdio)
 45 | [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=f2c-mcp&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMmNtZCUyMCUyRmMlMjBucHglMjAteSUyMCU0MGYyYyUyRm1jcCUyMiUyQyUyMmVudiUyMiUzQSU3QiUyMnBlcnNvbmFsVG9rZW4lMjIlM0ElMjIlMjIlN0QlN0Q%3D)
 46 | ```json
 47 | {
 48 |   "mcpServers": {
 49 |     "f2c-mcp": {
 50 |       "command": "cmd",
 51 |       "args": ["/c", "npx", "-y", "@f2c/mcp"],
 52 |       "env": {
 53 |         "personalToken": ""
 54 |       }
 55 |     }
 56 |   }
 57 | }
 58 | ```
 59 | or 
 60 | ```json
 61 | {
 62 |   "mcpServers": {
 63 |     "f2c-mcp": {
 64 |       "command": "cmd",
 65 |       "args": ["/c", "npx", "-y", "@f2c/mcp", "--figma-api-key=YOUR-KEY"],
 66 |     }
 67 |   }
 68 | }
 69 | ```
 70 | 
 71 | ## Global Installation MCP Configuration (stdio)
 72 | For cases where MCP client instability causes installation errors, we can use global installation and then configure it.
 73 | 
 74 | ```bash
 75 | npm install -g @f2c/mcp
 76 | ```
 77 | [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=f2c-mcp&config=JTdCJTIyY29tbWFuZCUyMiUzQSUyMmYyYy1tY3AlMjAlMjIlMkMlMjJlbnYlMjIlM0ElN0IlMjJwZXJzb25hbFRva2VuJTIyJTNBJTIyJTIyJTdEJTdE)
 78 | ```json
 79 | {
 80 |   "mcpServers": {
 81 |     "f2c-mcp": {
 82 |       "command": "f2c-mcp",
 83 |       "args": [],
 84 |       "env": {
 85 |         "personalToken": ""
 86 |       }
 87 |     }
 88 |   }
 89 | }
 90 | ```
 91 | 
 92 | ## Other Configuration Types
 93 | 
 94 | ### Add Streamable HTTP
 95 | [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=f2c_mcp&config=JTdCJTIydHJhbnNwb3J0JTIyJTNBJTIyc3RyZWFtYWJsZV9odHRwJTIyJTJDJTIydXJsJTIyJTNBJTIyaHR0cCUzQSUyRiUyRmxvY2FsaG9zdCUzQTMwMDAlMkZtY3AlMjIlMkMlMjJoZWFkZXJzJTIyJTNBJTdCJTdEJTJDJTIydGltZW91dCUyMiUzQTUwJTdE)
 96 | ```json
 97 | {
 98 |   "mcpServers": {
 99 |       "f2c_mcp": {
100 |         "transport": "streamableHttp",
101 |         "url": "http://localhost:3000/mcp",
102 |         "headers": {
103 |           "personalToken": ""
104 |         },
105 |         "timeout": 50
106 |       }
107 |     }
108 | }
109 | ```
110 | 
111 | ### Add SSE
112 | [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=f2c_mcp&config=JTdCJTIydHJhbnNwb3J0JTIyJTNBJTIyc3NlJTIyJTJDJTIydXJsJTIyJTNBJTIyaHR0cCUzQSUyRiUyRmxvY2FsaG9zdCUzQTMwMDAlMkZzc2UlMjIlMkMlMjJoZWFkZXJzJTIyJTNBJTdCJTdEJTJDJTIydGltZW91dCUyMiUzQTUwJTdE)
113 | ```json
114 | {
115 |   "mcpServers": {
116 |       "f2c_mcp": {
117 |         "transport": "sse",
118 |         "url": "http://localhost:3000/sse",
119 |         "headers": {
120 |           "personalToken": ""
121 |         },
122 |         "timeout": 50
123 |       }
124 |     }
125 | }
126 | ```
127 | 
128 | ## Development
129 | 
130 | ### 1. Set up your Figma API key in `.env` file:
131 | ```bash
132 | FIGMA_API_KEY=your_api_key_here
133 | ```
134 | 
135 | ### 2. Install dependencies:
136 | ```bash
137 | bun install
138 |  ```
139 | 
140 | ### 3. Start development server:
141 | ### stdio dev server
142 | ```bash
143 | bun run dev
144 |  ```
145 | ### streamable_http and SSE dev server
146 | ```bash
147 | bun run http:dev
148 |  ```
149 | 
150 | ## Install Smithery
151 | 
152 | To install F2C MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@f2c-ai/f2c-mcp):
153 | 
154 | ```bash
155 | npx -y @smithery/cli install @f2c-ai/f2c-mcp --client claude
156 | ```
```

--------------------------------------------------------------------------------
/src/server/figma/tools/figma.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import api from '@/server/figma/apis/figma'
  2 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
  3 | import type {CallToolResult} from '@modelcontextprotocol/sdk/types.js'
  4 | import {z} from 'zod'
  5 | 
  6 | export const registerFigmaServer = (server: McpServer) => {
  7 |   // Get Figma file information
  8 |   server.tool(
  9 |     'figma_get_file_data',
 10 |     'Get detailed information about a Figma file',
 11 |     {
 12 |       fileKey: z.string().describe('Unique identifier of the Figma file'),
 13 |       ids: z.string().describe('List of node IDs to retrieve, comma separated'),
 14 |       personalToken: z
 15 |         .string()
 16 |         .optional()
 17 |         .describe('Your Figma personal access token, The parameters are not required when the tool is called.'),
 18 |       version: z.string().optional().describe('Specify the version to return'),
 19 |       depth: z.number().optional().describe('Specify the depth of nodes to return'),
 20 |       geometry: z.enum(['paths']).optional().describe('Specify whether to include geometry path data'),
 21 |       plugin_data: z.string().optional().describe('Specify plugin data to return'),
 22 |       branch_data: z.boolean().optional().describe('Specify whether to return branch data'),
 23 |     },
 24 |     async (o): Promise<CallToolResult> => {
 25 |       try {
 26 |         const data = await api.files(o)
 27 |         return {
 28 |           content: [{type: 'text', text: JSON.stringify(data)}],
 29 |         }
 30 |       } catch (error: any) {
 31 |         return {
 32 |           content: [{type: 'text', text: `Error: ${error.message}`}],
 33 |         }
 34 |       }
 35 |     },
 36 |   )
 37 | 
 38 |   // Get Figma node images
 39 |   server.tool(
 40 |     'figma_get_images',
 41 |     'Get images of Figma nodes',
 42 |     {
 43 |       fileKey: z.string().describe('Unique identifier of the Figma file'),
 44 |       ids: z.string().describe('Node IDs to get images for, comma separated'),
 45 |       format: z.enum(['jpg', 'png', 'svg', 'pdf']).optional().describe('Image format, e.g., png, jpg, svg'),
 46 |       scale: z.number().optional().describe('Image scale factor'),
 47 |       svg_include_id: z.boolean().optional().describe('Whether SVG includes ID'),
 48 |       svg_simplify_stroke: z.boolean().optional().describe('Whether to simplify SVG strokes'),
 49 |       use_absolute_bounds: z.boolean().optional().describe('Whether to use absolute bounds'),
 50 |       version: z.string().optional().describe('Specify the version to return'),
 51 |       personalToken: z
 52 |         .string()
 53 |         .optional()
 54 |         .describe('Your Figma personal access token, The parameters are not required when the tool is called.'),
 55 |     },
 56 |     async (o): Promise<CallToolResult> => {
 57 |       try {
 58 |         const data = await api.images(o)
 59 | 
 60 |         return {
 61 |           content: [{type: 'text', text: JSON.stringify(data)}],
 62 |         }
 63 |       } catch (error: any) {
 64 |         return {
 65 |           content: [{type: 'text', text: `Error: ${error.message}`}],
 66 |         }
 67 |       }
 68 |     },
 69 |   )
 70 | 
 71 |   // // Returns download links for all images present in image fills
 72 |   // server.tool(
 73 |   //   'figma_get_image_fills',
 74 |   //   'Get all image resources in the specified Figma file',
 75 |   //   {
 76 |   //     fileKey: z.string().describe('Unique identifier of the Figma file'),
 77 |   //     personalToken: z.string().optional().describe('Your Figma personal access token'),
 78 |   //   },
 79 |   //   async (o): Promise<CallToolResult> => {
 80 |   //     try {
 81 |   //       const data = await api.imageFills(o)
 82 | 
 83 |   //       return {
 84 |   //         content: [{type: 'text', text: JSON.stringify(data)}],
 85 |   //       }
 86 |   //     } catch (error: any) {
 87 |   //       return {
 88 |   //         content: [{type: 'text', text: `Error: ${error.message}`}],
 89 |   //       }
 90 |   //     }
 91 |   //   },
 92 |   // )
 93 | 
 94 |   // // Get Figma file metadata
 95 |   // server.tool(
 96 |   //   'figma_get_file_meta',
 97 |   //   'Get metadata information for a Figma file',
 98 |   //   {
 99 |   //     fileKey: z.string().describe('Unique identifier of the Figma file'),
100 |   //     personalToken: z.string().optional().describe('Your Figma personal access token'),
101 |   //   },
102 |   //   async (o): Promise<CallToolResult> => {
103 |   //     try {
104 |   //       const data = await api.meta(o)
105 |   //       return {
106 |   //         content: [{type: 'text', text: JSON.stringify(data)}],
107 |   //       }
108 |   //     } catch (error: any) {
109 |   //       return {
110 |   //         content: [{type: 'text', text: `Error: ${error.message}`}],
111 |   //       }
112 |   //     }
113 |   //   },
114 |   // )
115 | }
116 | 
```

--------------------------------------------------------------------------------
/src/server/figma/tools/f2c.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import api from '@/server/figma/apis/f2c'
  2 | import {createLogger} from '@/utils/logger'
  3 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
  4 | import type {CallToolResult} from '@modelcontextprotocol/sdk/types.js'
  5 | import type {NodeToCodeFile} from 'src/server/figma/types/f2c'
  6 | import {z} from 'zod'
  7 | import downloader from '../helpers/downloader'
  8 | 
  9 | const logger = createLogger('F2cTool')
 10 | 
 11 | export const registerF2cServer = (server: McpServer) => {
 12 |   // Register Figma to HTML conversion tool
 13 |   server.tool(
 14 |     'figma_to_code',
 15 |     'Transform Figma designs into production-ready code. This tool converts selected Figma nodes into HTML,enabling seamless design-to-code workflow.',
 16 |     {
 17 |       fileKey: z
 18 |         .string()
 19 |         .describe(
 20 |           'The Figma file identifier found in the file URL (e.g., https://www.figma.com/file/XXXXXXXXXXXX/). Extract the XXXXXXXXXXXX portion as the fileKey.',
 21 |         ),
 22 |       ids: z
 23 |         .string()
 24 |         .describe(
 25 |           'Comma-separated list of Figma node IDs for conversion. To obtain node IDs, select elements in Figma, right-click and select "Copy/Paste as" → "Copy ID".',
 26 |         ),
 27 |       format: z
 28 |         .enum(['html', 'react-cssmodules', 'react-tailwind'])
 29 |         .default('html')
 30 |         .describe(
 31 |           'Specify the output format: "html" generates semantic HTML/CSS, "react-cssmodules" creates React components with scoped CSS modules, "react-tailwind" produces React components with utility-first Tailwind classes.',
 32 |         ),
 33 |       personalToken: z
 34 |         .string()
 35 |         .optional()
 36 |         .describe(
 37 |           'Figma personal access token for API authentication.The parameters are not required when the tool is called.',
 38 |         ),
 39 |       localPath: z
 40 |         .string()
 41 |         .optional()
 42 |         .describe(
 43 |           'Absolute path for image asset storage. Directory will be created if non-existent. Path must follow OS-specific format without special character escaping. When set, all static resources will be saved to the images directory under this path.',
 44 |         ),
 45 |       imgFormat: z
 46 |         .enum(['png', 'jpg', 'svg'])
 47 |         .default('png')
 48 |         .describe(
 49 |           'Export format for image assets: "png" for lossless quality, "jpg" for compressed files, or "svg" for vector graphics.',
 50 |         ),
 51 |       scaleSize: z
 52 |         .number()
 53 |         .min(1)
 54 |         .max(4)
 55 |         .default(2)
 56 |         .describe(
 57 |           'Image export scale factor (1-4). Higher values yield better quality at the cost of larger file sizes.',
 58 |         ),
 59 |     },
 60 |     async (o): Promise<CallToolResult> => {
 61 |       downloader.setup({...o, format: 'html'})
 62 |       try {
 63 |         // const cb: NodeToCodeFile[] = (await api.nodeToCode({...o, format: 'react-tailwind'})) || []
 64 |         const cb: NodeToCodeFile[] = (await api.nodeToCode(o)) || []
 65 |         await downloader.checkLocalAndDownload(cb)
 66 |         if (!cb) {
 67 |           return {
 68 |             content: [
 69 |               {
 70 |                 type: 'text',
 71 |                 text: 'Failed to generate any code. Please check if the provided Figma node IDs are correct or if the nodes can be converted to code.',
 72 |               },
 73 |             ],
 74 |           }
 75 |         }
 76 | 
 77 |         const files = Array.isArray(cb) ? cb : [cb]
 78 | 
 79 |         // Handle case when returned file array is empty
 80 |         if (files.length === 0) {
 81 |           return {
 82 |             content: [
 83 |               {
 84 |                 type: 'text',
 85 |                 text: 'Conversion succeeded but no files were generated. Please check if the selected Figma nodes contain convertible content.',
 86 |               },
 87 |             ],
 88 |           }
 89 |         }
 90 | 
 91 |         // Create file summary
 92 |         const summary = files.map((file, index) => `${index + 1}. ${file.path}`).join('\n')
 93 |         // If local path is specified, return save location information instead of detailed content
 94 |         if (o.localPath) {
 95 |           return {
 96 |             content: [
 97 |               {
 98 |                 type: 'text',
 99 |                 text: `# Files Saved Locally
100 | 
101 | ## Save Location
102 | ${o.localPath}
103 | 
104 | ## Generated Files
105 | ${summary}. Convert the Tailwind to vanilla CSS if not already used in the codebase. Do not install any dependencies.`,
106 |               },
107 |             ],
108 |           }
109 |         }
110 | 
111 |         // Create detailed file content (only when local path is not specified)
112 |         const fileDetails = files
113 |           .map((file, index) => {
114 |             const fileExtension = file.path.split('.').pop() || ''
115 |             return `## File ${index + 1}: ${file.path}\n\`\`\`${fileExtension}\n${file.content}\n\`\`\``
116 |           })
117 |           .join('\n\n')
118 | 
119 |         return {
120 |           content: [
121 |             {
122 |               type: 'text',
123 |               text: `# Generated Files Summary\n${summary}\n\n# File Details\n${fileDetails}. Convert the Tailwind to vanilla CSS if not already used in the codebase. Do not install any dependencies.`,
124 |             },
125 |           ],
126 |         }
127 |       } catch (error: any) {
128 |         logger.error('Tool execution error:', error)
129 |         return {
130 |           content: [{type: 'text', text: `Error: ${error.message}`}],
131 |         }
132 |       }
133 |     },
134 |   )
135 | }
136 | 
```

--------------------------------------------------------------------------------
/src/transports/streamable-http/http-server.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type {Server} from 'http'
  2 | import {randomUUID} from 'node:crypto'
  3 | import figmaConfig from '@/server/figma/config'
  4 | import {LogLevel, Logger, createLogger} from '@/utils/logger'
  5 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
  6 | import {SSEServerTransport} from '@modelcontextprotocol/sdk/server/sse.js'
  7 | import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js'
  8 | import {isInitializeRequest} from '@modelcontextprotocol/sdk/types.js'
  9 | import express, {type Request, type Response} from 'express'
 10 | 
 11 | const logger = createLogger('HttpServer', LogLevel.INFO)
 12 | let httpServer: Server | null = null
 13 | const transports = {
 14 |   streamable: {} as Record<string, StreamableHTTPServerTransport>,
 15 |   sse: {} as Record<string, SSEServerTransport>,
 16 | }
 17 | export async function startHttpServer(port: number, mcpServer: McpServer): Promise<void> {
 18 |   const app = express()
 19 | 
 20 |   // 拦截所有请求,检查并更新personalToken
 21 |   app.use((req, res, next) => {
 22 |     const personalToken = req.header('personalToken') as string
 23 |     //
 24 |     // console.log('[personalToken]', personalToken, figmaConfig.personalToken)
 25 |     //
 26 |     if (personalToken && personalToken.trim() !== '' && personalToken !== figmaConfig.personalToken) {
 27 |       logger.debug('Updating Figma personal token from request headers', personalToken, figmaConfig.personalToken)
 28 |       figmaConfig.personalToken = personalToken
 29 |     }
 30 |     next()
 31 |   })
 32 | 
 33 |   app.use('/mcp', express.json())
 34 | 
 35 |   app.post('/mcp', async (req, res) => {
 36 |     logger.debug('Received StreamableHTTP request', JSON.stringify(req.headers), JSON.stringify(req.body))
 37 |     res.setHeader('Content-Type', 'application/json')
 38 |     const sessionId = req.headers['mcp-session-id'] as string | undefined
 39 |     let transport: StreamableHTTPServerTransport
 40 | 
 41 |     if (sessionId && transports.streamable[sessionId]) {
 42 |       logger.debug('Reusing existing StreamableHTTP transport for sessionId', sessionId)
 43 |       transport = transports.streamable[sessionId]
 44 |     } else if (!sessionId && isInitializeRequest(req.body)) {
 45 |       logger.debug('New initialization request for StreamableHTTP sessionId', sessionId)
 46 |       transport = new StreamableHTTPServerTransport({
 47 |         sessionIdGenerator: () => randomUUID(),
 48 |         onsessioninitialized: sessionId => {
 49 |           transports.streamable[sessionId] = transport
 50 |         },
 51 |       })
 52 |       transport.onclose = () => {
 53 |         if (transport.sessionId) {
 54 |           delete transports.streamable[transport.sessionId]
 55 |         }
 56 |       }
 57 |       await mcpServer.connect(transport)
 58 |     } else {
 59 |       logger.error('Invalid request:', req.body)
 60 |       res.status(400).json({
 61 |         jsonrpc: '2.0',
 62 |         error: {
 63 |           code: -32000,
 64 |           message: 'Bad Request: No valid session ID provided',
 65 |         },
 66 |         id: null,
 67 |       })
 68 |       return
 69 |     }
 70 | 
 71 |     let progressInterval: NodeJS.Timeout | null = null
 72 |     const progressToken = req.body.params?._meta?.progressToken
 73 |     let progress = 0
 74 |     if (progressToken) {
 75 |       logger.info(`Setting up progress notifications for token ${progressToken} on session ${sessionId}`)
 76 |       progressInterval = setInterval(async () => {
 77 |         logger.info('Sending progress notification', progress)
 78 |         await mcpServer.server.notification({
 79 |           method: 'notifications/progress',
 80 |           params: {
 81 |             progress,
 82 |             progressToken,
 83 |           },
 84 |         })
 85 |         progress++
 86 |       }, 1000)
 87 |     }
 88 | 
 89 |     logger.debug('Handling StreamableHTTP request')
 90 |     await transport.handleRequest(req, res, req.body)
 91 | 
 92 |     if (progressInterval) {
 93 |       clearInterval(progressInterval)
 94 |     }
 95 |     logger.debug('StreamableHTTP request handled')
 96 |   })
 97 | 
 98 |   const handleSessionRequest = async (req: Request, res: Response) => {
 99 |     const sessionId = req.headers['mcp-session-id'] as string | undefined
100 |     if (!sessionId || !transports.streamable[sessionId]) {
101 |       res.status(400).send('Invalid or missing session ID')
102 |       return
103 |     }
104 | 
105 |     logger.info(`Received session termination request for session ${sessionId}`)
106 | 
107 |     try {
108 |       const transport = transports.streamable[sessionId]
109 |       await transport.handleRequest(req, res)
110 |     } catch (error) {
111 |       logger.error('Error handling session termination:', error)
112 |       if (!res.headersSent) {
113 |         res.status(500).send('Error processing session termination')
114 |       }
115 |     }
116 |   }
117 | 
118 |   app.get('/mcp', handleSessionRequest)
119 | 
120 |   app.delete('/mcp', handleSessionRequest)
121 | 
122 |   app.get('/sse', async (req, res) => {
123 |     logger.debug('Received SSE request', JSON.stringify(req.headers), JSON.stringify(req.body))
124 |     const transport = new SSEServerTransport('/messages', res)
125 |     transports.sse[transport.sessionId] = transport
126 |     res.on('close', () => {
127 |       delete transports.sse[transport.sessionId]
128 |     })
129 |     await mcpServer.connect(transport)
130 |   })
131 | 
132 |   app.post('/messages', async (req, res) => {
133 |     const sessionId = req.query.sessionId as string
134 |     const transport = transports.sse[sessionId]
135 |     if (transport) {
136 |       await transport.handlePostMessage(req, res)
137 |     } else {
138 |       res.status(400).send(`No transport found for sessionId ${sessionId}`)
139 |       return
140 |     }
141 |   })
142 | 
143 |   httpServer = app.listen(port, () => {
144 |     logger.info(`SSE endpoint available at http://localhost:${port}/sse`)
145 |     logger.info(`Message endpoint available at http://localhost:${port}/messages`)
146 |     logger.info(`StreamableHTTP endpoint available at http://localhost:${port}/mcp`)
147 |   })
148 | 
149 |   process.on('SIGINT', async () => {
150 |     logger.debug('Shutting down server...')
151 |     await closeTransports(transports.sse)
152 |     await closeTransports(transports.streamable)
153 | 
154 |     logger.debug('Server shutdown complete')
155 |     process.exit(0)
156 |   })
157 | }
158 | 
159 | async function closeTransports(transports: Record<string, SSEServerTransport | StreamableHTTPServerTransport>) {
160 |   for (const sessionId in transports) {
161 |     try {
162 |       await transports[sessionId]?.close()
163 |       delete transports[sessionId]
164 |     } catch (error) {
165 |       logger.error(`Error closing transport for session ${sessionId}:`, error)
166 |     }
167 |   }
168 | }
169 | 
170 | export async function stopHttpServer(): Promise<void> {
171 |   if (!httpServer) {
172 |     throw new Error('HTTP server is not running')
173 |   }
174 | 
175 |   return new Promise((resolve, reject) => {
176 |     httpServer!.close((err: Error | undefined) => {
177 |       if (err) {
178 |         reject(err)
179 |         return
180 |       }
181 |       httpServer = null
182 |       const closing = Object.values(transports.sse).map(transport => {
183 |         return transport.close()
184 |       })
185 |       Promise.all(closing).then(() => {
186 |         resolve()
187 |       })
188 |     })
189 |   })
190 | }
191 | 
```

--------------------------------------------------------------------------------
/src/server/figma/tools/v03.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import api from '@/server/figma/apis/f2c'
  2 | import figmaApi from '@/server/figma/apis/figma'
  3 | import {createLogger} from '@/utils/logger'
  4 | import {reportMcpLoader} from '@f2c/data-reporter'
  5 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
  6 | import type {CallToolResult} from '@modelcontextprotocol/sdk/types.js'
  7 | import type {NodeToCodeFile} from 'src/server/figma/types/f2c'
  8 | import {z} from 'zod'
  9 | import downloader from '../helpers/downloader'
 10 | const logger = createLogger('V3Tool')
 11 | let ideInfo = ''
 12 | export const registerV03Server = (server: McpServer) => {
 13 |   reportMcpLoader().then(ide => {
 14 |     ideInfo = ide || 'other'
 15 |   })
 16 |   // Register Figma to HTML conversion tool
 17 |   server.tool(
 18 |     'get_code',
 19 |     'Generate UI code for a given node or the currently selected node in the Figma desktop app. Use the nodeId parameter to specify a node id. If no node id is provided, the currently selected node will be used. If a URL is provided, extract the node id from the URL, for example, if given the URL https://figma.com/design/:fileKey/:fileName?node-id=1-2, the extracted nodeId would be 1:2.',
 20 |     {
 21 |       fileKey: z
 22 |         .string()
 23 |         .describe(
 24 |           'The Figma file identifier found in the file URL (e.g., https://www.figma.com/file/XXXXXXXXXXXX/). Extract the XXXXXXXXXXXX portion as the fileKey.',
 25 |         ),
 26 |       ids: z
 27 |         .string()
 28 |         .describe(
 29 |           'Comma-separated list of Figma node IDs for conversion. To obtain node IDs, select elements in Figma, right-click and select "Copy/Paste as" → "Copy ID".',
 30 |         ),
 31 |       // format: z
 32 |       //   .enum(['html', 'react-cssmodules', 'react-tailwind'])
 33 |       //   .default('html')
 34 |       //   .describe(
 35 |       //     'Specify the output format: "html" generates semantic HTML/CSS, "react-cssmodules" creates React components with scoped CSS modules, "react-tailwind" produces React components with utility-first Tailwind classes.',
 36 |       //   ),
 37 |       personalToken: z
 38 |         .string()
 39 |         .optional()
 40 |         .describe(
 41 |           'Figma personal access token for API authentication.The parameters are not required when the tool is called.',
 42 |         ),
 43 |       localPath: z
 44 |         .string()
 45 |         .optional()
 46 |         .describe(
 47 |           'Absolute path for asset(e.g., images) and code storage. Directory will be created if non-existent. Path must follow OS-specific format without special character escaping. When this path is set, all code-related static resources are stored in this directory, while other assets (e.g., images) will be saved into the subdirectory named assets under this path.',
 48 |         ),
 49 |       imgFormat: z
 50 |         .enum(['png', 'jpg', 'svg'])
 51 |         .default('png')
 52 |         .describe(
 53 |           'Export format for image assets: "png" for lossless quality, "jpg" for compressed files, or "svg" for vector graphics.',
 54 |         ),
 55 |       scaleSize: z
 56 |         .number()
 57 |         .min(1)
 58 |         .max(4)
 59 |         .default(1)
 60 |         .describe(
 61 |           'Image export scale factor (1-4). Higher values yield better quality at the cost of larger file sizes.',
 62 |         ),
 63 |     },
 64 |     async (o): Promise<CallToolResult> => {
 65 |       downloader.setup(o)
 66 |       try {
 67 |         const cb: NodeToCodeFile[] = (await api.nodeToCode({...o, format: 'react-tailwind', ideInfo})) || []
 68 | 
 69 |         await downloader.checkLocalAndDownload(cb)
 70 |         if (!cb) {
 71 |           return {
 72 |             content: [
 73 |               {
 74 |                 type: 'text',
 75 |                 text: 'Failed to generate any code. Please check if the provided Figma node IDs are correct or if the nodes can be converted to code.',
 76 |               },
 77 |             ],
 78 |           }
 79 |         }
 80 | 
 81 |         const files = Array.isArray(cb) ? cb : [cb]
 82 | 
 83 |         // Handle case when returned file array is empty
 84 |         if (files.length === 0) {
 85 |           return {
 86 |             content: [
 87 |               {
 88 |                 type: 'text',
 89 |                 text: 'Conversion succeeded but no files were generated. Please check if the selected Figma nodes contain convertible content.',
 90 |               },
 91 |             ],
 92 |           }
 93 |         }
 94 | 
 95 |         // Create file summary
 96 |         const summary = files
 97 |           .filter(file => !file.path.endsWith('/index.scss'))
 98 |           .map((file, index) => `${index + 1}. ${file.path}`)
 99 |           .join('\n')
100 |         // If local path is specified, return save location information instead of detailed content
101 |         if (o.localPath) {
102 |           return {
103 |             content: [
104 |               {
105 |                 type: 'text',
106 |                 text: `# Files Saved Locally
107 | 
108 | ## Save Location
109 | ${o.localPath}
110 | 
111 | ## Generated Files
112 | ${summary}. Convert the Tailwind to vanilla CSS if not already used in the codebase. Do not install any dependencies.`,
113 |               },
114 |             ],
115 |           }
116 |         }
117 | 
118 |         // Create detailed file content (only when local path is not specified)
119 |         /* const fileDetails = files
120 |           .map((file, index) => {
121 |             const fileExtension = file.path.split('.').pop() || ''
122 |             return `## File ${index + 1}: ${file.path}\n\`\`\`${fileExtension}\n${file.content}\n\`\`\``
123 |           })
124 |           .join('\n\n') */
125 |         const fileDetails = files
126 |           .filter(file => file.path.endsWith('.tsx'))
127 |           .map(file => file.content)
128 |           .join('\n\n')
129 | 
130 |         return {
131 |           content: [
132 |             {
133 |               type: 'text',
134 |               text: `${fileDetails} Convert the Tailwind to vanilla CSS if not already used in the codebase. Do not install any dependencies.`,
135 |             },
136 |           ],
137 |         }
138 |       } catch (error: any) {
139 |         logger.error('Tool execution error:', error)
140 |         return {
141 |           content: [{type: 'text', text: `Error: ${error.message}`}],
142 |         }
143 |       }
144 |     },
145 |   )
146 |   server.tool(
147 |     'get_image',
148 |     'Generate an image for a given node or the currently selected node in the Figma desktop app. Use the nodeId parameter to specify a node id. If no node id is provided, the currently selected node will be used. If a URL is provided, extract the node id from the URL, for example, if given the URL https://figma.com/design/:fileKey/:fileName?node-id=1-2, the extracted nodeId would be 1:2. Also export Figma design images for visual verification and design fidelity validation. Essential for comparing generated code output against original designs, ensuring pixel-perfect implementation and catching visual discrepancies during the design-to-code process.',
149 |     {
150 |       fileKey: z.string().describe('Figma file identifier from the URL for accessing the design source'),
151 |       ids: z
152 |         .string()
153 |         .describe(
154 |           'Comma-separated node IDs to export. Use "Copy ID" from Figma context menu to get precise element references for comparison',
155 |         ),
156 |       format: z
157 |         .enum(['jpg', 'png', 'svg', 'pdf'])
158 |         .optional()
159 |         .describe(
160 |           'Export format for verification: "png" for pixel-perfect comparison with transparency, "jpg" for quick previews, "svg" for scalable reference, "pdf" for print validation',
161 |         ),
162 |       scale: z
163 |         .number()
164 |         .optional()
165 |         .describe(
166 |           'Scale factor (1-4x) for high-resolution comparison. Use 2x+ for detailed fidelity checks on retina displays',
167 |         ),
168 |       svg_include_id: z
169 |         .boolean()
170 |         .optional()
171 |         .describe('Include element IDs in SVG for precise element mapping during code validation'),
172 |       svg_simplify_stroke: z
173 |         .boolean()
174 |         .optional()
175 |         .describe('Simplify stroke paths for cleaner reference images during visual comparison'),
176 |       use_absolute_bounds: z
177 |         .boolean()
178 |         .optional()
179 |         .describe('Use absolute positioning for accurate layout verification against implemented code'),
180 |       version: z.string().optional().describe('Specific design version for consistent comparison baseline'),
181 |       personalToken: z
182 |         .string()
183 |         .optional()
184 |         .describe('Figma personal access token for authenticated access to design files'),
185 |     },
186 |     async (o): Promise<CallToolResult> => {
187 |       try {
188 |         const data = await figmaApi.images(o)
189 | 
190 |         return {
191 |           content: [{type: 'text', text: JSON.stringify(data)}],
192 |         }
193 |       } catch (error: any) {
194 |         return {
195 |           content: [{type: 'text', text: `Error: ${error.message}`}],
196 |         }
197 |       }
198 |     },
199 |   )
200 | }
201 | 
```