├── .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 ├── 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 | ![](https://badge.mcpx.dev?type=server 'MCP Server') 3 | [![smithery badge](https://smithery.ai/badge/@f2c-ai/f2c-mcp)](https://smithery.ai/server/@f2c-ai/f2c-mcp) 4 | [![npm version][npm-version-src]][npm-version-href] 5 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 6 | [![github][github-src]][github-href] 7 | [![node][node-src]][node-href] 8 | 9 | [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 | f2c-mcp-server MCP server 24 | 25 | 26 | 27 | ## 主要功能 28 | f2c 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 | ![MCP Server](https://badge.mcpx.dev?type=server 'MCP Server') 3 | [![smithery badge](https://smithery.ai/badge/@f2c-ai/f2c-mcp)](https://smithery.ai/server/@f2c-ai/f2c-mcp) 4 | [![npm version][npm-version-src]][npm-version-href] 5 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 6 | [![github][github-src]][github-href] 7 | [![node][node-src]][node-href] 8 | 9 | 10 | [npm-version-src]: https://img.shields.io/npm/v/@f2c/mcp?style=flat&colorA=18181B&colorB=F0DB4F 11 | [npm-version-href]: https://npmjs.com/package/@f2c/mcp 12 | [npm-downloads-src]: https://img.shields.io/npm/dm/@f2c/mcp?style=flat&colorA=18181B&colorB=F0DB4F 13 | [npm-downloads-href]: https://npmjs.com/package/@f2c/mcp 14 | [github-src]: https://img.shields.io/badge/github-@f2c/mcp-blue?style=flat&colorA=18181B&colorB=F0DB4F 15 | [github-href]: https://github.com/f2c-ai/f2c-mcp 16 | [node-src]: https://img.shields.io/node/v/@f2c/mcp?style=flat&colorA=18181B&colorB=F0DB4F 17 | [node-href]: https://nodejs.org/en/about/previous-releases 18 | 19 | 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 | f2c-mcp-server MCP server 25 | 26 | 27 | ## Features 28 | f2c 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/e23d776fbe407fc433168977162c7d8a9f0b8768/docs/banner.png -------------------------------------------------------------------------------- /docs/bannerv3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f2c-ai/f2c-mcp/e23d776fbe407fc433168977162c7d8a9f0b8768/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.1", 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 | 4 | class F2cApi { 5 | protected f2cHost = `https://f2c-figma-api.yy.com/api` 6 | private personalToken = DEFAULT_PERSONAL_TOKEN 7 | // 8 | async nodeToCode(o: NodeToCodeWithF2COptions): Promise { 9 | const op = { 10 | fileKey: o.fileKey, 11 | nodeIds: o.ids, 12 | personal_token: o.personalToken || this.personalToken, 13 | option: { 14 | cssFramework: 'inlinecss', 15 | imgFormat: o.imgFormat || 'png', 16 | scaleSize: o.scaleSize || 2, 17 | }, 18 | format: 'files', 19 | // format: 'allFiles', 20 | } 21 | if (o.format === 'react-cssmodules') { 22 | op.option.cssFramework = 'cssmodules' 23 | } else if (o.format === 'react-tailwind') { 24 | op.option.cssFramework = 'tailwindcss' 25 | } 26 | const url = this.opToUrl(`${this.f2cHost}/nodes`, op) 27 | return this.fetch(url, 'json') 28 | } 29 | async fetch(url: string, resType: 'json' | 'text' = 'json'): Promise { 30 | try { 31 | const fetchOptions = { 32 | method: 'GET', 33 | } 34 | const response = await fetch(url, fetchOptions) 35 | if (!response.ok) { 36 | throw new Error(`HTTP error! status: ${response.status}`) 37 | } 38 | const data = resType === 'text' ? await response.text() : await response.json() 39 | return data 40 | } catch (error) { 41 | console.error('HTTP error', error) 42 | throw error 43 | } 44 | } 45 | private opToUrl(api: string, o: any = {}) { 46 | if (Object.keys(o).length === 0) { 47 | return api 48 | } 49 | const url: any = new URL(api) 50 | for (const [key, value] of Object.entries(o)) { 51 | if (typeof value === 'object' && value !== null) { 52 | for (const [nestedKey, nestedValue] of Object.entries(value)) { 53 | url.searchParams.append(`${key}[${nestedKey}]`, nestedValue as string) 54 | } 55 | } else { 56 | url.searchParams.append(key, value as string) 57 | } 58 | } 59 | return url.toString() 60 | } 61 | } 62 | export default new F2cApi() 63 | -------------------------------------------------------------------------------- /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 | 4 | class FigmaRestApi { 5 | protected figmaHost = `https://api.figma.com/v1` 6 | private personalToken = DEFAULT_PERSONAL_TOKEN 7 | async files(o: GetFileParams) { 8 | let url: string 9 | if (o.ids) { 10 | url = this.opToUrl(`${this.figmaHost}/files/${o.fileKey}/nodes`, o) 11 | } else { 12 | url = this.opToUrl(`${this.figmaHost}/files/${o.fileKey}`, o) 13 | } 14 | 15 | return this.fetch(url) 16 | } 17 | async images(o: GetImagesParams) { 18 | const url = this.opToUrl(`${this.figmaHost}/images/${o.fileKey}`, o) 19 | return this.fetch(url) 20 | } 21 | // Returns download links for all images present in image fills 22 | async imageFills(o: GetKeyParams) { 23 | const url = this.opToUrl(`${this.figmaHost}/files/${o.fileKey}/images`, o) 24 | return this.fetch(url) 25 | } 26 | // Returns the metadata for the file referred to by :key 27 | async meta(o: GetKeyParams) { 28 | const url = this.opToUrl(`${this.figmaHost}/files/${o.fileKey}/meta`, o) 29 | return this.fetch(url) 30 | } 31 | async fetch(url: string, resType: 'json' | 'text' = 'json'): Promise { 32 | try { 33 | const fetchOptions = { 34 | method: 'GET', 35 | headers: { 36 | 'X-FIGMA-TOKEN': this.personalToken, 37 | }, 38 | } 39 | const response = await fetch(url, fetchOptions) 40 | // console.log('response', url, JSON.stringify(fetchOptions)) 41 | if (!response.ok) { 42 | throw new Error(`HTTP error! status: ${response.status}`) 43 | } 44 | const data = resType === 'text' ? await response.text() : await response.json() 45 | return data 46 | } catch (error) { 47 | console.error('HTTP error', error) 48 | throw error 49 | } 50 | } 51 | private opToUrl(api: string, o: any = {}, filters = ['fileKey', 'personalToken']) { 52 | if (Object.keys(o).length === 0) { 53 | return api 54 | } 55 | if (o.personalToken) { 56 | this.personalToken = o.personalToken 57 | } 58 | const url: any = new URL(api) 59 | for (const [key, value] of Object.entries(o)) { 60 | if (!filters.includes(key)) url.searchParams.append(key, value) 61 | } 62 | return url.toString() 63 | } 64 | } 65 | export default new FigmaRestApi() 66 | -------------------------------------------------------------------------------- /src/server/figma/config.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_PERSONAL_TOKEN = process.env.personalToken || process.env.FIGMA_API_KEY || '' 2 | export const serverName = 'F2C MCP' 3 | export const serverVersion = process.env.FIGMA_VERSION || '0.0.1' 4 | // console.log('DEFAULT_PERSONAL_TOKEN', DEFAULT_PERSONAL_TOKEN) 5 | -------------------------------------------------------------------------------- /src/server/figma/helpers/downloader.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | export interface DownloadOptions { 5 | localPath: string 6 | fileName: string 7 | } 8 | 9 | export class Downloader { 10 | imgFormat = 'png' 11 | setImgFormat(format: string) { 12 | this.imgFormat = format 13 | } 14 | /** 15 | * 下载远程图片到本地 16 | * @param url 远程图片URL 17 | * @param options 下载选项 18 | * @returns 返回本地相对路径 19 | */ 20 | async downloadImage(url: string, options: DownloadOptions): Promise { 21 | try { 22 | // 确保目标目录存在 23 | if (!fs.existsSync(options.localPath)) { 24 | fs.mkdirSync(options.localPath, {recursive: true}) 25 | } 26 | const localfileName = `${options.fileName}.${this.imgFormat}` 27 | // 构建本地文件路径 28 | const localFilePath = path.join(options.localPath, localfileName) 29 | 30 | // 下载图片 31 | const response = await fetch(url) 32 | if (!response.ok) { 33 | throw new Error(`下载失败: ${response.status} ${response.statusText}`) 34 | } 35 | 36 | // 将响应内容转换为Buffer并写入文件 37 | const buffer = await response.arrayBuffer() 38 | fs.writeFileSync(localFilePath, new Uint8Array(buffer)) 39 | 40 | // 返回相对路径 41 | return path.join(path.basename(options.localPath), localfileName).replace(/\\/g, '/') 42 | } catch (error) { 43 | console.error('图片下载错误:', error) 44 | throw error 45 | } 46 | } 47 | 48 | /** 49 | * 从HTML/JSX内容中提取并下载图片 50 | * @param content 包含图片URL的内容 51 | * @param localPath 本地存储路径 52 | * @returns 替换后的内容 53 | */ 54 | async processContent(content: string, localPath: string): Promise { 55 | localPath = path.join(localPath, 'images') 56 | try { 57 | // 匹配Figma图片URL的正则表达式 58 | const imgRegex = /https:\/\/figma-alpha-api\.s3\.us-west-2\.amazonaws\.com\/images\/[a-f0-9-]+/g 59 | const matches = content.match(imgRegex) 60 | 61 | if (!matches) { 62 | return content 63 | } 64 | 65 | let processedContent = content 66 | for (const remoteUrl of matches) { 67 | const fileName = path.basename(remoteUrl) 68 | const localUrl = await this.downloadImage(remoteUrl, { 69 | localPath, 70 | fileName, 71 | }) 72 | 73 | // 替换内容中的远程URL为本地路径 74 | processedContent = processedContent.replace(remoteUrl, localUrl) 75 | } 76 | 77 | return processedContent 78 | } catch (error) { 79 | console.error('内容处理错误:', error) 80 | throw error 81 | } 82 | } 83 | } 84 | 85 | export default new Downloader() 86 | -------------------------------------------------------------------------------- /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 type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' 3 | import type {CallToolResult} from '@modelcontextprotocol/sdk/types.js' 4 | import type {NodeToCodeFile} from 'src/server/figma/types/f2c' 5 | import {z} from 'zod' 6 | import downloader from '../helpers/downloader' 7 | 8 | export const registerF2cServer = (server: McpServer) => { 9 | // Register Figma to HTML conversion tool 10 | server.tool( 11 | 'figma_to_code', 12 | '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.', 13 | { 14 | fileKey: z 15 | .string() 16 | .describe( 17 | 'The Figma file identifier found in the file URL (e.g., https://www.figma.com/file/XXXXXXXXXXXX/). Extract the XXXXXXXXXXXX portion as the fileKey.', 18 | ), 19 | ids: z 20 | .string() 21 | .describe( 22 | '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".', 23 | ), 24 | format: z 25 | .enum(['html', 'react-cssmodules', 'react-tailwind']) 26 | .default('html') 27 | .describe( 28 | '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.', 29 | ), 30 | personalToken: z 31 | .string() 32 | .optional() 33 | .describe( 34 | 'Figma personal access token for API authentication.The parameters are not required when the tool is called.', 35 | ), 36 | localPath: z 37 | .string() 38 | .optional() 39 | .describe( 40 | 'Absolute path for image asset storage. Directory will be created if non-existent. Path must follow OS-specific format without special character escaping.', 41 | ), 42 | imgFormat: z 43 | .enum(['png', 'jpg', 'svg']) 44 | .default('png') 45 | .describe( 46 | 'Export format for image assets: "png" for lossless quality, "jpg" for compressed files, or "svg" for vector graphics.', 47 | ), 48 | scaleSize: z 49 | .number() 50 | .min(1) 51 | .max(4) 52 | .default(2) 53 | .describe( 54 | 'Image export scale factor (1-4). Higher values yield better quality at the cost of larger file sizes.', 55 | ), 56 | }, 57 | async (o, context): Promise => { 58 | console.log(context) 59 | try { 60 | const cb: NodeToCodeFile[] = (await api.nodeToCode(o)) || [] 61 | if (o.localPath) { 62 | downloader.setImgFormat(o.imgFormat) 63 | await Promise.all( 64 | cb.map(async f => { 65 | f.content = await downloader.processContent(f.content, o.localPath as string) 66 | }), 67 | ) 68 | } 69 | return { 70 | content: [ 71 | { 72 | type: 'text', 73 | text: JSON.stringify(cb), 74 | }, 75 | ], 76 | } 77 | } catch (error: any) { 78 | console.error('Tool execution error:', error) 79 | return { 80 | content: [{type: 'text', text: `Error: ${error.message}`}], 81 | } 82 | } 83 | }, 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /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 {DEFAULT_PERSONAL_TOKEN} from 'src/server/figma/config' 5 | 6 | // 测试常量 7 | const fileKey = 'DkzGbKo09kf2w1ytMPALxd' 8 | const ids = '293-1752' 9 | const personalToken = DEFAULT_PERSONAL_TOKEN 10 | 11 | describe('Figma API 端到端测试', () => { 12 | // 设置较长的超时时间,因为实际网络请求可能需要更多时间 13 | const timeout = 30000 14 | 15 | describe('f2cNodeToCode 端到端测试', () => { 16 | it('应该能够从真实 Figma API 获取节点代码', async () => { 17 | // 跳过测试如果没有设置个人令牌 18 | if (!personalToken) { 19 | console.log('跳过测试:未设置 FIGMA_API_KEY 环境变量') 20 | return 21 | } 22 | 23 | try { 24 | // 发起真实的 API 请求 25 | const result = await f2cApi.nodeToCode({ 26 | fileKey, 27 | ids, 28 | format: 'html', 29 | personalToken, 30 | }) 31 | 32 | // 验证返回结果 33 | expect(result).toBeDefined() 34 | expect(typeof result).toBe('string') 35 | 36 | // 验证返回的 HTML 包含基本结构 37 | expect(result).toContain('<') 38 | expect(result).toContain('>') 39 | 40 | // 记录结果以便手动检查 41 | console.log('API 返回结果预览(前100字符):', result.substring(0, 100)) 42 | } catch (error) { 43 | console.error('API 请求失败:', error) 44 | throw error 45 | } 46 | }) 47 | }) 48 | 49 | describe('files 端到端测试', () => { 50 | it('应该能够从真实 Figma API 获取文件节点数据', async () => { 51 | // 跳过测试如果没有设置个人令牌 52 | if (!personalToken) { 53 | console.log('跳过测试:未设置 FIGMA_API_KEY 环境变量') 54 | return 55 | } 56 | 57 | try { 58 | // 发起真实的 API 请求 59 | const result = await api.files({ 60 | fileKey, 61 | ids, 62 | personalToken, 63 | }) 64 | 65 | // 验证返回结果 66 | expect(result).toBeDefined() 67 | expect(result).toHaveProperty('nodes') 68 | 69 | // 验证返回的节点数据包含请求的节点ID 70 | const nodeIds = Object.keys(result.nodes || {}) 71 | expect(nodeIds.length).toBeGreaterThan(0) 72 | 73 | // 记录结果以便手动检查 74 | console.log('文件节点数据:', nodeIds) 75 | } catch (error) { 76 | console.error('API 请求失败:', error) 77 | throw error 78 | } 79 | }) 80 | }) 81 | 82 | describe('meta 端到端测试', () => { 83 | it('应该能够从真实 Figma API 获取文件元数据', async () => { 84 | // 跳过测试如果没有设置个人令牌 85 | if (!personalToken) { 86 | console.log('跳过测试:未设置 FIGMA_API_KEY 环境变量') 87 | return 88 | } 89 | 90 | try { 91 | // 发起真实的 API 请求 92 | const result = await api.meta({ 93 | fileKey, 94 | personalToken, 95 | }) 96 | // console.log('result', result) 97 | // 验证返回结果 98 | expect(result).toBeDefined() 99 | expect(result).toHaveProperty('file') 100 | // expect(result).toHaveProperty('lastModified') 101 | 102 | // 记录结果以便手动检查 103 | console.log('文件元数据:', JSON.stringify(result.file)) 104 | } catch (error) { 105 | console.error('API 请求失败:', error) 106 | throw error 107 | } 108 | }) 109 | }) 110 | }) 111 | -------------------------------------------------------------------------------- /src/transports/stdio.ts: -------------------------------------------------------------------------------- 1 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' 2 | import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js' 3 | 4 | export async function startServer(server: McpServer) { 5 | try { 6 | const transport = new StdioServerTransport() 7 | await server.connect(transport) 8 | } catch (e: any) { 9 | console.log( 10 | JSON.stringify({ 11 | jsonrpc: '2.0', 12 | id: null, 13 | error: { 14 | code: -32000, 15 | message: `Server startup failed: ${e.message}`, 16 | }, 17 | }), 18 | ) 19 | process.exit(1) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/transports/streamable-http/http-server.ts: -------------------------------------------------------------------------------- 1 | import type {Server} from 'http' 2 | import {randomUUID} from 'node:crypto' 3 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' 4 | import {SSEServerTransport} from '@modelcontextprotocol/sdk/server/sse.js' 5 | import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js' 6 | import {isInitializeRequest} from '@modelcontextprotocol/sdk/types.js' 7 | import express, {type Request, type Response} from 'express' 8 | 9 | let httpServer: Server | null = null 10 | const transports = { 11 | streamable: {} as Record, 12 | sse: {} as Record, 13 | } 14 | 15 | export async function startHttpServer(port: number, mcpServer: McpServer): Promise { 16 | const app = express() 17 | 18 | app.use('/mcp', express.json()) 19 | 20 | app.post('/mcp', async (req, res) => { 21 | console.log('Received StreamableHTTP request', JSON.stringify(req.headers), JSON.stringify(req.body)) 22 | res.setHeader('Content-Type', 'application/json') 23 | const sessionId = req.headers['mcp-session-id'] as string | undefined 24 | let transport: StreamableHTTPServerTransport 25 | 26 | if (sessionId && transports.streamable[sessionId]) { 27 | console.log('Reusing existing StreamableHTTP transport for sessionId', sessionId) 28 | transport = transports.streamable[sessionId] 29 | } else if (!sessionId && isInitializeRequest(req.body)) { 30 | console.log('New initialization request for StreamableHTTP sessionId', sessionId) 31 | transport = new StreamableHTTPServerTransport({ 32 | sessionIdGenerator: () => randomUUID(), 33 | onsessioninitialized: sessionId => { 34 | transports.streamable[sessionId] = transport 35 | }, 36 | }) 37 | transport.onclose = () => { 38 | if (transport.sessionId) { 39 | delete transports.streamable[transport.sessionId] 40 | } 41 | } 42 | await mcpServer.connect(transport) 43 | } else { 44 | console.log('Invalid request:', req.body) 45 | res.status(400).json({ 46 | jsonrpc: '2.0', 47 | error: { 48 | code: -32000, 49 | message: 'Bad Request: No valid session ID provided', 50 | }, 51 | id: null, 52 | }) 53 | return 54 | } 55 | 56 | let progressInterval: NodeJS.Timeout | null = null 57 | const progressToken = req.body.params?._meta?.progressToken 58 | let progress = 0 59 | if (progressToken) { 60 | console.log(`Setting up progress notifications for token ${progressToken} on session ${sessionId}`) 61 | progressInterval = setInterval(async () => { 62 | console.log('Sending progress notification', progress) 63 | await mcpServer.server.notification({ 64 | method: 'notifications/progress', 65 | params: { 66 | progress, 67 | progressToken, 68 | }, 69 | }) 70 | progress++ 71 | }, 1000) 72 | } 73 | 74 | console.log('Handling StreamableHTTP request') 75 | await transport.handleRequest(req, res, req.body) 76 | 77 | if (progressInterval) { 78 | clearInterval(progressInterval) 79 | } 80 | console.log('StreamableHTTP request handled') 81 | }) 82 | 83 | const handleSessionRequest = async (req: Request, res: Response) => { 84 | const sessionId = req.headers['mcp-session-id'] as string | undefined 85 | if (!sessionId || !transports.streamable[sessionId]) { 86 | res.status(400).send('Invalid or missing session ID') 87 | return 88 | } 89 | 90 | console.log(`Received session termination request for session ${sessionId}`) 91 | 92 | try { 93 | const transport = transports.streamable[sessionId] 94 | await transport.handleRequest(req, res) 95 | } catch (error) { 96 | console.error('Error handling session termination:', error) 97 | if (!res.headersSent) { 98 | res.status(500).send('Error processing session termination') 99 | } 100 | } 101 | } 102 | 103 | app.get('/mcp', handleSessionRequest) 104 | 105 | app.delete('/mcp', handleSessionRequest) 106 | 107 | app.get('/sse', async (req, res) => { 108 | const transport = new SSEServerTransport('/messages', res) 109 | transports.sse[transport.sessionId] = transport 110 | res.on('close', () => { 111 | delete transports.sse[transport.sessionId] 112 | }) 113 | await mcpServer.connect(transport) 114 | }) 115 | 116 | app.post('/messages', async (req, res) => { 117 | const sessionId = req.query.sessionId as string 118 | const transport = transports.sse[sessionId] 119 | if (transport) { 120 | await transport.handlePostMessage(req, res) 121 | } else { 122 | res.status(400).send(`No transport found for sessionId ${sessionId}`) 123 | return 124 | } 125 | }) 126 | 127 | httpServer = app.listen(port, () => { 128 | console.log(`SSE endpoint available at http://localhost:${port}/sse`) 129 | console.log(`Message endpoint available at http://localhost:${port}/messages`) 130 | console.log(`StreamableHTTP endpoint available at http://localhost:${port}/mcp`) 131 | }) 132 | 133 | process.on('SIGINT', async () => { 134 | console.log('Shutting down server...') 135 | await closeTransports(transports.sse) 136 | await closeTransports(transports.streamable) 137 | 138 | console.log('Server shutdown complete') 139 | process.exit(0) 140 | }) 141 | } 142 | 143 | async function closeTransports(transports: Record) { 144 | for (const sessionId in transports) { 145 | try { 146 | await transports[sessionId]?.close() 147 | delete transports[sessionId] 148 | } catch (error) { 149 | console.error(`Error closing transport for session ${sessionId}:`, error) 150 | } 151 | } 152 | } 153 | 154 | export async function stopHttpServer(): Promise { 155 | if (!httpServer) { 156 | throw new Error('HTTP server is not running') 157 | } 158 | 159 | return new Promise((resolve, reject) => { 160 | httpServer!.close((err: Error | undefined) => { 161 | if (err) { 162 | reject(err) 163 | return 164 | } 165 | httpServer = null 166 | const closing = Object.values(transports.sse).map(transport => { 167 | return transport.close() 168 | }) 169 | Promise.all(closing).then(() => { 170 | resolve() 171 | }) 172 | }) 173 | }) 174 | } 175 | -------------------------------------------------------------------------------- /src/transports/streamable-http/index.ts: -------------------------------------------------------------------------------- 1 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' 2 | import {startHttpServer} from './http-server' 3 | /* export async function startServer(server: McpServer, port = 3000, useSession = false) { 4 | if (useSession) { 5 | console.log('Starting MCP server with session support') 6 | const {startServer: startWithSessionServer} = await import('./with-session-steamable-http.js') 7 | return startWithSessionServer(server, port) 8 | } else { 9 | console.log('Starting MCP server without session support') 10 | const {startServer: startWithoutSessionServer} = await import('./without-session-steamable-http.js') 11 | return startWithoutSessionServer(server, port) 12 | } 13 | } */ 14 | 15 | export async function startServer(server: McpServer, port = 3000) { 16 | startHttpServer(port, server) 17 | } 18 | -------------------------------------------------------------------------------- /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 | const app = express() 7 | app.use(express.json()) 8 | export const startServer = (server: McpServer, port = 3000) => { 9 | const transports: {[sessionId: string]: StreamableHTTPServerTransport} = {} 10 | app.post('/mcp', async (req, res) => { 11 | // let acceptHeader = req.headers.accept as string 12 | // if (acceptHeader === '*/*') { 13 | // acceptHeader = '*/*,application/json, text/event-stream' 14 | // req.headers.accept = acceptHeader 15 | // } 16 | res.setHeader('Content-Type', 'application/json') 17 | const sessionId = req.headers['mcp-session-id'] as string | undefined 18 | let transport: StreamableHTTPServerTransport 19 | if (sessionId && transports[sessionId]) { 20 | transport = transports[sessionId] 21 | } else if (!sessionId && isInitializeRequest(req.body)) { 22 | transport = new StreamableHTTPServerTransport({ 23 | sessionIdGenerator: () => randomUUID(), 24 | enableJsonResponse: true, 25 | onsessioninitialized: sessionId => { 26 | transports[sessionId] = transport 27 | }, 28 | }) 29 | transport.onclose = () => { 30 | if (transport.sessionId) { 31 | delete transports[transport.sessionId] 32 | } 33 | } 34 | await server.connect(transport) 35 | } else { 36 | res.status(400).json({ 37 | jsonrpc: '2.0', 38 | error: { 39 | code: -32000, 40 | message: 'Bad Request: No valid session ID provided', 41 | }, 42 | id: null, 43 | }) 44 | return 45 | } 46 | await transport.handleRequest(req, res, req.body) 47 | }) 48 | const handleSessionRequest = async (req: express.Request, res: express.Response) => { 49 | const sessionId = req.headers['mcp-session-id'] as string | undefined 50 | if (!sessionId || !transports[sessionId]) { 51 | res.status(400).send('Invalid or missing session ID') 52 | return 53 | } 54 | const transport = transports[sessionId] 55 | await transport.handleRequest(req, res) 56 | } 57 | app.get('/mcp', handleSessionRequest) 58 | app.delete('/mcp', handleSessionRequest) 59 | app.listen(port, () => { 60 | console.log(`MCP Session-based Streamable HTTP Server listening on port ${port}`) 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /src/transports/streamable-http/without-session-steamable-http.ts: -------------------------------------------------------------------------------- 1 | import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js' 2 | import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js' 3 | import express from 'express' 4 | const app = express() 5 | app.use( 6 | express.json({ 7 | type: ['application/json', 'application/*+json', '*/*'], // 扩展支持的 Content-Type 8 | }), 9 | ) 10 | const noAllowAcess = (req: any, res: any, next: any) => { 11 | return res.writeHead(405).end( 12 | JSON.stringify({ 13 | jsonrpc: '2.0', 14 | error: { 15 | code: -32000, 16 | message: 'Method not allowed.', 17 | }, 18 | id: null, 19 | }), 20 | ) 21 | } 22 | // const polyfillRequest = (req: any, res: any) => { 23 | // // 设置响应头 24 | // res.setHeader('Content-Type', 'application/json') 25 | // res.setHeader('Connection', 'keep-alive') 26 | // res.setHeader('Keep-Alive', 'timeout=5') 27 | 28 | // // let acceptHeader = req.headers.accept as string 29 | // // if (acceptHeader === '*/*') { 30 | // // acceptHeader = '*/*,application/json, text/event-stream' 31 | // // req.headers.accept = acceptHeader 32 | // // } 33 | 34 | // // 确保请求的 Content-Type 存在 35 | // if (!req.headers['content-type']) { 36 | // req.headers['content-type'] = 'application/json' 37 | // } 38 | // } 39 | export const startServer = (server: McpServer, port = 3000) => { 40 | app.post('/mcp', async (req, res) => { 41 | res.setHeader('Content-Type', 'application/json') 42 | console.log('Request body:', JSON.stringify(req.body)) 43 | // polyfillRequest(req, res) 44 | 45 | try { 46 | const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({ 47 | sessionIdGenerator: undefined, 48 | enableJsonResponse: true, 49 | }) 50 | 51 | res.on('close', () => { 52 | transport.close() 53 | server.close() 54 | }) 55 | 56 | await server.connect(transport) 57 | await transport.handleRequest(req, res, req.body) 58 | } catch (error: any) { 59 | console.error('Error handling MCP request:', error) 60 | console.error('Error stack:', error.stack) 61 | if (!res.headersSent) { 62 | res.status(500).json({ 63 | jsonrpc: '2.0', 64 | error: { 65 | code: -32603, 66 | message: 'Internal server error', 67 | data: { 68 | errorMessage: error.message, 69 | errorName: error.name, 70 | }, 71 | }, 72 | id: req.body?.id || null, 73 | }) 74 | } 75 | } 76 | }) 77 | 78 | app.get('/mcp', noAllowAcess) 79 | app.delete('/mcp', noAllowAcess) 80 | 81 | app.listen(port, () => { 82 | console.log(`MCP Stateless Streamable HTTP server started, listening on port ${port}`) 83 | console.log(`Server address: http://localhost:${port}/mcp`) 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------