#
tokens: 4804/50000 9/9 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── .prettierrc
├── Dockerfile
├── figma_api.ts
├── index.ts
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
└── tsconfig.json
```

# Files

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

```
dist
node_modules
```

--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------

```
{
  "printWidth": 100,
  "tabWidth": 2,
  "useTabs": false,
  "semi": true,
  "singleQuote": false,
  "trailingComma": "es5",
  "bracketSpacing": true,
  "arrowParens": "always",
  "ignore": ["dist/**/*"]
}

```

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

```markdown
# 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": "<YOUR_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.

```

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

```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"]
}

```

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

```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"]

```

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

```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

```

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

```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"
  }
}

```

--------------------------------------------------------------------------------
/figma_api.ts:
--------------------------------------------------------------------------------

```typescript
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<FigFile> {
  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");
}

```

--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------

```typescript
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 `<number>:<number>`",
      },
    },
    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 `<number>:<number>`",
      },
      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 `<number>`",
      },
      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<CallToolResult> {
  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<CallToolResult> {
  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<CallToolResult> {
  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<CallToolResult> {
  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<CallToolResult> {
  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");
}

```