Figma MCP Server
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,
getThumbnailsOfCanvases,
parseKeyFromUrl,
getThumbnails,
readComments,
postComment,
replyToComment,
} from "./figma_api.js";
const server = new Server(
{
name: "figma-mcp",
version: "0.1.0",
},
{
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");
}