├── .env.example
├── .gitignore
├── .vscode
└── settings.json
├── Dockerfile
├── LICENSE
├── README-zh-CN.md
├── README.md
├── STARTKIT.md
├── biome.jsonc
├── bun.build.script.ts
├── bun.lock
├── docs
├── Info.md
├── banner.png
├── bannerv3.png
├── en
│ ├── FAQ.md
│ └── GettingStarted.md
└── zh
│ ├── FAQ.md
│ └── GettingStarted.md
├── glama.json
├── mcp.json
├── package.json
├── smithery.yaml
├── src
├── cli.ts
├── server
│ └── figma
│ │ ├── apis
│ │ ├── f2c.ts
│ │ └── figma.ts
│ │ ├── config.ts
│ │ ├── helpers
│ │ ├── downloader.ts
│ │ ├── figma.ts
│ │ └── index.ts
│ │ ├── index.ts
│ │ ├── tools
│ │ ├── f2c.ts
│ │ └── figma.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
│ └── logger.ts
├── tsconfig.json
└── user_rules.json
/.env.example:
--------------------------------------------------------------------------------
1 | FIGMA_API_KEY=your_figma_api_key_here
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist/
3 | .env
4 | .demo/
5 | build/
--------------------------------------------------------------------------------
/.vscode/settings.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 f2c-ai
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README-zh-CN.md:
--------------------------------------------------------------------------------
1 | # F2C MCP Server
2 | 
3 | [](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 | [npm-version-src]: https://img.shields.io/npm/v/@f2c/mcp?style=flat&colorA=18181B&colorB=F0DB4F
10 | [npm-version-href]: https://npmjs.com/package/@f2c/mcp
11 | [npm-downloads-src]: https://img.shields.io/npm/dm/@f2c/mcp?style=flat&colorA=18181B&colorB=F0DB4F
12 | [npm-downloads-href]: https://npmjs.com/package/@f2c/mcp
13 | [github-src]: https://img.shields.io/badge/github-@f2c/mcp-blue?style=flat&colorA=18181B&colorB=F0DB4F
14 | [github-href]: https://github.com/f2c-ai/f2c-mcp
15 | [node-src]: https://img.shields.io/node/v/@f2c/mcp?style=flat&colorA=18181B&colorB=F0DB4F
16 | [node-href]: https://nodejs.org/en/about/previous-releases
17 |
18 | [English](./README.md) | 简体中文
19 |
20 | 使用[F2C](https://f2c.yy.com/) 根据 Figma设计稿生成代码的模型上下文协议服务器。
21 |
22 |
23 |
24 |
25 |
26 |
27 | ## 主要功能
28 |
29 |
30 | - 🎨 高保真 HTML/CSS 还原:F2C 实现像素级高保真 Figma 设计到 HTML/CSS 的精准转换。
31 | - ⚛️ 多框架支持:F2C 生成 React、CSS Modules 和 Tailwind CSS 代码,加速前端开发。
32 | - 🧠 Figma 设计上下文:F2C 提供设计上下文,与 AI 工具兼容,确保代码一致性。
33 | - 🔗 Figma 文件 URL 解析:F2C 通过 URL 直接转换 Figma 设计节点,简化工作流程。
34 | - 🖼️ 远程图片本地化:F2C 自动下载 Figma 远程图片到本地,优化开发体验。
35 |
36 | ## 使用说明
37 | 1. 在支持 MCP 的 IDE(如 Cursor、Trae) 中 [配置 Server](docs/zh/GettingStarted.md)。
38 | 2. 在 IDE 中打开聊天窗口(例如:Cursor中的代理模式)。
39 | 3. 粘贴 Figma 节点链接(在Figma的Layer面板选择你想要的节点右键即可复制)。
40 | 4. 在对话框中输入需求,例如:获取节点数据、下载图片、转换为代码等。
41 |
42 | ## 配置开发
43 | [配置&开发](docs/zh/GettingStarted.md)
44 |
45 | ## 常见问题
46 | [FAQ](docs/zh/FAQ.md)
47 |
48 | ## 致谢
49 |
50 | 感谢:
51 |
52 | + [Framelink Figma MCP Server](https://github.com/GLips/Figma-Context-MCP) 通过此模型上下文协议服务器,为Cursor和其他AI编程工具提供Figma文件访问能力。
53 | + [Cursor Talk to Figma MCP](https://github.com/sonnylazuardi/cursor-talk-to-figma-mcp) 允许 Cursor 与 Figma 进行通信,以便读取设计内容并通过编程方式修改它们。这种集成使开发者能够以编程方式访问和操作 Figma 中的设计元素。
54 | + [Figma MCP Server](https://github.com/MatthewDailey/figma-mcp) 该服务器提供直接通过ModelContextProtocol查看、评论和分析Figma设计的工具。
55 |
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # F2C MCP Server
2 | 
3 | [](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 | English | [简体中文](./README-zh-CN.md)
20 |
21 | A Model Context Protocol server for Figma Design to Code using [F2C](https://f2c.yy.com/).
22 |
23 |
24 |
25 |
26 |
27 | ## Features
28 |
29 |
30 | - 🎨 Pixel-Perfect HTML/CSS:F2C converts Figma designs to pixel-perfect HTML/CSS with precision.
31 | - ⚛️ Multi-Framework Support:F2C generates React, CSS Modules, and Tailwind CSS code for fast development.
32 | - 🧠 Figma Design Context:F2C integrates design context, ensuring compatibility with AI tools like Cursor.
33 | - 🔗 Figma File URL Parsing:F2C converts design nodes via Figma URLs, streamlining workflows.
34 | - 🖼️ Remote Image Localization:F2C automates downloading Figma images to local assets for efficiency.
35 |
36 | ## How it works
37 | 1. [Configure the Server](docs/en/GettingStarted.md) in an MCP-supported IDE (e.g., Cursor, Trae).
38 | 2. Open your chat in IDE (e.g. agent mode in Cursor).
39 | 3. Paste a link to a Figma Node (Right-click any node in the Figma Layer panel to copy it).
40 | 4. Enter your requirements in the chat, such as fetching node data, downloading images, converting to code, etc.
41 |
42 | ## Configuration and Development
43 |
44 | See [Configuration and Development](docs/en/GettingStarted.md)
45 |
46 | ## FAQ
47 | See [FAQ](docs/en/FAQ.md)
48 |
49 | ## Credits
50 |
51 | Thanks to:
52 |
53 | + [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.
54 | + [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.
55 | + [Figma MCP Server](https://github.com/MatthewDailey/figma-mcp) This server provides tools for viewing, commenting, and analyzing Figma designs directly through the ModelContextProtocol.
--------------------------------------------------------------------------------
/STARTKIT.md:
--------------------------------------------------------------------------------
1 | ## 开发指南
2 | ### 开发指令
3 | ```sh
4 | npm run dev
5 | ```
6 | ### 调试指令
7 | ```sh
8 | npx @modelcontextprotocol/inspector node ./dist/index.mjs
9 | ```
10 | ### .env文件编辑
11 | 创建 .env 文件,填入你的 figma api key 参考:
12 | ```sh
13 | FIGMA_API_KEY=your_figma_api_key_here
14 | ```
--------------------------------------------------------------------------------
/biome.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@empjs/biome-config"]
3 | }
4 |
--------------------------------------------------------------------------------
/bun.build.script.ts:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/bun.lock:
--------------------------------------------------------------------------------
1 | {
2 | "lockfileVersion": 1,
3 | "workspaces": {
4 | "": {
5 | "name": "@f2c/mcp",
6 | "dependencies": {
7 | "cors": "^2.8.5",
8 | "express": "^5.1.0",
9 | "zod": "^3.22.4",
10 | },
11 | "devDependencies": {
12 | "@biomejs/biome": "^1.9.4",
13 | "@empjs/biome-config": "^0.7.2",
14 | "@modelcontextprotocol/sdk": "1.12.1",
15 | "@types/bun": "^1.2.13",
16 | "@types/cors": "^2.8.18",
17 | "@types/express": "^5.0.1",
18 | "@types/node": "^22.15.0",
19 | "typescript": "^5.8.3",
20 | },
21 | },
22 | },
23 | "packages": {
24 | "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="],
25 |
26 | "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="],
27 |
28 | "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="],
29 |
30 | "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="],
31 |
32 | "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="],
33 |
34 | "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="],
35 |
36 | "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="],
37 |
38 | "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="],
39 |
40 | "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="],
41 |
42 | "@empjs/biome-config": ["@empjs/biome-config@0.7.2", "", { "dependencies": { "@biomejs/biome": "^1.9.4" }, "bin": { "biome": "bin/biome.js" } }, "sha512-KFX877IXvGGY2lkjSxViwtE6l+u8IvTY6scIomTp3Zk8qSoIlfclPuJ3cfb2+xOTllsHFRytOSmYMBKJvePmhA=="],
43 |
44 | "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.12.1", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw=="],
45 |
46 | "@types/body-parser": ["@types/body-parser@1.19.5", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg=="],
47 |
48 | "@types/bun": ["@types/bun@1.2.14", "", { "dependencies": { "bun-types": "1.2.14" } }, "sha512-VsFZKs8oKHzI7zwvECiAJ5oSorWndIWEVhfbYqZd4HI/45kzW7PN2Rr5biAzvGvRuNmYLSANY+H59ubHq8xw7Q=="],
49 |
50 | "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
51 |
52 | "@types/cors": ["@types/cors@2.8.18", "", { "dependencies": { "@types/node": "*" } }, "sha512-nX3d0sxJW41CqQvfOzVG1NCTXfFDrDWIghCZncpHeWlVFd81zxB/DLhg7avFg6eHLCRX7ckBmoIIcqa++upvJA=="],
53 |
54 | "@types/express": ["@types/express@5.0.2", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-BtjL3ZwbCQriyb0DGw+Rt12qAXPiBTPs815lsUvtt1Grk0vLRMZNMUZ741d5rjk+UQOxfDiBZ3dxpX00vSkK3g=="],
55 |
56 | "@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA=="],
57 |
58 | "@types/http-errors": ["@types/http-errors@2.0.4", "", {}, "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="],
59 |
60 | "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
61 |
62 | "@types/node": ["@types/node@22.15.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ=="],
63 |
64 | "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
65 |
66 | "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
67 |
68 | "@types/send": ["@types/send@0.17.4", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA=="],
69 |
70 | "@types/serve-static": ["@types/serve-static@1.15.7", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw=="],
71 |
72 | "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
73 |
74 | "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
75 |
76 | "body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
77 |
78 | "bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="],
79 |
80 | "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
81 |
82 | "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
83 |
84 | "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
85 |
86 | "content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
87 |
88 | "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
89 |
90 | "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
91 |
92 | "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
93 |
94 | "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
95 |
96 | "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
97 |
98 | "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
99 |
100 | "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
101 |
102 | "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
103 |
104 | "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
105 |
106 | "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
107 |
108 | "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
109 |
110 | "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
111 |
112 | "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
113 |
114 | "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
115 |
116 | "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
117 |
118 | "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
119 |
120 | "eventsource-parser": ["eventsource-parser@3.0.2", "", {}, "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA=="],
121 |
122 | "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
123 |
124 | "express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="],
125 |
126 | "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
127 |
128 | "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
129 |
130 | "finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
131 |
132 | "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
133 |
134 | "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
135 |
136 | "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
137 |
138 | "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
139 |
140 | "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
141 |
142 | "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
143 |
144 | "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
145 |
146 | "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
147 |
148 | "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
149 |
150 | "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
151 |
152 | "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
153 |
154 | "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
155 |
156 | "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
157 |
158 | "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
159 |
160 | "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
161 |
162 | "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
163 |
164 | "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
165 |
166 | "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
167 |
168 | "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
169 |
170 | "mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
171 |
172 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
173 |
174 | "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
175 |
176 | "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
177 |
178 | "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
179 |
180 | "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
181 |
182 | "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
183 |
184 | "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
185 |
186 | "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
187 |
188 | "path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
189 |
190 | "pkce-challenge": ["pkce-challenge@5.0.0", "", {}, "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="],
191 |
192 | "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
193 |
194 | "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
195 |
196 | "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
197 |
198 | "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
199 |
200 | "raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
201 |
202 | "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
203 |
204 | "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
205 |
206 | "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
207 |
208 | "send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
209 |
210 | "serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
211 |
212 | "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
213 |
214 | "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
215 |
216 | "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
217 |
218 | "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
219 |
220 | "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
221 |
222 | "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
223 |
224 | "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
225 |
226 | "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
227 |
228 | "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
229 |
230 | "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
231 |
232 | "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
233 |
234 | "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
235 |
236 | "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
237 |
238 | "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
239 |
240 | "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
241 |
242 | "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
243 |
244 | "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
245 |
246 | "zod": ["zod@3.25.23", "", {}, "sha512-Od2bdMosahjSrSgJtakrwjMDb1zM1A3VIHCPGveZt/3/wlrTWBya2lmEh2OYe4OIu8mPTmmr0gnLHIWQXdtWBg=="],
247 |
248 | "zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
249 | }
250 | }
251 |
--------------------------------------------------------------------------------
/docs/Info.md:
--------------------------------------------------------------------------------
1 | # F2C MCP 架构
2 | ## 目录结构
3 | + 1. 工具 (Tools):允许 LLM(在用户同意后)请求服务器执行某些动作,比如 get_weather(location) 或 run_code(script)。
4 | + 2. 资源 (Resources):允许客户端读取服务器提供的数据,比如文件的内容、API 的响应结果、数据库的模式信息等。
5 | + 3. 提示 (Prompts):提供预设的交互模板,帮助用户或 LLM 更方便地使用服务器的功能。
6 |
7 | ## 通信方式 (Transports)
8 |
9 | + 1. Stdio (标准输入/输出):主要用于本地场景,例如 IDE 插件连接到本地运行的 MCP 服务器。通信通过进程的标准输入和输出来进行。
10 | + 2. Streamable Http:主要用于网络/云端场景,例如连接到部署在云函数或服务器上的 MCP 服务。它基于 HTTP 长连接。
11 |
12 | ## 协议 (Protocols)
13 | 1. JSON-RPC:主要用于本地场景,例如 IDE 插件连接到本地运行的 MCP 服务器。
14 |
15 | ## 本地调试
16 | ### stido
17 | ```json
18 | {
19 | "mcpServers": {
20 | "f2c-mcp": {
21 | "command": "node",
22 | "args": [
23 | "/Users/xuhongbin/Desktop/Develop/f2c/f2c-mcp/dist/stdio.js"
24 | ],
25 | "env": {
26 | "personalToken": ""
27 | }
28 | }
29 | }
30 | }
31 | ```
32 | ### steamable http
33 | ```json
34 | {
35 | "f2c_mcp": {
36 | "transport": "streamable_http",
37 | "url": "http://172.29.97.170:3000/mcp",
38 | "headers": {},
39 | "timeout": 50
40 | }
41 | }
42 | ```
43 |
44 | ### SSE
45 | ```json
46 | {
47 | "f2c_mcp": {
48 | "transport": "sse",
49 | "url": "http://172.29.97.170:3000/sse",
50 | "headers": {},
51 | "timeout": 50
52 | }
53 | }
54 | ```
55 |
56 | ### 多MCP Server 配置
57 | ```json
58 | {
59 | "mcpServers": {
60 | "f2c_mcp": {
61 | "transport": "streamable_http",
62 | "url": "http://172.29.97.170:3000/mcp",
63 | "headers": {},
64 | "timeout": 50
65 | }
66 | }
67 | }
68 | ```
--------------------------------------------------------------------------------
/docs/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/f2c-ai/f2c-mcp/7000a21d4726d119ef1fca3134d39d3c31e52a1d/docs/banner.png
--------------------------------------------------------------------------------
/docs/bannerv3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/f2c-ai/f2c-mcp/7000a21d4726d119ef1fca3134d39d3c31e52a1d/docs/bannerv3.png
--------------------------------------------------------------------------------
/docs/en/FAQ.md:
--------------------------------------------------------------------------------
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 | ```
--------------------------------------------------------------------------------
/docs/en/GettingStarted.md:
--------------------------------------------------------------------------------
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 | ```json
12 | {
13 | "mcpServers": {
14 | "f2c-mcp": {
15 | "command": "npx",
16 | "args": [
17 | "-y",
18 | "@f2c/mcp"
19 | ],
20 | "env": {
21 | "personalToken": ""
22 | }
23 | }
24 | }
25 | }
26 | ```
27 |
28 | ### Windows (stdio)
29 | ```json
30 | {
31 | "mcpServers": {
32 | "f2c-mcp": {
33 | "command": "cmd",
34 | "args": ["/c", "npx", "-y", "@f2c/mcp"],
35 | "env": {
36 | "personalToken": ""
37 | }
38 | }
39 | }
40 | }
41 | ```
42 |
43 | ## Global Installation MCP Configuration (stdio)
44 | For cases where MCP client instability causes installation errors, we can use global installation and then configure it.
45 |
46 | ```bash
47 | npm install -g @f2c/mcp
48 | ```
49 |
50 | ```json
51 | {
52 | "mcpServers": {
53 | "f2c-mcp": {
54 | "command": "f2c-mcp",
55 | "args": [],
56 | "env": {
57 | "personalToken": ""
58 | }
59 | }
60 | }
61 | }
62 | ```
63 |
64 | ## Other Configuration Types
65 |
66 | ### Add Streamable HTTP
67 | ```json
68 | {
69 | "mcpServers": {
70 | "f2c_mcp": {
71 | "transport": "streamable_http",
72 | "url": "http://localhost:3000/mcp",
73 | "headers": {},
74 | "timeout": 50
75 | }
76 | }
77 | }
78 | ```
79 |
80 | ### Add SSE
81 | ```json
82 | {
83 | "mcpServers": {
84 | "f2c_mcp": {
85 | "transport": "sse",
86 | "url": "http://localhost:3000/sse",
87 | "headers": {},
88 | "timeout": 50
89 | }
90 | }
91 | }
92 | ```
93 |
94 | ## Development
95 |
96 | ### 1. Set up your Figma API key in `.env` file:
97 | ```bash
98 | FIGMA_API_KEY=your_api_key_here
99 | ```
100 |
101 | ### 2. Install dependencies:
102 | ```bash
103 | bun install
104 | ```
105 |
106 | ### 3. Start development server:
107 | ### stdio dev server
108 | ```bash
109 | bun run dev
110 | ```
111 | ### streamable_http and SSE dev server
112 | ```bash
113 | bun run http:dev
114 | ```
115 |
116 | ## Install Smithery
117 |
118 | To install F2C MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@f2c-ai/f2c-mcp):
119 |
120 | ```bash
121 | npx -y @smithery/cli install @f2c-ai/f2c-mcp --client claude
122 | ```
--------------------------------------------------------------------------------
/docs/zh/FAQ.md:
--------------------------------------------------------------------------------
1 | # 常见问题
2 | ## 使用NVM导致Node环境问题
3 | ```
4 | Error: spawn npx ENOENT
5 | ```
6 | 解决方案:将nvm的node目录添加到PATH
7 | ```
8 | {
9 | "env": {
10 | "PATH": "/Users/xxx/.nvm/versions/node/v20.10.0/bin:/bin"
11 | }
12 | }
13 | ```
14 |
--------------------------------------------------------------------------------
/docs/zh/GettingStarted.md:
--------------------------------------------------------------------------------
1 | # Getting 快速开始
2 | 通常,代码编辑器和其他 AI 客户端通过配置文件来管理 MCP 服务器。
3 |
4 | 可以将以下内容添加到配置文件中来设置 `f2c-mcp` 服务器。
5 |
6 | > 注意:您需要创建 Figma 访问令牌才能使用此服务器。有关如何创建 Figma API 访问令牌的说明,请参见[此处](https://help.figma.com/hc/en-us/articles/8085703771159-Manage-personal-access-tokens)。
7 |
8 | ## 免安装配置MCP(stdio)
9 |
10 | ### MacOS / Linux
11 | ```json
12 | {
13 | "mcpServers": {
14 | "f2c-mcp": {
15 | "command": "npx",
16 | "args": [
17 | "-y",
18 | "@f2c/mcp"
19 | ],
20 | "env": {
21 | "personalToken": ""
22 | }
23 | }
24 | }
25 | }
26 | ```
27 |
28 | ### Windows(stdio)
29 | ```json
30 | {
31 | "mcpServers": {
32 | "f2c-mcp": {
33 | "command": "cmd",
34 | "args": ["/c", "npx", "-y", "@f2c/mcp"],
35 | "env": {
36 | "personalToken": ""
37 | }
38 | }
39 | }
40 | }
41 | ```
42 |
43 | ## 全局安装配置MCP(stdio)
44 | 对于mcp client不稳定导致安装报错的情况,我们可以采用全局安装再配置的方式
45 |
46 | ```bash
47 | npm install -g @f2c/mcp
48 | ```
49 |
50 | ```json
51 | {
52 | "mcpServers": {
53 | "f2c-mcp": {
54 | "command": "f2c-mcp",
55 | "args": [],
56 | "env": {
57 | "personalToken": ""
58 | }
59 | }
60 | }
61 | }
62 | ```
63 |
64 | ## 其它类型配置
65 |
66 | ### 添加 Streamable HTTP
67 | ```json
68 | {
69 | "mcpServers": {
70 | "f2c_mcp": {
71 | "transport": "streamable_http",
72 | "url": "http://localhost:3000/mcp",
73 | "headers": {},
74 | "timeout": 50
75 | }
76 | }
77 | }
78 | ```
79 |
80 | ### 添加 SSE
81 | ```json
82 | {
83 | "mcpServers": {
84 | "f2c_mcp": {
85 | "transport": "sse",
86 | "url": "http://localhost:3000/sse",
87 | "headers": {},
88 | "timeout": 50
89 | }
90 | }
91 | }
92 | ```
93 |
94 | ## 开发
95 |
96 | ### 1. 在`.env`文件中设置您的Figma API密钥:
97 | ```bash
98 | FIGMA_API_KEY=your_api_key_here
99 | ```
100 |
101 | ### 2. 安装依赖:
102 | ```bash
103 | bun install
104 | ```
105 |
106 | ### 3. 启动开发服务器:
107 | ### stdio dev server
108 | ```bash
109 | bun run dev
110 | ```
111 | ### streamable_http and SSE dev server
112 | ```bash
113 | bun run http:dev
114 | ```
115 |
116 | ## 安装 Smithery
117 |
118 | To install F2C MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@f2c-ai/f2c-mcp):
119 |
120 | ```bash
121 | npx -y @smithery/cli install @f2c-ai/f2c-mcp --client claude
122 | ```
--------------------------------------------------------------------------------
/glama.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://glama.ai/mcp/schemas/server.json",
3 | "maintainers": ["ckken"]
4 | }
5 |
--------------------------------------------------------------------------------
/mcp.json:
--------------------------------------------------------------------------------
1 | {
2 | "mcpServers": {
3 | "f2c-mcp": {
4 | "command": "npx",
5 | "args": ["-y", "f2c-mcp"],
6 | "env": {
7 | "personalToken": ""
8 | }
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@f2c/mcp",
3 | "version": "0.2.2",
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 | "zod": "^3.22.4"
60 | },
61 | "devDependencies": {
62 | "@biomejs/biome": "^1.9.4",
63 | "@empjs/biome-config": "^0.7.2",
64 | "@modelcontextprotocol/sdk": "1.12.1",
65 | "@types/bun": "^1.2.13",
66 | "@types/cors": "^2.8.18",
67 | "@types/express": "^5.0.1",
68 | "@types/node": "^22.15.0",
69 | "typescript": "^5.8.3"
70 | },
71 | "engines": {
72 | "node": ">=16.0.0"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/smithery.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 |
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
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/apis/f2c.ts:
--------------------------------------------------------------------------------
1 | import type {NodeToCodeAllFiles, NodeToCodeFile, NodeToCodeWithF2COptions} from '@/server/figma/types/f2c'
2 | import {DEFAULT_PERSONAL_TOKEN} from 'src/server/figma/config'
3 | import {createLogger} from '@/utils/logger'
4 |
5 | const logger = createLogger('F2cApi')
6 |
7 | class F2cApi {
8 | protected f2cHost = `https://f2c-figma-api.yy.com/api`
9 | private personalToken = DEFAULT_PERSONAL_TOKEN
10 | //
11 | async nodeToCode(o: NodeToCodeWithF2COptions): Promise {
12 | const op = {
13 | fileKey: o.fileKey,
14 | nodeIds: o.ids,
15 | personal_token: o.personalToken || this.personalToken,
16 | option: {
17 | cssFramework: 'inlinecss',
18 | imgFormat: o.imgFormat || 'png',
19 | scaleSize: o.scaleSize || 2,
20 | },
21 | format: 'files',
22 | // format: 'allFiles',
23 | }
24 | if (o.format === 'react-cssmodules') {
25 | op.option.cssFramework = 'cssmodules'
26 | } else if (o.format === 'react-tailwind') {
27 | op.option.cssFramework = 'tailwindcss'
28 | }
29 | const url = this.opToUrl(`${this.f2cHost}/nodes`, op)
30 | return this.fetch(url, 'json')
31 | }
32 | async fetch(url: string, resType: 'json' | 'text' = 'json'): Promise {
33 | try {
34 | const fetchOptions = {
35 | method: 'GET',
36 | }
37 | const response = await fetch(url, fetchOptions)
38 | if (!response.ok) {
39 | throw new Error(`HTTP error! status: ${response.status}`)
40 | }
41 | const data = resType === 'text' ? await response.text() : await response.json()
42 | return data
43 | } catch (error) {
44 | logger.error('HTTP error', error)
45 | throw error
46 | }
47 | }
48 | private opToUrl(api: string, o: any = {}) {
49 | if (Object.keys(o).length === 0) {
50 | return api
51 | }
52 | const url: any = new URL(api)
53 | for (const [key, value] of Object.entries(o)) {
54 | if (typeof value === 'object' && value !== null) {
55 | for (const [nestedKey, nestedValue] of Object.entries(value)) {
56 | url.searchParams.append(`${key}[${nestedKey}]`, nestedValue as string)
57 | }
58 | } else {
59 | url.searchParams.append(key, value as string)
60 | }
61 | }
62 | return url.toString()
63 | }
64 | }
65 | export default new F2cApi()
66 |
--------------------------------------------------------------------------------
/src/server/figma/apis/figma.ts:
--------------------------------------------------------------------------------
1 | import type {GetFileParams, GetImagesParams, GetKeyParams} from '@/server/figma/types/figma'
2 | import {DEFAULT_PERSONAL_TOKEN} from 'src/server/figma/config'
3 | import {createLogger} from '@/utils/logger'
4 |
5 | const logger = createLogger('FigmaRestApi')
6 |
7 | class FigmaRestApi {
8 | protected figmaHost = `https://api.figma.com/v1`
9 | private personalToken = DEFAULT_PERSONAL_TOKEN
10 | async files(o: GetFileParams) {
11 | let url: string
12 | if (o.ids) {
13 | url = this.opToUrl(`${this.figmaHost}/files/${o.fileKey}/nodes`, o)
14 | } else {
15 | url = this.opToUrl(`${this.figmaHost}/files/${o.fileKey}`, o)
16 | }
17 |
18 | return this.fetch(url)
19 | }
20 | async images(o: GetImagesParams) {
21 | const url = this.opToUrl(`${this.figmaHost}/images/${o.fileKey}`, o)
22 | return this.fetch(url)
23 | }
24 | // Returns download links for all images present in image fills
25 | async imageFills(o: GetKeyParams) {
26 | const url = this.opToUrl(`${this.figmaHost}/files/${o.fileKey}/images`, o)
27 | return this.fetch(url)
28 | }
29 | // Returns the metadata for the file referred to by :key
30 | async meta(o: GetKeyParams) {
31 | const url = this.opToUrl(`${this.figmaHost}/files/${o.fileKey}/meta`, o)
32 | return this.fetch(url)
33 | }
34 | async fetch(url: string, resType: 'json' | 'text' = 'json'): Promise {
35 | try {
36 | const fetchOptions = {
37 | method: 'GET',
38 | headers: {
39 | 'X-FIGMA-TOKEN': this.personalToken,
40 | },
41 | }
42 | const response = await fetch(url, fetchOptions)
43 | // logger.debug('response', url, JSON.stringify(fetchOptions))
44 | if (!response.ok) {
45 | throw new Error(`HTTP error! status: ${response.status}`)
46 | }
47 | const data = resType === 'text' ? await response.text() : await response.json()
48 | return data
49 | } catch (error) {
50 | logger.error('HTTP error', error)
51 | throw error
52 | }
53 | }
54 | private opToUrl(api: string, o: any = {}, filters = ['fileKey', 'personalToken']) {
55 | if (Object.keys(o).length === 0) {
56 | return api
57 | }
58 | if (o.personalToken) {
59 | this.personalToken = o.personalToken
60 | }
61 | const url: any = new URL(api)
62 | for (const [key, value] of Object.entries(o)) {
63 | if (!filters.includes(key)) url.searchParams.append(key, value)
64 | }
65 | return url.toString()
66 | }
67 | }
68 | export default new FigmaRestApi()
69 |
--------------------------------------------------------------------------------
/src/server/figma/config.ts:
--------------------------------------------------------------------------------
1 | // import {createLogger} from '@/utils/logger'
2 |
3 | // const logger = createLogger('FigmaConfig')
4 |
5 | export const DEFAULT_PERSONAL_TOKEN = process.env.FIGMA_API_KEY || ''
6 | // logger.debug('DEFAULT_PERSONAL_TOKEN', DEFAULT_PERSONAL_TOKEN)
7 | export const serverName = 'F2C MCP'
8 | export const serverVersion = process.env.FIGMA_VERSION || '0.0.1'
9 | // logger.debug('DEFAULT_PERSONAL_TOKEN', DEFAULT_PERSONAL_TOKEN)
10 |
--------------------------------------------------------------------------------
/src/server/figma/helpers/downloader.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import {createLogger} from '@/utils/logger'
4 |
5 | const logger = createLogger('Downloader')
6 |
7 | export interface DownloadOptions {
8 | localPath: string
9 | fileName: string
10 | }
11 |
12 | export class Downloader {
13 | imgFormat = 'png'
14 | setImgFormat(format: string) {
15 | this.imgFormat = format
16 | }
17 | /**
18 | * 下载远程图片到本地
19 | * @param url 远程图片URL
20 | * @param options 下载选项
21 | * @returns 返回本地相对路径
22 | */
23 | async downloadImage(url: string, options: DownloadOptions): Promise {
24 | try {
25 | // 确保目标目录存在
26 | if (!fs.existsSync(options.localPath)) {
27 | fs.mkdirSync(options.localPath, {recursive: true})
28 | }
29 | const localfileName = `${options.fileName}.${this.imgFormat}`
30 | // 构建本地文件路径
31 | const localFilePath = path.join(options.localPath, localfileName)
32 |
33 | // 下载图片
34 | const response = await fetch(url)
35 | if (!response.ok) {
36 | throw new Error(`下载失败: ${response.status} ${response.statusText}`)
37 | }
38 |
39 | // 将响应内容转换为Buffer并写入文件
40 | const buffer = await response.arrayBuffer()
41 | fs.writeFileSync(localFilePath, new Uint8Array(buffer))
42 |
43 | // 返回相对路径
44 | return path.join(path.basename(options.localPath), localfileName).replace(/\\/g, '/')
45 | } catch (error) {
46 | logger.error('图片下载错误:', error)
47 | throw error
48 | }
49 | }
50 |
51 | /**
52 | * 从HTML/JSX内容中提取并下载图片
53 | * @param content 包含图片URL的内容
54 | * @param localPath 本地存储路径
55 | * @returns 替换后的内容
56 | */
57 | async processContent(content: string, localPath: string): Promise {
58 | localPath = path.join(localPath, 'images')
59 | try {
60 | // 匹配Figma图片URL的正则表达式
61 | const imgRegex = /https:\/\/figma-alpha-api\.s3\.us-west-2\.amazonaws\.com\/images\/[a-f0-9-]+/g
62 | const matches = content.match(imgRegex)
63 |
64 | if (!matches) {
65 | return content
66 | }
67 |
68 | // 去重URL
69 | const uniqueUrls = [...new Set(matches)];
70 |
71 | // 创建下载任务映射
72 | const downloadTasks = new Map();
73 |
74 | // 并行下载所有图片
75 | await Promise.all(
76 | uniqueUrls.map(async (remoteUrl) => {
77 | const fileName = path.basename(remoteUrl);
78 | const localUrl = await this.downloadImage(remoteUrl, {
79 | localPath,
80 | fileName,
81 | });
82 | downloadTasks.set(remoteUrl, localUrl);
83 | })
84 | );
85 |
86 | // 一次性替换所有URL
87 | let processedContent = content;
88 | for (const [remoteUrl, localUrl] of downloadTasks.entries()) {
89 | // 使用全局替换以处理重复的URL
90 | const regex = new RegExp(remoteUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g');
91 | processedContent = processedContent.replace(regex, localUrl);
92 | }
93 |
94 | return processedContent;
95 | } catch (error) {
96 | logger.error('内容处理错误:', error)
97 | throw error
98 | }
99 | }
100 | }
101 |
102 | export default new Downloader()
103 |
--------------------------------------------------------------------------------
/src/server/figma/helpers/figma.ts:
--------------------------------------------------------------------------------
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/server/figma/helpers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './figma'
2 | export * from './downloader'
3 |
--------------------------------------------------------------------------------
/src/server/figma/index.ts:
--------------------------------------------------------------------------------
1 | import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
2 | import {serverName, serverVersion} from 'src/server/figma/config'
3 | import {registerF2cServer} from 'src/server/figma/tools/f2c'
4 | import {registerFigmaServer} from 'src/server/figma/tools/figma'
5 |
6 | export const server = new McpServer(
7 | {
8 | name: serverName,
9 | version: serverVersion,
10 | },
11 | {
12 | capabilities: {
13 | logging: {},
14 | },
15 | },
16 | )
17 | registerFigmaServer(server)
18 | registerF2cServer(server)
19 |
--------------------------------------------------------------------------------
/src/server/figma/tools/f2c.ts:
--------------------------------------------------------------------------------
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, React with CSS Modules, or React with Tailwind CSS, 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.',
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, context): Promise => {
61 | logger.info(context)
62 | try {
63 | const cb: NodeToCodeFile[] = (await api.nodeToCode(o)) || []
64 | if (o.localPath) {
65 | downloader.setImgFormat(o.imgFormat)
66 | await Promise.all(
67 | cb.map(async f => {
68 | f.content = await downloader.processContent(f.content, o.localPath as string)
69 | }),
70 | )
71 | }
72 | return {
73 | content: [
74 | {
75 | type: 'text',
76 | text: JSON.stringify(cb),
77 | },
78 | ],
79 | }
80 | } catch (error: any) {
81 | logger.error('Tool execution error:', error)
82 | return {
83 | content: [{type: 'text', text: `Error: ${error.message}`}],
84 | }
85 | }
86 | },
87 | )
88 | }
89 |
--------------------------------------------------------------------------------
/src/server/figma/tools/figma.ts:
--------------------------------------------------------------------------------
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 => {
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 => {
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 => {
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 => {
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/types/f2c.ts:
--------------------------------------------------------------------------------
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 | format: string
10 | ids: string
11 | fileKey: string
12 | imgFormat: 'png' | 'jpg' | 'svg'
13 | scaleSize: number
14 | }
15 |
16 | export interface NodeToCodeAllFiles {
17 | files: NodeToCodeFile[]
18 | images: {
19 | [key: string]: {id: string; name: string; fileExt: 'png' | 'jpg' | 'svg'; nodeType: string}
20 | }
21 | }
22 | export interface NodeToCodeFile {
23 | content: string
24 | path: string
25 | }
26 |
--------------------------------------------------------------------------------
/src/server/figma/types/figma.ts:
--------------------------------------------------------------------------------
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/stdio.ts:
--------------------------------------------------------------------------------
1 | import {server} from 'src/server/figma'
2 | import {startServer} from 'src/transports/stdio'
3 | startServer(server)
4 |
--------------------------------------------------------------------------------
/src/streamable-http.ts:
--------------------------------------------------------------------------------
1 | import {startServer} from '@/transports/streamable-http'
2 | import {server} from 'src/server/figma'
3 | startServer(server, 3000)
4 |
--------------------------------------------------------------------------------
/src/test/api.test.ts:
--------------------------------------------------------------------------------
1 | import {afterEach, describe, expect, it} from 'bun:test'
2 | import f2cApi from '@/server/figma/apis/f2c'
3 | import figmaApi from '@/server/figma/apis/figma'
4 | import {DEFAULT_PERSONAL_TOKEN} from 'src/server/figma/config'
5 |
6 | // 测试常量
7 | const originalFetch = global.fetch
8 | const fileKey = 'DkzGbKo09kf2w1ytMPALxd'
9 | const ids = '293-1752'
10 | const personalToken = DEFAULT_PERSONAL_TOKEN
11 |
12 | // 模拟响应生成器
13 | const createMockResponse = (options: {
14 | ok?: boolean
15 | status?: number
16 | textData?: string
17 | jsonData?: any
18 | throwError?: boolean
19 | }) => {
20 | if (options.throwError) {
21 | return () => {
22 | throw new Error('网络连接失败')
23 | }
24 | }
25 |
26 | return () => {
27 | return {
28 | ok: options.ok ?? true,
29 | status: options.status ?? 200,
30 | text: async () => options.textData ?? '',
31 | json: async () => options.jsonData ?? {},
32 | }
33 | }
34 | }
35 |
36 | describe('Figma API', () => {
37 | // 每个测试后恢复原始 fetch
38 | afterEach(() => {
39 | global.fetch = originalFetch
40 | })
41 |
42 | describe('f2cApi.nodeToCode', () => {
43 | const htmlCode = '测试 HTML 代码
'
44 |
45 | it('应该正确转换 Figma 节点为代码', async () => {
46 | // 模拟成功的响应
47 | global.fetch = createMockResponse({textData: htmlCode}) as any
48 |
49 | // 使用手动方法替换来跟踪函数调用
50 | const fetchCalls: any[] = []
51 | const originalFetchMethod = f2cApi.fetch
52 | f2cApi.fetch = function (...args: any) {
53 | fetchCalls.push(args)
54 | return originalFetchMethod.apply(this, args)
55 | }
56 |
57 | const result = await f2cApi.nodeToCode({
58 | fileKey,
59 | ids,
60 | format: 'html',
61 | personalToken,
62 | })
63 |
64 | expect(result).toBe(htmlCode)
65 | expect(fetchCalls.length).toBeGreaterThan(0)
66 | expect(fetchCalls[0][0]).toContain('https://f2c-figma-api.yy.com/api/nodes')
67 | expect(fetchCalls[0][0]).toContain(`fileKey=${fileKey}`)
68 | expect(fetchCalls[0][0]).toContain(`nodeIds=${encodeURIComponent(ids)}`)
69 | expect(fetchCalls[0][0]).toContain(`personal_token=${personalToken}`)
70 |
71 | // 恢复原始方法
72 | f2cApi.fetch = originalFetchMethod
73 | })
74 |
75 | it('应该处理 API 错误', async () => {
76 | // 模拟失败的响应
77 | global.fetch = createMockResponse({ok: false, status: 404}) as any
78 |
79 | try {
80 | await f2cApi.nodeToCode({
81 | fileKey,
82 | ids,
83 | format: 'html',
84 | })
85 | // 如果没有抛出错误,测试应该失败
86 | expect(true).toBe(false)
87 | } catch (error: any) {
88 | expect(error.message).toContain('HTTP error')
89 | }
90 | })
91 |
92 | it('应该使用默认令牌当未提供个人令牌时', async () => {
93 | global.fetch = createMockResponse({textData: htmlCode}) as any
94 |
95 | // 使用手动方法替换来跟踪函数调用
96 | const fetchCalls: any[] = []
97 | const originalFetchMethod = f2cApi.fetch
98 | f2cApi.fetch = function (...args: any) {
99 | fetchCalls.push(args)
100 | return originalFetchMethod.apply(this, args)
101 | }
102 |
103 | await f2cApi.nodeToCode({
104 | fileKey,
105 | ids,
106 | format: 'html',
107 | })
108 |
109 | // 验证使用了默认令牌
110 | expect(fetchCalls.length).toBeGreaterThan(0)
111 | expect(fetchCalls[0][0]).toContain(`personal_token=${DEFAULT_PERSONAL_TOKEN}`)
112 |
113 | // 恢复原始方法
114 | f2cApi.fetch = originalFetchMethod
115 | })
116 | })
117 |
118 | describe('figmaApi.files', () => {
119 | it('应该获取文件节点数据', async () => {
120 | const mockData = {nodes: {[ids]: {id: ids}}}
121 | global.fetch = createMockResponse({jsonData: mockData}) as any
122 |
123 | // 使用手动方法替换来跟踪函数调用
124 | const fetchCalls: any[] = []
125 | const originalFetchMethod = figmaApi.fetch
126 | figmaApi.fetch = function (...args: any) {
127 | fetchCalls.push(args)
128 | return originalFetchMethod.apply(this, args)
129 | }
130 |
131 | await figmaApi.files({
132 | fileKey,
133 | ids,
134 | })
135 |
136 | expect(fetchCalls.length).toBeGreaterThan(0)
137 | expect(fetchCalls[0][0]).toContain(`https://api.figma.com/v1/files/${fileKey}/nodes`)
138 |
139 | // 恢复原始方法
140 | figmaApi.fetch = originalFetchMethod
141 | })
142 |
143 | it('应该获取整个文件数据当未提供节点 ID 时', async () => {
144 | const mockData = {document: {}}
145 | global.fetch = createMockResponse({jsonData: mockData}) as any
146 |
147 | // 使用手动方法替换来跟踪函数调用
148 | const fetchCalls: any[] = []
149 | const originalFetchMethod = figmaApi.fetch
150 | figmaApi.fetch = function (...args: any) {
151 | fetchCalls.push(args)
152 | return originalFetchMethod.apply(this, args)
153 | }
154 |
155 | await figmaApi.files({
156 | fileKey,
157 | })
158 |
159 | expect(fetchCalls.length).toBeGreaterThan(0)
160 | expect(fetchCalls[0][0]).toContain(`https://api.figma.com/v1/files/${fileKey}`)
161 | expect(fetchCalls[0][0]).not.toContain('/nodes')
162 |
163 | // 恢复原始方法
164 | figmaApi.fetch = originalFetchMethod
165 | })
166 | })
167 |
168 | describe('figmaApi.images', () => {
169 | it('应该获取图像数据', async () => {
170 | const mockData = {images: {[ids]: 'image-url'}}
171 | global.fetch = createMockResponse({jsonData: mockData}) as any
172 |
173 | // 使用手动方法替换来跟踪函数调用
174 | const fetchCalls: any[] = []
175 | const originalFetchMethod = figmaApi.fetch
176 | figmaApi.fetch = function (...args: any) {
177 | fetchCalls.push(args)
178 | return originalFetchMethod.apply(this, args)
179 | }
180 |
181 | await figmaApi.images({
182 | fileKey,
183 | ids,
184 | format: 'png',
185 | })
186 |
187 | expect(fetchCalls.length).toBeGreaterThan(0)
188 | expect(fetchCalls[0][0]).toContain(`https://api.figma.com/v1/images/${fileKey}`)
189 | expect(fetchCalls[0][0]).toContain(`ids=${ids}`)
190 | expect(fetchCalls[0][0]).toContain('format=png')
191 |
192 | // 恢复原始方法
193 | figmaApi.fetch = originalFetchMethod
194 | })
195 | })
196 |
197 | describe('figmaApi.imageFills', () => {
198 | it('应该获取图像填充数据', async () => {
199 | const mockData = {meta: {images: {}}}
200 | global.fetch = createMockResponse({jsonData: mockData}) as any
201 |
202 | // 使用手动方法替换来跟踪函数调用
203 | const fetchCalls: any[] = []
204 | const originalFetchMethod = figmaApi.fetch
205 | figmaApi.fetch = function (...args: any) {
206 | fetchCalls.push(args)
207 | return originalFetchMethod.apply(this, args)
208 | }
209 |
210 | await figmaApi.imageFills({
211 | fileKey,
212 | })
213 |
214 | expect(fetchCalls.length).toBeGreaterThan(0)
215 | expect(fetchCalls[0][0]).toContain(`https://api.figma.com/v1/files/${fileKey}/images`)
216 |
217 | // 恢复原始方法
218 | figmaApi.fetch = originalFetchMethod
219 | })
220 | })
221 |
222 | describe('figmaApi.meta', () => {
223 | it('应该获取元数据', async () => {
224 | const mockData = {name: 'Test File'}
225 | global.fetch = createMockResponse({jsonData: mockData}) as any
226 |
227 | // 使用手动方法替换来跟踪函数调用
228 | const fetchCalls: any[] = []
229 | const originalFetchMethod = figmaApi.fetch
230 | figmaApi.fetch = function (...args: any) {
231 | fetchCalls.push(args)
232 | return originalFetchMethod.apply(this, args)
233 | }
234 |
235 | await figmaApi.meta({
236 | fileKey,
237 | })
238 |
239 | expect(fetchCalls.length).toBeGreaterThan(0)
240 | expect(fetchCalls[0][0]).toContain(`https://api.figma.com/v1/files/${fileKey}/meta`)
241 |
242 | // 恢复原始方法
243 | figmaApi.fetch = originalFetchMethod
244 | })
245 | })
246 |
247 | describe('API fetch 方法', () => {
248 | describe('figmaApi.fetch', () => {
249 | it('应该处理成功的文本响应', async () => {
250 | global.fetch = createMockResponse({textData: '测试数据'}) as any
251 | const textResult = await figmaApi.fetch('https://test-url.com', 'text')
252 | expect(textResult).toBe('测试数据')
253 | })
254 |
255 | it('应该处理成功的 JSON 响应', async () => {
256 | const mockData = {data: '测试数据'}
257 | global.fetch = createMockResponse({jsonData: mockData}) as any
258 | const jsonResult = await figmaApi.fetch('https://test-url.com', 'json')
259 | expect(jsonResult).toEqual(mockData)
260 | })
261 |
262 | it('应该处理失败的响应', async () => {
263 | global.fetch = createMockResponse({ok: false, status: 500}) as any
264 |
265 | try {
266 | await figmaApi.fetch('https://test-url.com')
267 | // 如果没有抛出错误,测试应该失败
268 | expect(true).toBe(false)
269 | } catch (error: any) {
270 | expect(error.message).toContain('HTTP error')
271 | expect(error.message).toContain('500')
272 | }
273 | })
274 |
275 | it('应该处理网络错误', async () => {
276 | global.fetch = createMockResponse({throwError: true}) as any
277 |
278 | try {
279 | await figmaApi.fetch('https://test-url.com')
280 | // 如果没有抛出错误,测试应该失败
281 | expect(true).toBe(false)
282 | } catch (error: any) {
283 | expect(error.message).toBe('网络连接失败')
284 | }
285 | })
286 | })
287 |
288 | describe('f2cApi.fetch', () => {
289 | it('应该处理成功的文本响应', async () => {
290 | global.fetch = createMockResponse({textData: '测试数据'}) as any
291 | const textResult = await f2cApi.fetch('https://test-url.com', 'text')
292 | expect(textResult).toBe('测试数据')
293 | })
294 |
295 | it('应该处理成功的 JSON 响应', async () => {
296 | const mockData = {data: '测试数据'}
297 | global.fetch = createMockResponse({jsonData: mockData}) as any
298 | const jsonResult = await f2cApi.fetch('https://test-url.com', 'json')
299 | expect(jsonResult).toEqual(mockData)
300 | })
301 |
302 | it('应该处理失败的响应', async () => {
303 | global.fetch = createMockResponse({ok: false, status: 500}) as any
304 |
305 | try {
306 | await f2cApi.fetch('https://test-url.com')
307 | // 如果没有抛出错误,测试应该失败
308 | expect(true).toBe(false)
309 | } catch (error: any) {
310 | expect(error.message).toContain('HTTP error')
311 | expect(error.message).toContain('500')
312 | }
313 | })
314 | })
315 | })
316 |
317 | describe('URL 构建方法', () => {
318 | describe('figmaApi.opToUrl', () => {
319 | it('应该正确构建 Figma API URL', async () => {
320 | global.fetch = createMockResponse({jsonData: {}}) as any
321 |
322 | // 使用手动方法替换来跟踪函数调用
323 | const fetchCalls: any[] = []
324 | const originalGlobalFetch = global.fetch
325 | const fetchSpy = function (url: string, options?: any) {
326 | fetchCalls.push([url, options])
327 | return originalGlobalFetch(url, options)
328 | }
329 | global.fetch = fetchSpy as any
330 |
331 | await figmaApi.files({
332 | fileKey,
333 | ids,
334 | version: '123',
335 | })
336 |
337 | // 验证构建的 URL 包含所有参数
338 | expect(fetchCalls.length).toBeGreaterThan(0)
339 | const url = fetchCalls[0][0]
340 | expect(url).toContain(`https://api.figma.com/v1/files/${fileKey}/nodes`)
341 | expect(url).toContain(`ids=${ids}`)
342 | expect(url).toContain('version=123')
343 | expect(url).not.toContain('fileKey=')
344 | })
345 |
346 | it('应该设置个人令牌', async () => {
347 | global.fetch = createMockResponse({jsonData: {}}) as any
348 |
349 | // 使用手动方法替换来跟踪函数调用
350 | const fetchCalls: any[] = []
351 | const originalGlobalFetch = global.fetch
352 | const fetchSpy = function (url: string, options?: any) {
353 | fetchCalls.push([url, options])
354 | return originalGlobalFetch(url, options)
355 | }
356 | global.fetch = fetchSpy as any
357 |
358 | await figmaApi.files({
359 | fileKey,
360 | personalToken: 'custom-token',
361 | })
362 |
363 | // 验证请求头中包含个人令牌
364 | expect(fetchCalls.length).toBeGreaterThan(0)
365 | const options = fetchCalls[0][1]
366 | expect(options.headers['X-FIGMA-TOKEN']).toBe('custom-token')
367 | })
368 | })
369 |
370 | describe('f2cApi.opToUrl', () => {
371 | it('应该正确构建 F2C API URL', async () => {
372 | global.fetch = createMockResponse({textData: 'test'}) as any
373 |
374 | // 使用手动方法替换来跟踪函数调用
375 | const fetchCalls: any[] = []
376 | const originalGlobalFetch = global.fetch
377 | const fetchSpy = function (url: string, options?: any) {
378 | fetchCalls.push([url, options])
379 | return originalGlobalFetch(url, options)
380 | }
381 | global.fetch = fetchSpy as any
382 |
383 | await f2cApi.nodeToCode({
384 | fileKey,
385 | ids,
386 | format: 'html',
387 | personalToken,
388 | })
389 |
390 | // 验证构建的 URL 包含所有参数
391 | expect(fetchCalls.length).toBeGreaterThan(0)
392 | const url = fetchCalls[0][0]
393 | expect(url).toContain('https://f2c-figma-api.yy.com/api/nodes')
394 | expect(url).toContain(`fileKey=${fileKey}`)
395 | expect(url).toContain(`nodeIds=${ids}`)
396 | expect(url).toContain('format=html')
397 | expect(url).toContain(`personal_token=${personalToken}`)
398 | })
399 | })
400 | })
401 | })
402 |
--------------------------------------------------------------------------------
/src/test/e2e.test.ts:
--------------------------------------------------------------------------------
1 | import {describe, expect, it} from 'bun:test'
2 | import f2cApi from '@/server/figma/apis/f2c'
3 | import api from '@/server/figma/apis/figma'
4 | import {createLogger} from '@/utils/logger'
5 | import {DEFAULT_PERSONAL_TOKEN} from 'src/server/figma/config'
6 |
7 | const logger = createLogger('E2ETest')
8 |
9 | // 测试常量
10 | const fileKey = 'DkzGbKo09kf2w1ytMPALxd'
11 | const ids = '293-1752'
12 | const personalToken = DEFAULT_PERSONAL_TOKEN
13 |
14 | describe('Figma API 端到端测试', () => {
15 | // 设置较长的超时时间,因为实际网络请求可能需要更多时间
16 | const timeout = 30000
17 |
18 | describe('f2cNodeToCode 端到端测试', () => {
19 | it('应该能够从真实 Figma API 获取节点代码', async () => {
20 | // 跳过测试如果没有设置个人令牌
21 | if (!personalToken) {
22 | logger.info('跳过测试:未设置 FIGMA_API_KEY 环境变量')
23 | return
24 | }
25 |
26 | try {
27 | // 发起真实的 API 请求
28 | const result = await f2cApi.nodeToCode({
29 | fileKey,
30 | ids,
31 | format: 'html',
32 | personalToken,
33 | imgFormat: 'png',
34 | scaleSize: 0,
35 | })
36 |
37 | // 验证返回结果
38 | expect(result).toBeDefined()
39 | expect(typeof result).toBe('string')
40 |
41 | // 验证返回的 HTML 包含基本结构
42 | expect(result).toContain('<')
43 | expect(result).toContain('>')
44 |
45 | // 记录结果以便手动检查
46 | // logger.info('API 返回结果预览(前100字符):', result.substring(0, 100))
47 | } catch (error) {
48 | logger.error('API 请求失败:', error)
49 | throw error
50 | }
51 | })
52 | })
53 |
54 | describe('files 端到端测试', () => {
55 | it('应该能够从真实 Figma API 获取文件节点数据', async () => {
56 | // 跳过测试如果没有设置个人令牌
57 | if (!personalToken) {
58 | logger.info('跳过测试:未设置 FIGMA_API_KEY 环境变量')
59 | return
60 | }
61 |
62 | try {
63 | // 发起真实的 API 请求
64 | const result = await api.files({
65 | fileKey,
66 | ids,
67 | personalToken,
68 | })
69 |
70 | // 验证返回结果
71 | expect(result).toBeDefined()
72 | expect(result).toHaveProperty('nodes')
73 |
74 | // 验证返回的节点数据包含请求的节点ID
75 | const nodeIds = Object.keys(result.nodes || {})
76 | expect(nodeIds.length).toBeGreaterThan(0)
77 |
78 | // 记录结果以便手动检查
79 | logger.info('文件节点数据:', nodeIds)
80 | } catch (error) {
81 | logger.error('API 请求失败:', error)
82 | throw error
83 | }
84 | })
85 | })
86 |
87 | describe('meta 端到端测试', () => {
88 | it('应该能够从真实 Figma API 获取文件元数据', async () => {
89 | // 跳过测试如果没有设置个人令牌
90 | if (!personalToken) {
91 | logger.info('跳过测试:未设置 FIGMA_API_KEY 环境变量')
92 | return
93 | }
94 |
95 | try {
96 | // 发起真实的 API 请求
97 | const result = await api.meta({
98 | fileKey,
99 | personalToken,
100 | })
101 | // console.log('result', result)
102 | // 验证返回结果
103 | expect(result).toBeDefined()
104 | expect(result).toHaveProperty('file')
105 | // expect(result).toHaveProperty('lastModified')
106 |
107 | // 记录结果以便手动检查
108 | logger.info('文件元数据:', JSON.stringify(result.file))
109 | } catch (error) {
110 | logger.error('API 请求失败:', error)
111 | throw error
112 | }
113 | })
114 | })
115 | })
116 |
--------------------------------------------------------------------------------
/src/transports/stdio.ts:
--------------------------------------------------------------------------------
1 | import {createLogger} from '@/utils/logger'
2 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
3 | import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'
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/transports/streamable-http/http-server.ts:
--------------------------------------------------------------------------------
1 | import type {Server} from 'http'
2 | import {randomUUID} from 'node:crypto'
3 | import {createLogger} from '@/utils/logger'
4 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
5 | import {SSEServerTransport} from '@modelcontextprotocol/sdk/server/sse.js'
6 | import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js'
7 | import {isInitializeRequest} from '@modelcontextprotocol/sdk/types.js'
8 | import express, {type Request, type Response} from 'express'
9 |
10 | const logger = createLogger('HttpServer')
11 |
12 | let httpServer: Server | null = null
13 | const transports = {
14 | streamable: {} as Record,
15 | sse: {} as Record,
16 | }
17 |
18 | export async function startHttpServer(port: number, mcpServer: McpServer): Promise {
19 | const app = express()
20 |
21 | app.use('/mcp', express.json())
22 |
23 | app.post('/mcp', async (req, res) => {
24 | logger.info('Received StreamableHTTP request', JSON.stringify(req.headers), JSON.stringify(req.body))
25 | res.setHeader('Content-Type', 'application/json')
26 | const sessionId = req.headers['mcp-session-id'] as string | undefined
27 | let transport: StreamableHTTPServerTransport
28 |
29 | if (sessionId && transports.streamable[sessionId]) {
30 | logger.info('Reusing existing StreamableHTTP transport for sessionId', sessionId)
31 | transport = transports.streamable[sessionId]
32 | } else if (!sessionId && isInitializeRequest(req.body)) {
33 | logger.info('New initialization request for StreamableHTTP sessionId', sessionId)
34 | transport = new StreamableHTTPServerTransport({
35 | sessionIdGenerator: () => randomUUID(),
36 | onsessioninitialized: sessionId => {
37 | transports.streamable[sessionId] = transport
38 | },
39 | })
40 | transport.onclose = () => {
41 | if (transport.sessionId) {
42 | delete transports.streamable[transport.sessionId]
43 | }
44 | }
45 | await mcpServer.connect(transport)
46 | } else {
47 | logger.info('Invalid request:', req.body)
48 | res.status(400).json({
49 | jsonrpc: '2.0',
50 | error: {
51 | code: -32000,
52 | message: 'Bad Request: No valid session ID provided',
53 | },
54 | id: null,
55 | })
56 | return
57 | }
58 |
59 | let progressInterval: NodeJS.Timeout | null = null
60 | const progressToken = req.body.params?._meta?.progressToken
61 | let progress = 0
62 | if (progressToken) {
63 | logger.info(`Setting up progress notifications for token ${progressToken} on session ${sessionId}`)
64 | progressInterval = setInterval(async () => {
65 | logger.info('Sending progress notification', progress)
66 | await mcpServer.server.notification({
67 | method: 'notifications/progress',
68 | params: {
69 | progress,
70 | progressToken,
71 | },
72 | })
73 | progress++
74 | }, 1000)
75 | }
76 |
77 | logger.info('Handling StreamableHTTP request')
78 | await transport.handleRequest(req, res, req.body)
79 |
80 | if (progressInterval) {
81 | clearInterval(progressInterval)
82 | }
83 | logger.info('StreamableHTTP request handled')
84 | })
85 |
86 | const handleSessionRequest = async (req: Request, res: Response) => {
87 | const sessionId = req.headers['mcp-session-id'] as string | undefined
88 | if (!sessionId || !transports.streamable[sessionId]) {
89 | res.status(400).send('Invalid or missing session ID')
90 | return
91 | }
92 |
93 | logger.info(`Received session termination request for session ${sessionId}`)
94 |
95 | try {
96 | const transport = transports.streamable[sessionId]
97 | await transport.handleRequest(req, res)
98 | } catch (error) {
99 | logger.error('Error handling session termination:', error)
100 | if (!res.headersSent) {
101 | res.status(500).send('Error processing session termination')
102 | }
103 | }
104 | }
105 |
106 | app.get('/mcp', handleSessionRequest)
107 |
108 | app.delete('/mcp', handleSessionRequest)
109 |
110 | app.get('/sse', async (req, res) => {
111 | const transport = new SSEServerTransport('/messages', res)
112 | transports.sse[transport.sessionId] = transport
113 | res.on('close', () => {
114 | delete transports.sse[transport.sessionId]
115 | })
116 | await mcpServer.connect(transport)
117 | })
118 |
119 | app.post('/messages', async (req, res) => {
120 | const sessionId = req.query.sessionId as string
121 | const transport = transports.sse[sessionId]
122 | if (transport) {
123 | await transport.handlePostMessage(req, res)
124 | } else {
125 | res.status(400).send(`No transport found for sessionId ${sessionId}`)
126 | return
127 | }
128 | })
129 |
130 | httpServer = app.listen(port, () => {
131 | logger.info(`SSE endpoint available at http://localhost:${port}/sse`)
132 | logger.info(`Message endpoint available at http://localhost:${port}/messages`)
133 | logger.info(`StreamableHTTP endpoint available at http://localhost:${port}/mcp`)
134 | })
135 |
136 | process.on('SIGINT', async () => {
137 | logger.info('Shutting down server...')
138 | await closeTransports(transports.sse)
139 | await closeTransports(transports.streamable)
140 |
141 | logger.info('Server shutdown complete')
142 | process.exit(0)
143 | })
144 | }
145 |
146 | async function closeTransports(transports: Record) {
147 | for (const sessionId in transports) {
148 | try {
149 | await transports[sessionId]?.close()
150 | delete transports[sessionId]
151 | } catch (error) {
152 | logger.error(`Error closing transport for session ${sessionId}:`, error)
153 | }
154 | }
155 | }
156 |
157 | export async function stopHttpServer(): Promise {
158 | if (!httpServer) {
159 | throw new Error('HTTP server is not running')
160 | }
161 |
162 | return new Promise((resolve, reject) => {
163 | httpServer!.close((err: Error | undefined) => {
164 | if (err) {
165 | reject(err)
166 | return
167 | }
168 | httpServer = null
169 | const closing = Object.values(transports.sse).map(transport => {
170 | return transport.close()
171 | })
172 | Promise.all(closing).then(() => {
173 | resolve()
174 | })
175 | })
176 | })
177 | }
178 |
--------------------------------------------------------------------------------
/src/transports/streamable-http/index.ts:
--------------------------------------------------------------------------------
1 | import {createLogger} from '@/utils/logger'
2 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
3 | import {startHttpServer} from './http-server'
4 |
5 | const logger = createLogger('StreamableHttp')
6 |
7 | /* export async function startServer(server: McpServer, port = 3000, useSession = false) {
8 | if (useSession) {
9 | logger.info('Starting MCP server with session support')
10 | const {startServer: startWithSessionServer} = await import('./with-session-steamable-http.js')
11 | return startWithSessionServer(server, port)
12 | } else {
13 | logger.info('Starting MCP server without session support')
14 | const {startServer: startWithoutSessionServer} = await import('./without-session-steamable-http.js')
15 | return startWithoutSessionServer(server, port)
16 | }
17 | } */
18 |
19 | export async function startServer(server: McpServer, port = 3000) {
20 | startHttpServer(port, server)
21 | }
22 |
--------------------------------------------------------------------------------
/src/transports/streamable-http/with-session-steamable-http.ts:
--------------------------------------------------------------------------------
1 | import {randomUUID} from 'node:crypto'
2 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
3 | import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js'
4 | import {isInitializeRequest} from '@modelcontextprotocol/sdk/types.js'
5 | import express from 'express'
6 | import {createLogger} from '@/utils/logger'
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 | })
66 | }
67 |
--------------------------------------------------------------------------------
/src/transports/streamable-http/without-session-steamable-http.ts:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------
1 | export enum LogLevel {
2 | DEBUG = 0,
3 | INFO = 1,
4 | WARN = 2,
5 | ERROR = 3,
6 | }
7 |
8 | export class Logger {
9 | private context: string
10 | private level: LogLevel
11 |
12 | constructor(context: string, level: LogLevel = LogLevel.INFO) {
13 | this.context = context
14 | this.level = level
15 | }
16 |
17 | setLevel(level: LogLevel): void {
18 | this.level = level
19 | }
20 |
21 | debug(message: string, ...args: any[]): void {
22 | if (this.level <= LogLevel.DEBUG) {
23 | console.log(`[DEBUG] [${this.context}] ${message}`, ...args)
24 | }
25 | }
26 |
27 | info(message: any, ...args: any[]): void {
28 | if (this.level <= LogLevel.INFO) {
29 | console.log(`[INFO] [${this.context}] ${message}`, ...args)
30 | }
31 | }
32 |
33 | warn(message: string, ...args: any[]): void {
34 | if (this.level <= LogLevel.WARN) {
35 | console.warn(`[WARN] [${this.context}] ${message}`, ...args)
36 | }
37 | }
38 |
39 | error(message: string, ...args: any[]): void {
40 | if (this.level <= LogLevel.ERROR) {
41 | console.error(`[ERROR] [${this.context}] ${message}`, ...args)
42 | }
43 | }
44 | }
45 |
46 | // 创建默认日志实例
47 | export const createLogger = (context: string) => new Logger(context)
48 |
--------------------------------------------------------------------------------
/tsconfig.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 |
--------------------------------------------------------------------------------
/user_rules.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 |
--------------------------------------------------------------------------------