# 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
[](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");
}
```