Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ This MCP server is specifically designed for use with Cursor. Before responding

Reducing the amount of context provided to the model helps make the AI more accurate and the responses more relevant.

## Built-in MCP Tools

The server currently exposes these tools:

- `get_figma_data` — Fetches and simplifies Figma file or node data for AI consumption.
- `download_figma_images` — Downloads node renders and image fills as local PNG/SVG assets.
- `get_node_screenshot` — Returns a single node screenshot as base64-encoded PNG image content.

## Getting Started

Many code editors and other AI clients use a configuration file to manage MCP servers.
Expand Down
10 changes: 10 additions & 0 deletions src/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { Logger } from "../utils/logger.js";
import {
downloadFigmaImagesTool,
getFigmaDataTool,
getNodeScreenshotTool,
type DownloadImagesParams,
type GetFigmaDataParams,
type GetNodeScreenshotParams,
} from "./tools/index.js";

const serverInfo = {
Expand Down Expand Up @@ -49,6 +51,14 @@ function registerTools(
getFigmaDataTool.handler(params, figmaService, options.outputFormat),
);

// Register get_node_screenshot tool
server.tool(
getNodeScreenshotTool.name,
getNodeScreenshotTool.description,
getNodeScreenshotTool.parameters,
(params: GetNodeScreenshotParams) => getNodeScreenshotTool.handler(params, figmaService),
);

// Register download_figma_images tool if CLI flag or env var is not set
if (!options.skipImageDownloads) {
server.tool(
Expand Down
82 changes: 82 additions & 0 deletions src/mcp/tools/get-node-screenshot-tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { z } from "zod";
import { FigmaService } from "../../services/figma.js";
import { Logger } from "../../utils/logger.js";

const parameters = {
nodeId: z
.string()
.regex(
/^I?\d+[:|-]\d+(?:;\d+[:|-]\d+)*$/,
"Node ID must be like '1234:5678' or 'I5666:180910;1:10515;1:10336'",
)
.describe(
'The ID of the node in the Figma document, eg. "123:456" or "123-456". This should be a valid node ID in the Figma document.',
),
fileKey: z
.string()
.regex(/^[a-zA-Z0-9]+$/, "File key must be alphanumeric")
.describe(
"The key of the Figma file to use. If the URL is provided, extract the file key from the URL. The given URL must be in the format https://figma.com/design/:fileKey/:fileName?node-id=:int1-:int2. The extracted fileKey would be :fileKey.",
),
};

const parametersSchema = z.object(parameters);
export type GetNodeScreenshotParams = z.infer<typeof parametersSchema>;

/**
* Handler function to get screenshot for a single Figma node.
*/
async function getNodeScreenshot(params: GetNodeScreenshotParams, figmaService: FigmaService) {
try {
const { nodeId: rawNodeId, fileKey } = parametersSchema.parse(params);

// Replace - with : in nodeId for our query—Figma API expects :
const nodeId = rawNodeId.replace(/-/g, ":");

Logger.log(`Getting screenshot for node ${nodeId} from file ${fileKey}`);

const imageData = await figmaService.getNodeScreenshot(fileKey, nodeId);

if (imageData) {
Logger.log(`Screenshot retrieved for node ${nodeId}`);
return {
content: [
{
type: "image" as const,
data: imageData,
mimeType: "image/png",
},
],
};
}

return {
isError: true,
content: [
{
type: "text" as const,
text: `Failed to render node ${nodeId}. The node may not exist or has no renderable content.`,
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
Logger.error("Error getting screenshot:", message);
return {
isError: true,
content: [
{
type: "text" as const,
text: `Failed to get screenshot: ${message}`,
},
],
};
}
}

export const getNodeScreenshotTool = {
name: "get_node_screenshot",
description: "Get a PNG screenshot for a specific Figma node. Requires fileKey and nodeId.",
parameters,
handler: getNodeScreenshot,
} as const;
2 changes: 2 additions & 0 deletions src/mcp/tools/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { getFigmaDataTool } from "./get-figma-data-tool.js";
export { downloadFigmaImagesTool } from "./download-figma-images-tool.js";
export { getNodeScreenshotTool } from "./get-node-screenshot-tool.js";
export type { DownloadImagesParams } from "./download-figma-images-tool.js";
export type { GetFigmaDataParams } from "./get-figma-data-tool.js";
export type { GetNodeScreenshotParams } from "./get-node-screenshot-tool.js";
37 changes: 37 additions & 0 deletions src/services/figma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,43 @@ export class FigmaService {
}
}

/**
* Gets screenshot for a node and returns as base64 encoded PNG image data.
*
* @param fileKey - The Figma file key
* @param nodeId - The node ID to render
* @returns Base64 encoded PNG image data, or null if screenshot could not be generated
*/
async getNodeScreenshot(fileKey: string, nodeId: string): Promise<string | null> {
const scale = 0.9; //reduce the size of the image

Logger.log(`Getting screenshot for node: ${nodeId} (scale: ${scale})`);

const endpoint = `/images/${fileKey}?ids=${nodeId}&format=png&scale=${scale}`;
const response = await this.request<GetImagesResponse>(endpoint);
const images = this.filterValidImages(response.images);
const imageUrl = images[nodeId];

if (!imageUrl) {
Logger.log(`No image URL returned for node ${nodeId}`);
return null;
}

Logger.log(`Downloading image from: ${imageUrl}`);
const imageResponse = await fetch(imageUrl);

if (!imageResponse.ok) {
Logger.error(`Failed to download image: ${imageResponse.statusText}`);
return null;
}

const arrayBuffer = await imageResponse.arrayBuffer();
const base64 = Buffer.from(arrayBuffer).toString("base64");

Logger.log(`Successfully downloaded and encoded image (${base64.length} chars)`);
return base64;
}

/**
* Download images method with post-processing support for cropping and returning image dimensions.
*
Expand Down
4 changes: 4 additions & 0 deletions src/tests/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe("StreamableHTTP transport", () => {
const toolNames = tools.map((t) => t.name);

expect(toolNames).toContain("get_figma_data");
expect(toolNames).toContain("get_node_screenshot");
expect(toolNames).toContain("download_figma_images");

await transport.terminateSession();
Expand Down Expand Up @@ -73,6 +74,7 @@ describe("SSE transport", () => {
const toolNames = tools.map((t) => t.name);

expect(toolNames).toContain("get_figma_data");
expect(toolNames).toContain("get_node_screenshot");

await client.close();
}, 15_000);
Expand Down Expand Up @@ -183,7 +185,9 @@ describe("Multi-client test", () => {
]);

expect(streamableTools.tools.map((t) => t.name)).toContain("get_figma_data");
expect(streamableTools.tools.map((t) => t.name)).toContain("get_node_screenshot");
expect(sseTools.tools.map((t) => t.name)).toContain("get_figma_data");
expect(sseTools.tools.map((t) => t.name)).toContain("get_node_screenshot");

// Clean up
await streamableTransport.terminateSession();
Expand Down
1 change: 1 addition & 0 deletions src/tests/stdio.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe("stdio transport", () => {
const toolNames = tools.map((t) => t.name);

expect(toolNames).toContain("get_figma_data");
expect(toolNames).toContain("get_node_screenshot");
expect(toolNames).toContain("download_figma_images");
}, 30_000);
});