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