server.ts•7.56 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { FigmaService } from "./services/figma.js";
import express, { Request, Response } from "express";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { IncomingMessage, ServerResponse } from "http";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import { SimplifiedDesign } from "./services/simplify-node-response.js";
export const Logger = {
log: (...args: any[]) => {},
error: (...args: any[]) => {},
};
export class FigmaMcpServer {
private readonly server: McpServer;
private readonly figmaService: FigmaService;
private sseTransport: SSEServerTransport | null = null;
constructor(figmaApiKey: string) {
this.figmaService = new FigmaService(figmaApiKey);
this.server = new McpServer(
{
name: "Figma MCP Server",
version: "0.1.12",
},
{
capabilities: {
logging: {},
tools: {},
},
},
);
this.registerTools();
}
private registerTools(): void {
// Tool to get file information
this.server.tool(
"get_figma_data",
"When the nodeId cannot be obtained, obtain the layout information about the entire Figma file",
{
fileKey: z
.string()
.describe(
"The key of the Figma file to fetch, often found in a provided URL like figma.com/(file|design)/<fileKey>/...",
),
nodeId: z
.string()
.optional()
.describe(
"The ID of the node to fetch, often found as URL parameter node-id=<nodeId>, always use if provided",
),
depth: z
.number()
.optional()
.describe(
"How many levels deep to traverse the node tree, only use if explicitly requested by the user",
),
},
async ({ fileKey, nodeId, depth }) => {
try {
Logger.log(
`Fetching ${
depth ? `${depth} layers deep` : "all layers"
} of ${nodeId ? `node ${nodeId} from file` : `full file`} ${fileKey}`,
);
let file: SimplifiedDesign;
if (nodeId) {
file = await this.figmaService.getNode(fileKey, nodeId, depth);
} else {
file = await this.figmaService.getFile(fileKey, depth);
}
Logger.log(`Successfully fetched file: ${file.name}`);
const { nodes, ...metadata } = file;
// Stringify each node individually to try to avoid max string length error with big files
const nodesJson = `[${nodes.map((node) => JSON.stringify(node, null, 2)).join(",")}]`;
const metadataJson = JSON.stringify(metadata, null, 2);
const resultJson = `{ "metadata": ${metadataJson}, "nodes": ${nodesJson} }`;
return {
content: [{ type: "text", text: resultJson }],
};
} catch (error) {
Logger.error(`Error fetching file ${fileKey}:`, error);
return {
isError: true,
content: [{ type: "text", text: `Error fetching file: ${error}` }],
};
}
},
);
// TODO: Clean up all image download related code, particularly getImages in Figma service
// Tool to download images
this.server.tool(
"download_figma_images",
"Download SVG and PNG images used in a Figma file based on the IDs of image or icon nodes",
{
fileKey: z.string().describe("The key of the Figma file containing the node"),
nodes: z
.object({
nodeId: z
.string()
.describe("The ID of the Figma image node to fetch, formatted as 1234:5678"),
imageRef: z
.string()
.optional()
.describe(
"If a node has an imageRef fill, you must include this variable. Leave blank when downloading Vector SVG images.",
),
fileName: z.string().describe("The local name for saving the fetched file"),
})
.array()
.describe("The nodes to fetch as images"),
localPath: z
.string()
.describe(
"The absolute path to the directory where images are stored in the project. Automatically creates directories if needed.",
),
},
async ({ fileKey, nodes, localPath }) => {
try {
const imageFills = nodes.filter(({ imageRef }) => !!imageRef) as {
nodeId: string;
imageRef: string;
fileName: string;
}[];
const fillDownloads = this.figmaService.getImageFills(fileKey, imageFills, localPath);
const renderRequests = nodes
.filter(({ imageRef }) => !imageRef)
.map(({ nodeId, fileName }) => ({
nodeId,
fileName,
fileType: fileName.endsWith(".svg") ? ("svg" as const) : ("png" as const),
}));
const renderDownloads = this.figmaService.getImages(fileKey, renderRequests, localPath);
const downloads = await Promise.all([fillDownloads, renderDownloads]).then(([f, r]) => [
...f,
...r,
]);
// If any download fails, return false
const saveSuccess = !downloads.find((success) => !success);
return {
content: [
{
type: "text",
text: saveSuccess
? `Success, ${downloads.length} images downloaded: ${downloads.join(", ")}`
: "Failed",
},
],
};
} catch (error) {
Logger.error(`Error downloading images from file ${fileKey}:`, error);
return {
isError: true,
content: [{ type: "text", text: `Error downloading images: ${error}` }],
};
}
},
);
}
async connect(transport: Transport): Promise<void> {
// Logger.log("Connecting to transport...");
await this.server.connect(transport);
Logger.log = (...args: any[]) => {
this.server.server.sendLoggingMessage({
level: "info",
data: args,
});
};
Logger.error = (...args: any[]) => {
this.server.server.sendLoggingMessage({
level: "error",
data: args,
});
};
Logger.log("Server connected and ready to process requests");
}
async startHttpServer(port: number): Promise<void> {
const app = express();
app.get("/sse", async (req: Request, res: Response) => {
console.log("New SSE connection established");
this.sseTransport = new SSEServerTransport(
"/messages",
res as unknown as ServerResponse<IncomingMessage>,
);
await this.server.connect(this.sseTransport);
});
app.post("/messages", async (req: Request, res: Response) => {
if (!this.sseTransport) {
res.sendStatus(400);
return;
}
await this.sseTransport.handlePostMessage(
req as unknown as IncomingMessage,
res as unknown as ServerResponse<IncomingMessage>,
);
});
Logger.log = console.log;
Logger.error = console.error;
app.listen(port, () => {
Logger.log(`HTTP server listening on port ${port}`);
Logger.log(`SSE endpoint available at http://localhost:${port}/sse`);
Logger.log(`Message endpoint available at http://localhost:${port}/messages`);
});
}
}