Repository: MatthewDailey/figma-mcp Branch: main Commit: 765155fb7042 Files: 9 Total size: 17.1 KB Directory structure: gitextract_6ugqw82s/ ├── .gitignore ├── .prettierrc ├── Dockerfile ├── README.md ├── figma_api.ts ├── index.ts ├── package.json ├── smithery.yaml └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ dist node_modules ================================================ FILE: .prettierrc ================================================ { "printWidth": 100, "tabWidth": 2, "useTabs": false, "semi": true, "singleQuote": false, "trailingComma": "es5", "bracketSpacing": true, "arrowParens": "always", "ignore": ["dist/**/*"] } ================================================ FILE: Dockerfile ================================================ # Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config FROM node:lts-alpine # Create app directory WORKDIR /app # Install dependencies COPY package.json package-lock.json tsconfig.json index.ts figma_api.ts ./ # Install deps without running prepare/build scripts RUN npm ci --ignore-scripts # Build the project RUN npm run build # Copy only built files are already in place because build outputs to dist # Expose no port needed for stdio # Default command CMD ["node", "dist/index.cjs"] ================================================ FILE: README.md ================================================ # Figma MCP Server [![smithery badge](https://smithery.ai/badge/@MatthewDailey/figma-mcp)](https://smithery.ai/server/@MatthewDailey/figma-mcp) A [ModelContextProtocol](https://modelcontextprotocol.io) server that enables AI assistants to interact with Figma files. This server provides tools for viewing, commenting, and analyzing Figma designs directly through the ModelContextProtocol. ## Features - Add a Figma file to your chat with Claude by providing the url - Read and post comments on Figma files ## Setup with Claude ### Installing via Smithery To install Figma MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@MatthewDailey/figma-mcp): ```bash npx -y @smithery/cli install @MatthewDailey/figma-mcp --client claude ``` 1. Download and install Claude desktop app from [claude.ai/download](https://claude.ai/download) 2. Get a Figma API Key (figma.com -> click your name top left -> settings -> Security). Grant `File content` and `Comments` scopes. 2. Configure Claude to use the Figma MCP server. If this is your first MCP server, run the following in terminal. ```bash echo '{ "mcpServers": { "figma-mcp": { "command": "npx", "args": ["figma-mcp"], "env": { "FIGMA_API_KEY": "" } } } }' > ~/Library/Application\ Support/Claude/claude_desktop_config.json ``` If it's not, copy the `figma-mcp` block to your `claude_desktop_config.json` 3. Restart Claude Desktop. 4. Look for the hammer icon with the number of available tools in Claude's interface to confirm the server is running. ## Example usage Start a new chat with claude desktop and paste the following ``` What's in this figma file? https://www.figma.com/design/MLkM98c1s4A9o9CMnHEyEC ``` ## Demo of a more realistic usage https://www.loom.com/share/0e759622e05e4ab1819325bcf6128945?sid=bcf6125b-b5de-4098-bf81-baff157e3dc3 ## Development Setup ### Running with Inspector For development and debugging purposes, you can use the MCP Inspector tool. The Inspector provides a visual interface for testing and monitoring MCP server interactions. Visit the [Inspector documentation](https://modelcontextprotocol.io/docs/tools/inspector) for detailed setup instructions and usage guidelines. The command to test locally with Inspector is ``` npx @modelcontextprotocol/inspector npx figma-mcp ``` ### Local Development 1. Clone the repository 2. Install dependencies: ```bash npm install ``` 3. Build the project: ```bash npm run build ``` 4. For development with auto-rebuilding: ```bash npm run watch ``` ## Available Tools The server provides the following tools: - `add_figma_file`: Add a Figma file to your context by providing its URL - `view_node`: Get a thumbnail for a specific node in a Figma file - `read_comments`: Get all comments on a Figma file - `post_comment`: Post a comment on a node in a Figma file - `reply_to_comment`: Reply to an existing comment in a Figma file Each tool is designed to provide specific functionality for interacting with Figma files through the ModelContextProtocol interface. ================================================ FILE: figma_api.ts ================================================ import axios from "axios"; function getFigmaApiKey() { const apiKey = process.env.FIGMA_API_KEY; if (!apiKey) { throw new Error("FIGMA_API_KEY is not set"); } return apiKey; } export function parseKeyFromUrl(url: string) { // Extract key from URLs like: // https://www.figma.com/board/vJzJ1oVCzowAKAayQJx6Ug/... // https://www.figma.com/design/8SvxepW26v4d0AyyTAw23c/... // https://www.figma.com/file/8SvxepW26v4d0AyyTAw23c/... const matches = url.match(/figma\.com\/(board|design|file)\/([^/?]+)/); if (matches) { return matches[2]; // Return the second capture group which contains the key } throw new Error("Could not parse Figma key from URL"); } type FigNode = { id: string; name: string; type: string; children?: FigNode[]; }; type FigFile = { name: string; version: string; document: FigNode; thumbnailUrl: string; thumbnailB64: string; }; export function getCanvasIds(figFileJson: FigNode) { const canvasIds: string[] = []; const queue: FigNode[] = [figFileJson]; while (queue.length > 0) { const node = queue.shift()!; if (node.type === "CANVAS") { canvasIds.push(node.id); continue; // Skip children of canvases } if (node.children) { queue.push(...node.children); } } return canvasIds; } export async function downloadFigmaFile(key: string): Promise { const response = await axios.get(`https://api.figma.com/v1/files/${key}`, { headers: { "X-FIGMA-TOKEN": getFigmaApiKey(), }, }); const data = response.data; return { ...data, thumbnailB64: await imageUrlToBase64(data.thumbnailUrl), }; } export async function getThumbnails(key: string, ids: string[]): Promise<{ [id: string]: string }> { const response = await axios.get( `https://api.figma.com/v1/images/${key}?ids=${ids.join(",")}&format=png&page_size=1`, { headers: { "X-FIGMA-TOKEN": getFigmaApiKey(), }, } ); const data = response.data as { images: { [id: string]: string }; err?: string }; if (data.err) { throw new Error(`Error getting thumbnails: ${data.err}`); } return data.images; } export async function getThumbnailsOfCanvases( key: string, document: FigNode ): Promise<{ id: string; url: string; b64: string }[]> { const canvasIds = getCanvasIds(document); const thumbnails = await getThumbnails(key, canvasIds); const results = []; for (const [id, url] of Object.entries(thumbnails)) { results.push({ id, url, b64: await imageUrlToBase64(url), }); } return results; } export async function readComments(fileKey: string) { const response = await axios.get(`https://api.figma.com/v1/files/${fileKey}/comments`, { headers: { "X-FIGMA-TOKEN": getFigmaApiKey(), }, }); return response.data; } export async function postComment( fileKey: string, message: string, x: number, y: number, nodeId?: string ) { const response = await axios.post( `https://api.figma.com/v1/files/${fileKey}/comments`, { message, client_meta: { node_offset: { x, y }, node_id: nodeId }, }, { headers: { "X-FIGMA-TOKEN": getFigmaApiKey(), "Content-Type": "application/json", }, } ); return response.data; } export async function replyToComment(fileKey: string, commentId: string, message: string) { const response = await axios.post( `https://api.figma.com/v1/files/${fileKey}/comments`, { message, comment_id: commentId, }, { headers: { "X-FIGMA-TOKEN": getFigmaApiKey(), "Content-Type": "application/json", }, } ); return response.data; } async function imageUrlToBase64(url: string) { const response = await axios.get(url, { responseType: "arraybuffer" }); return Buffer.from(response.data).toString("base64"); } ================================================ FILE: index.ts ================================================ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, CallToolResult, ErrorCode, ListToolsRequestSchema, McpError, Tool, } from "@modelcontextprotocol/sdk/types.js"; import { downloadFigmaFile, getThumbnails, parseKeyFromUrl, postComment, readComments, replyToComment, } from "./figma_api.js"; const server = new Server( { name: "figma-mcp", version: "0.1.3", }, { capabilities: { resources: {}, tools: {}, logging: {}, }, } ); const ADD_FIGMA_FILE: Tool = { name: "add_figma_file", description: "Add a Figma file to your context", inputSchema: { type: "object", properties: { url: { type: "string", description: "The URL of the Figma file to add", }, }, required: ["url"], }, }; const VIEW_NODE: Tool = { name: "view_node", description: "Get a thumbnail for a specific node in a Figma file", inputSchema: { type: "object", properties: { file_key: { type: "string", description: "The key of the Figma file", }, node_id: { type: "string", description: "The ID of the node to view. Node ids have the format `:`", }, }, required: ["file_key", "node_id"], }, }; const READ_COMMENTS: Tool = { name: "read_comments", description: "Get all comments on a Figma file", inputSchema: { type: "object", properties: { file_key: { type: "string", description: "The key of the Figma file", }, }, required: ["file_key"], }, }; const POST_COMMENT: Tool = { name: "post_comment", description: "Post a comment on a node in a Figma file", inputSchema: { type: "object", properties: { file_key: { type: "string", description: "The key of the Figma file", }, node_id: { type: "string", description: "The ID of the node to comment on. Node ids have the format `:`", }, message: { type: "string", description: "The comment message", }, x: { type: "number", description: "The x coordinate of the comment pin", }, y: { type: "number", description: "The y coordinate of the comment pin", }, }, required: ["file_key", "message", "x", "y"], }, }; const REPLY_TO_COMMENT: Tool = { name: "reply_to_comment", description: "Reply to an existing comment in a Figma file", inputSchema: { type: "object", properties: { file_key: { type: "string", description: "The key of the Figma file", }, comment_id: { type: "string", description: "The ID of the comment to reply to. Comment ids have the format ``", }, message: { type: "string", description: "The reply message", }, }, required: ["file_key", "comment_id", "message"], }, }; server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ADD_FIGMA_FILE, VIEW_NODE, READ_COMMENTS, POST_COMMENT, REPLY_TO_COMMENT], })); async function doAddFigmaFile(url: string): Promise { const key = parseKeyFromUrl(url); const figFileJson = await downloadFigmaFile(key); // Claude seems to error when this is used // const thumbnails = await getThumbnailsOfCanvases(key, figFileJson.document); return { content: [ { type: "text", text: JSON.stringify({ name: figFileJson.name, key, version: figFileJson.version, }), }, { type: "text", text: "Here is the thumbnail of the Figma file", }, { type: "image", data: figFileJson.thumbnailB64, mimeType: "image/png", }, { type: "text", text: "Here is the JSON representation of the Figma file", }, { type: "text", text: JSON.stringify(figFileJson.document), }, { type: "text", text: "Here are thumbnails of the canvases in the Figma file", }, // ...thumbnails // .map((thumbnail) => [ // { // type: "text" as const, // text: `Next is the image of canvas ID: ${thumbnail.id}`, // }, // { // type: "image" as const, // data: thumbnail.b64, // mimeType: "image/png", // }, // ]) // .flat(), ], }; } async function doViewNode(fileKey: string, nodeId: string): Promise { const thumbnails = await getThumbnails(fileKey, [nodeId]); const nodeThumb = thumbnails[nodeId]; if (!nodeThumb) { throw new Error(`Could not get thumbnail for node ${nodeId}`); } const b64 = await imageUrlToBase64(nodeThumb); return { content: [ { type: "text", text: `Thumbnail for node ${nodeId}:`, }, { type: "image", data: b64, mimeType: "image/png", }, ], }; } async function doReadComments(fileKey: string): Promise { const data = await readComments(fileKey); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async function doPostComment( fileKey: string, message: string, x: number, y: number, nodeId?: string ): Promise { const data = await postComment(fileKey, message, x, y, nodeId); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } async function doReplyToComment( fileKey: string, commentId: string, message: string ): Promise { const data = await replyToComment(fileKey, commentId, message); return { content: [ { type: "text", text: JSON.stringify(data, null, 2), }, ], }; } server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "add_figma_file") { console.error("Adding Figma file", request.params.arguments); const input = request.params.arguments as { url: string }; return doAddFigmaFile(input.url); } if (request.params.name === "view_node") { const input = request.params.arguments as { file_key: string; node_id: string }; return doViewNode(input.file_key, input.node_id); } if (request.params.name === "read_comments") { const input = request.params.arguments as { file_key: string }; return doReadComments(input.file_key); } if (request.params.name === "post_comment") { const input = request.params.arguments as { file_key: string; node_id?: string; message: string; x: number; y: number; }; return doPostComment(input.file_key, input.message, input.x, input.y, input.node_id); } if (request.params.name === "reply_to_comment") { const input = request.params.arguments as { file_key: string; comment_id: string; message: string; }; return doReplyToComment(input.file_key, input.comment_id, input.message); } throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); }); server.onerror = (error) => { console.error(error); }; process.on("SIGINT", async () => { await server.close(); process.exit(0); }); async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Figma MCP Server running on stdio"); } runServer().catch((error) => { console.error("Fatal error running server:", error); process.exit(1); }); async function imageUrlToBase64(url: string) { const response = await fetch(url); const arrayBuffer = await response.arrayBuffer(); return Buffer.from(arrayBuffer).toString("base64"); } ================================================ FILE: package.json ================================================ { "name": "figma-mcp", "version": "0.1.4", "description": "ModelContextProtocol server for Figma", "type": "module", "scripts": { "build": "esbuild index.ts --outfile=dist/index.cjs --bundle --platform=node --format=cjs --banner:js='#!/usr/bin/env node' && shx chmod +x dist/*.cjs", "prepare": "npm run build", "watch": "esbuild index.ts --outfile=dist/index.cjs --bundle --platform=node --format=cjs --banner:js='#!/usr/bin/env node' --watch" }, "bin": { "figma-mcp": "./dist/index.cjs" }, "files": [ "dist" ], "dependencies": { "@modelcontextprotocol/sdk": "^1.0.3", "axios": "^1.9.0" }, "devDependencies": { "@types/node": "^22.10.1", "esbuild": "^0.24.0", "prettier": "^3.4.2", "shx": "^0.3.4", "typescript": "^5.3.3" } } ================================================ FILE: smithery.yaml ================================================ # Smithery configuration file: https://smithery.ai/docs/build/project-config startCommand: type: stdio commandFunction: # A JS function that produces the CLI command based on the given config to start the MCP on stdio. |- (config) => ({ command: 'node', args: ['dist/index.cjs'], env: { FIGMA_API_KEY: config.figmaApiKey } }) configSchema: # JSON Schema defining the configuration options for the MCP. type: object required: - figmaApiKey properties: figmaApiKey: type: string description: Figma API key with File content and Comments scopes exampleConfig: figmaApiKey: ABCD1234EFGH5678IJKL ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "outDir": "./dist", "rootDir": "." }, "include": [ "./**/*.ts" ], "exclude": ["node_modules"] }