Skip to main content
Glama

Talk to Figma MCP

by pipethedev
server.ts89.9 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import WebSocket from "ws"; import { v4 as uuidv4 } from "uuid"; // Define TypeScript interfaces for Figma responses interface FigmaResponse { id: string; result?: any; error?: string; } // Define interface for command progress updates interface CommandProgressUpdate { type: 'command_progress'; commandId: string; commandType: string; status: 'started' | 'in_progress' | 'completed' | 'error'; progress: number; totalItems: number; processedItems: number; currentChunk?: number; totalChunks?: number; chunkSize?: number; message: string; payload?: any; timestamp: number; } // Add TypeScript interfaces for component overrides after line 21 interface ComponentOverride { id: string; overriddenFields: string[]; } // Update the getInstanceOverridesResult interface to match the plugin implementation interface getInstanceOverridesResult { success: boolean; message: string; sourceInstanceId: string; mainComponentId: string; overridesCount: number; } interface setInstanceOverridesResult { success: boolean; message: string; totalCount?: number; results?: Array<{ success: boolean; instanceId: string; instanceName: string; appliedCount?: number; message?: string; }>; } // Custom logging functions that write to stderr instead of stdout to avoid being captured const logger = { info: (message: string) => process.stderr.write(`[INFO] ${message}\n`), debug: (message: string) => process.stderr.write(`[DEBUG] ${message}\n`), warn: (message: string) => process.stderr.write(`[WARN] ${message}\n`), error: (message: string) => process.stderr.write(`[ERROR] ${message}\n`), log: (message: string) => process.stderr.write(`[LOG] ${message}\n`) }; // WebSocket connection and request tracking let ws: WebSocket | null = null; const pendingRequests = new Map<string, { resolve: (value: unknown) => void; reject: (reason: unknown) => void; timeout: ReturnType<typeof setTimeout>; lastActivity: number; // Add timestamp for last activity }>(); // Track which channel each client is in let currentChannel: string | null = null; // Create MCP server const server = new McpServer({ name: "TalkToFigmaMCP", version: "1.0.0", }); // Add command line argument parsing const args = process.argv.slice(2); const serverArg = args.find(arg => arg.startsWith('--server=')); const serverUrl = serverArg ? serverArg.split('=')[1] : 'localhost'; const WS_URL = serverUrl === 'localhost' ? `ws://${serverUrl}` : `wss://${serverUrl}`; // Document Info Tool server.tool( "get_document_info", "Get detailed information about the current Figma document", {}, async () => { try { const result = await sendCommandToFigma("get_document_info"); return { content: [ { type: "text", text: JSON.stringify(result) } ] }; } catch (error) { return { content: [ { type: "text", text: `Error getting document info: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Selection Tool server.tool( "get_selection", "Get information about the current selection in Figma", {}, async () => { try { const result = await sendCommandToFigma("get_selection"); return { content: [ { type: "text", text: JSON.stringify(result) } ] }; } catch (error) { return { content: [ { type: "text", text: `Error getting selection: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Read My Design Tool server.tool( "read_my_design", "Get detailed information about the current selection in Figma, including all node details", {}, async () => { try { const result = await sendCommandToFigma("read_my_design", {}); return { content: [ { type: "text", text: JSON.stringify(result) } ] }; } catch (error) { return { content: [ { type: "text", text: `Error getting node info: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Node Info Tool server.tool( "get_node_info", "Get detailed information about a specific node in Figma", { nodeId: z.string().describe("The ID of the node to get information about"), }, async ({ nodeId }) => { try { const result = await sendCommandToFigma("get_node_info", { nodeId }); return { content: [ { type: "text", text: JSON.stringify(filterFigmaNode(result)) } ] }; } catch (error) { return { content: [ { type: "text", text: `Error getting node info: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); function rgbaToHex(color: any): string { // skip if color is already hex if (color.startsWith('#')) { return color; } const r = Math.round(color.r * 255); const g = Math.round(color.g * 255); const b = Math.round(color.b * 255); const a = Math.round(color.a * 255); return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}${a === 255 ? '' : a.toString(16).padStart(2, '0')}`; } function filterFigmaNode(node: any) { // Skip VECTOR type nodes if (node.type === "VECTOR") { return null; } const filtered: any = { id: node.id, name: node.name, type: node.type, }; if (node.fills && node.fills.length > 0) { filtered.fills = node.fills.map((fill: any) => { const processedFill = { ...fill }; // Remove boundVariables and imageRef delete processedFill.boundVariables; delete processedFill.imageRef; // Process gradientStops if present if (processedFill.gradientStops) { processedFill.gradientStops = processedFill.gradientStops.map((stop: any) => { const processedStop = { ...stop }; // Convert color to hex if present if (processedStop.color) { processedStop.color = rgbaToHex(processedStop.color); } // Remove boundVariables delete processedStop.boundVariables; return processedStop; }); } // Convert solid fill colors to hex if (processedFill.color) { processedFill.color = rgbaToHex(processedFill.color); } return processedFill; }); } if (node.strokes && node.strokes.length > 0) { filtered.strokes = node.strokes.map((stroke: any) => { const processedStroke = { ...stroke }; // Remove boundVariables delete processedStroke.boundVariables; // Convert color to hex if present if (processedStroke.color) { processedStroke.color = rgbaToHex(processedStroke.color); } return processedStroke; }); } if (node.cornerRadius !== undefined) { filtered.cornerRadius = node.cornerRadius; } if (node.absoluteBoundingBox) { filtered.absoluteBoundingBox = node.absoluteBoundingBox; } if (node.characters) { filtered.characters = node.characters; } if (node.style) { filtered.style = { fontFamily: node.style.fontFamily, fontStyle: node.style.fontStyle, fontWeight: node.style.fontWeight, fontSize: node.style.fontSize, textAlignHorizontal: node.style.textAlignHorizontal, letterSpacing: node.style.letterSpacing, lineHeightPx: node.style.lineHeightPx }; } if (node.children) { filtered.children = node.children .map((child: any) => filterFigmaNode(child)) .filter((child: any) => child !== null); // Remove null children (VECTOR nodes) } return filtered; } // Nodes Info Tool server.tool( "get_nodes_info", "Get detailed information about multiple nodes in Figma", { nodeIds: z.array(z.string()).describe("Array of node IDs to get information about") }, async ({ nodeIds }) => { try { const results = await Promise.all( nodeIds.map(async (nodeId) => { const result = await sendCommandToFigma('get_node_info', { nodeId }); return { nodeId, info: result }; }) ); return { content: [ { type: "text", text: JSON.stringify(results.map((result) => filterFigmaNode(result.info))) } ] }; } catch (error) { return { content: [ { type: "text", text: `Error getting nodes info: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Create Rectangle Tool server.tool( "create_rectangle", "Create a new rectangle in Figma", { x: z.number().describe("X position"), y: z.number().describe("Y position"), width: z.number().describe("Width of the rectangle"), height: z.number().describe("Height of the rectangle"), name: z.string().optional().describe("Optional name for the rectangle"), parentId: z .string() .optional() .describe("Optional parent node ID to append the rectangle to"), }, async ({ x, y, width, height, name, parentId }) => { try { const result = await sendCommandToFigma("create_rectangle", { x, y, width, height, name: name || "Rectangle", parentId, }); return { content: [ { type: "text", text: `Created rectangle "${JSON.stringify(result)}"`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error creating rectangle: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Create Frame Tool server.tool( "create_frame", "Create a new frame in Figma", { x: z.number().describe("X position"), y: z.number().describe("Y position"), width: z.number().describe("Width of the frame"), height: z.number().describe("Height of the frame"), name: z.string().optional().describe("Optional name for the frame"), parentId: z .string() .optional() .describe("Optional parent node ID to append the frame to"), fillColor: z .object({ r: z.number().min(0).max(1).describe("Red component (0-1)"), g: z.number().min(0).max(1).describe("Green component (0-1)"), b: z.number().min(0).max(1).describe("Blue component (0-1)"), a: z .number() .min(0) .max(1) .optional() .describe("Alpha component (0-1)"), }) .optional() .describe("Fill color in RGBA format"), strokeColor: z .object({ r: z.number().min(0).max(1).describe("Red component (0-1)"), g: z.number().min(0).max(1).describe("Green component (0-1)"), b: z.number().min(0).max(1).describe("Blue component (0-1)"), a: z .number() .min(0) .max(1) .optional() .describe("Alpha component (0-1)"), }) .optional() .describe("Stroke color in RGBA format"), strokeWeight: z.number().positive().optional().describe("Stroke weight"), layoutMode: z.enum(["NONE", "HORIZONTAL", "VERTICAL"]).optional().describe("Auto-layout mode for the frame"), layoutWrap: z.enum(["NO_WRAP", "WRAP"]).optional().describe("Whether the auto-layout frame wraps its children"), paddingTop: z.number().optional().describe("Top padding for auto-layout frame"), paddingRight: z.number().optional().describe("Right padding for auto-layout frame"), paddingBottom: z.number().optional().describe("Bottom padding for auto-layout frame"), paddingLeft: z.number().optional().describe("Left padding for auto-layout frame"), primaryAxisAlignItems: z .enum(["MIN", "MAX", "CENTER", "SPACE_BETWEEN"]) .optional() .describe("Primary axis alignment for auto-layout frame. Note: When set to SPACE_BETWEEN, itemSpacing will be ignored as children will be evenly spaced."), counterAxisAlignItems: z.enum(["MIN", "MAX", "CENTER", "BASELINE"]).optional().describe("Counter axis alignment for auto-layout frame"), layoutSizingHorizontal: z.enum(["FIXED", "HUG", "FILL"]).optional().describe("Horizontal sizing mode for auto-layout frame"), layoutSizingVertical: z.enum(["FIXED", "HUG", "FILL"]).optional().describe("Vertical sizing mode for auto-layout frame"), itemSpacing: z .number() .optional() .describe("Distance between children in auto-layout frame. Note: This value will be ignored if primaryAxisAlignItems is set to SPACE_BETWEEN.") }, async ({ x, y, width, height, name, parentId, fillColor, strokeColor, strokeWeight, layoutMode, layoutWrap, paddingTop, paddingRight, paddingBottom, paddingLeft, primaryAxisAlignItems, counterAxisAlignItems, layoutSizingHorizontal, layoutSizingVertical, itemSpacing }) => { try { const result = await sendCommandToFigma("create_frame", { x, y, width, height, name: name || "Frame", parentId, fillColor: fillColor || { r: 1, g: 1, b: 1, a: 1 }, strokeColor: strokeColor, strokeWeight: strokeWeight, layoutMode, layoutWrap, paddingTop, paddingRight, paddingBottom, paddingLeft, primaryAxisAlignItems, counterAxisAlignItems, layoutSizingHorizontal, layoutSizingVertical, itemSpacing }); const typedResult = result as { name: string; id: string }; return { content: [ { type: "text", text: `Created frame "${typedResult.name}" with ID: ${typedResult.id}. Use the ID as the parentId to appendChild inside this frame.`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error creating frame: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Create Text Tool server.tool( "create_text", "Create a new text element in Figma", { x: z.number().describe("X position"), y: z.number().describe("Y position"), text: z.string().describe("Text content"), fontSize: z.number().optional().describe("Font size (default: 14)"), fontWeight: z .number() .optional() .describe("Font weight (e.g., 400 for Regular, 700 for Bold)"), fontColor: z .object({ r: z.number().min(0).max(1).describe("Red component (0-1)"), g: z.number().min(0).max(1).describe("Green component (0-1)"), b: z.number().min(0).max(1).describe("Blue component (0-1)"), a: z .number() .min(0) .max(1) .optional() .describe("Alpha component (0-1)"), }) .optional() .describe("Font color in RGBA format"), name: z .string() .optional() .describe("Semantic layer name for the text node"), parentId: z .string() .optional() .describe("Optional parent node ID to append the text to"), }, async ({ x, y, text, fontSize, fontWeight, fontColor, name, parentId }) => { try { const result = await sendCommandToFigma("create_text", { x, y, text, fontSize: fontSize || 14, fontWeight: fontWeight || 400, fontColor: fontColor || { r: 0, g: 0, b: 0, a: 1 }, name: name || "Text", parentId, }); const typedResult = result as { name: string; id: string }; return { content: [ { type: "text", text: `Created text "${typedResult.name}" with ID: ${typedResult.id}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error creating text: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Set Fill Color Tool server.tool( "set_fill_color", "Set the fill color of a node in Figma can be TextNode or FrameNode", { nodeId: z.string().describe("The ID of the node to modify"), r: z.number().min(0).max(1).describe("Red component (0-1)"), g: z.number().min(0).max(1).describe("Green component (0-1)"), b: z.number().min(0).max(1).describe("Blue component (0-1)"), a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)"), }, async ({ nodeId, r, g, b, a }) => { try { const result = await sendCommandToFigma("set_fill_color", { nodeId, color: { r, g, b, a: a || 1 }, }); const typedResult = result as { name: string }; return { content: [ { type: "text", text: `Set fill color of node "${typedResult.name }" to RGBA(${r}, ${g}, ${b}, ${a || 1})`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error setting fill color: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Set Stroke Color Tool server.tool( "set_stroke_color", "Set the stroke color of a node in Figma", { nodeId: z.string().describe("The ID of the node to modify"), r: z.number().min(0).max(1).describe("Red component (0-1)"), g: z.number().min(0).max(1).describe("Green component (0-1)"), b: z.number().min(0).max(1).describe("Blue component (0-1)"), a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)"), weight: z.number().positive().optional().describe("Stroke weight"), }, async ({ nodeId, r, g, b, a, weight }) => { try { const result = await sendCommandToFigma("set_stroke_color", { nodeId, color: { r, g, b, a: a || 1 }, weight: weight || 1, }); const typedResult = result as { name: string }; return { content: [ { type: "text", text: `Set stroke color of node "${typedResult.name }" to RGBA(${r}, ${g}, ${b}, ${a || 1}) with weight ${weight || 1}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error setting stroke color: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Move Node Tool server.tool( "move_node", "Move a node to a new position in Figma", { nodeId: z.string().describe("The ID of the node to move"), x: z.number().describe("New X position"), y: z.number().describe("New Y position"), }, async ({ nodeId, x, y }) => { try { const result = await sendCommandToFigma("move_node", { nodeId, x, y }); const typedResult = result as { name: string }; return { content: [ { type: "text", text: `Moved node "${typedResult.name}" to position (${x}, ${y})`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error moving node: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Clone Node Tool server.tool( "clone_node", "Clone an existing node in Figma", { nodeId: z.string().describe("The ID of the node to clone"), x: z.number().optional().describe("New X position for the clone"), y: z.number().optional().describe("New Y position for the clone") }, async ({ nodeId, x, y }) => { try { const result = await sendCommandToFigma('clone_node', { nodeId, x, y }); const typedResult = result as { name: string, id: string }; return { content: [ { type: "text", text: `Cloned node "${typedResult.name}" with new ID: ${typedResult.id}${x !== undefined && y !== undefined ? ` at position (${x}, ${y})` : ''}` } ] }; } catch (error) { return { content: [ { type: "text", text: `Error cloning node: ${error instanceof Error ? error.message : String(error)}` } ] }; } } ); // Resize Node Tool server.tool( "resize_node", "Resize a node in Figma", { nodeId: z.string().describe("The ID of the node to resize"), width: z.number().positive().describe("New width"), height: z.number().positive().describe("New height"), }, async ({ nodeId, width, height }) => { try { const result = await sendCommandToFigma("resize_node", { nodeId, width, height, }); const typedResult = result as { name: string }; return { content: [ { type: "text", text: `Resized node "${typedResult.name}" to width ${width} and height ${height}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error resizing node: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Delete Node Tool server.tool( "delete_node", "Delete a node from Figma", { nodeId: z.string().describe("The ID of the node to delete"), }, async ({ nodeId }) => { try { await sendCommandToFigma("delete_node", { nodeId }); return { content: [ { type: "text", text: `Deleted node with ID: ${nodeId}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error deleting node: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Delete Multiple Nodes Tool server.tool( "delete_multiple_nodes", "Delete multiple nodes from Figma at once", { nodeIds: z.array(z.string()).describe("Array of node IDs to delete"), }, async ({ nodeIds }) => { try { const result = await sendCommandToFigma("delete_multiple_nodes", { nodeIds }); return { content: [ { type: "text", text: JSON.stringify(result) } ] }; } catch (error) { return { content: [ { type: "text", text: `Error deleting multiple nodes: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Export Node as Image Tool server.tool( "export_node_as_image", "Export a node as an image from Figma", { nodeId: z.string().describe("The ID of the node to export"), format: z .enum(["PNG", "JPG", "SVG", "PDF"]) .optional() .describe("Export format"), scale: z.number().positive().optional().describe("Export scale"), }, async ({ nodeId, format, scale }) => { try { const result = await sendCommandToFigma("export_node_as_image", { nodeId, format: format || "PNG", scale: scale || 1, }); const typedResult = result as { imageData: string; mimeType: string }; return { content: [ { type: "image", data: typedResult.imageData, mimeType: typedResult.mimeType || "image/png", }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error exporting node as image: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Set Text Content Tool server.tool( "set_text_content", "Set the text content of an existing text node in Figma", { nodeId: z.string().describe("The ID of the text node to modify"), text: z.string().describe("New text content"), }, async ({ nodeId, text }) => { try { const result = await sendCommandToFigma("set_text_content", { nodeId, text, }); const typedResult = result as { name: string }; return { content: [ { type: "text", text: `Updated text content of node "${typedResult.name}" to "${text}"`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error setting text content: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Get Styles Tool server.tool( "get_styles", "Get all styles from the current Figma document", {}, async () => { try { const result = await sendCommandToFigma("get_styles"); return { content: [ { type: "text", text: JSON.stringify(result) } ] }; } catch (error) { return { content: [ { type: "text", text: `Error getting styles: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Get Local Components Tool server.tool( "get_local_components", "Get all local components from the Figma document", {}, async () => { try { const result = await sendCommandToFigma("get_local_components"); return { content: [ { type: "text", text: JSON.stringify(result) } ] }; } catch (error) { return { content: [ { type: "text", text: `Error getting local components: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Get Annotations Tool server.tool( "get_annotations", "Get all annotations in the current document or specific node", { nodeId: z.string().optional().describe("Optional node ID to get annotations for specific node"), includeCategories: z.boolean().optional().default(true).describe("Whether to include category information") }, async ({ nodeId, includeCategories }) => { try { const result = await sendCommandToFigma("get_annotations", { nodeId, includeCategories }); return { content: [ { type: "text", text: JSON.stringify(result) } ] }; } catch (error) { return { content: [ { type: "text", text: `Error getting annotations: ${error instanceof Error ? error.message : String(error)}` } ] }; } } ); // Set Annotation Tool server.tool( "set_annotation", "Create or update an annotation", { nodeId: z.string().describe("The ID of the node to annotate"), annotationId: z.string().optional().describe("The ID of the annotation to update (if updating existing annotation)"), labelMarkdown: z.string().describe("The annotation text in markdown format"), categoryId: z.string().optional().describe("The ID of the annotation category"), properties: z.array(z.object({ type: z.string() })).optional().describe("Additional properties for the annotation") }, async ({ nodeId, annotationId, labelMarkdown, categoryId, properties }) => { try { const result = await sendCommandToFigma("set_annotation", { nodeId, annotationId, labelMarkdown, categoryId, properties }); return { content: [ { type: "text", text: JSON.stringify(result) } ] }; } catch (error) { return { content: [ { type: "text", text: `Error setting annotation: ${error instanceof Error ? error.message : String(error)}` } ] }; } } ); interface SetMultipleAnnotationsParams { nodeId: string; annotations: Array<{ nodeId: string; labelMarkdown: string; categoryId?: string; annotationId?: string; properties?: Array<{ type: string }>; }>; } // Set Multiple Annotations Tool server.tool( "set_multiple_annotations", "Set multiple annotations parallelly in a node", { nodeId: z .string() .describe("The ID of the node containing the elements to annotate"), annotations: z .array( z.object({ nodeId: z.string().describe("The ID of the node to annotate"), labelMarkdown: z.string().describe("The annotation text in markdown format"), categoryId: z.string().optional().describe("The ID of the annotation category"), annotationId: z.string().optional().describe("The ID of the annotation to update (if updating existing annotation)"), properties: z.array(z.object({ type: z.string() })).optional().describe("Additional properties for the annotation") }) ) .describe("Array of annotations to apply"), }, async ({ nodeId, annotations }, extra) => { try { if (!annotations || annotations.length === 0) { return { content: [ { type: "text", text: "No annotations provided", }, ], }; } // Initial response to indicate we're starting the process const initialStatus = { type: "text" as const, text: `Starting annotation process for ${annotations.length} nodes. This will be processed in batches of 5...`, }; // Track overall progress let totalProcessed = 0; const totalToProcess = annotations.length; // Use the plugin's set_multiple_annotations function with chunking const result = await sendCommandToFigma("set_multiple_annotations", { nodeId, annotations, }); // Cast the result to a specific type to work with it safely interface AnnotationResult { success: boolean; nodeId: string; annotationsApplied?: number; annotationsFailed?: number; totalAnnotations?: number; completedInChunks?: number; results?: Array<{ success: boolean; nodeId: string; error?: string; annotationId?: string; }>; } const typedResult = result as AnnotationResult; // Format the results for display const success = typedResult.annotationsApplied && typedResult.annotationsApplied > 0; const progressText = ` Annotation process completed: - ${typedResult.annotationsApplied || 0} of ${totalToProcess} successfully applied - ${typedResult.annotationsFailed || 0} failed - Processed in ${typedResult.completedInChunks || 1} batches `; // Detailed results const detailedResults = typedResult.results || []; const failedResults = detailedResults.filter(item => !item.success); // Create the detailed part of the response let detailedResponse = ""; if (failedResults.length > 0) { detailedResponse = `\n\nNodes that failed:\n${failedResults.map(item => `- ${item.nodeId}: ${item.error || "Unknown error"}` ).join('\n')}`; } return { content: [ initialStatus, { type: "text" as const, text: progressText + detailedResponse, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error setting multiple annotations: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Create Component Instance Tool server.tool( "create_component_instance", "Create an instance of a component in Figma", { componentKey: z.string().describe("Key of the component to instantiate"), x: z.number().describe("X position"), y: z.number().describe("Y position"), }, async ({ componentKey, x, y }) => { try { const result = await sendCommandToFigma("create_component_instance", { componentKey, x, y, }); const typedResult = result as any; return { content: [ { type: "text", text: JSON.stringify(typedResult), } ] } } catch (error) { return { content: [ { type: "text", text: `Error creating component instance: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Copy Instance Overrides Tool server.tool( "get_instance_overrides", "Get all override properties from a selected component instance. These overrides can be applied to other instances, which will swap them to match the source component.", { nodeId: z.string().optional().describe("Optional ID of the component instance to get overrides from. If not provided, currently selected instance will be used."), }, async ({ nodeId }) => { try { const result = await sendCommandToFigma("get_instance_overrides", { instanceNodeId: nodeId || null }); const typedResult = result as getInstanceOverridesResult; return { content: [ { type: "text", text: typedResult.success ? `Successfully got instance overrides: ${typedResult.message}` : `Failed to get instance overrides: ${typedResult.message}` } ] }; } catch (error) { return { content: [ { type: "text", text: `Error copying instance overrides: ${error instanceof Error ? error.message : String(error)}` } ] }; } } ); // Set Instance Overrides Tool server.tool( "set_instance_overrides", "Apply previously copied overrides to selected component instances. Target instances will be swapped to the source component and all copied override properties will be applied.", { sourceInstanceId: z.string().describe("ID of the source component instance"), targetNodeIds: z.array(z.string()).describe("Array of target instance IDs. Currently selected instances will be used.") }, async ({ sourceInstanceId, targetNodeIds }) => { try { const result = await sendCommandToFigma("set_instance_overrides", { sourceInstanceId: sourceInstanceId, targetNodeIds: targetNodeIds || [] }); const typedResult = result as setInstanceOverridesResult; if (typedResult.success) { const successCount = typedResult.results?.filter(r => r.success).length || 0; return { content: [ { type: "text", text: `Successfully applied ${typedResult.totalCount || 0} overrides to ${successCount} instances.` } ] }; } else { return { content: [ { type: "text", text: `Failed to set instance overrides: ${typedResult.message}` } ] }; } } catch (error) { return { content: [ { type: "text", text: `Error setting instance overrides: ${error instanceof Error ? error.message : String(error)}` } ] }; } } ); // Set Corner Radius Tool server.tool( "set_corner_radius", "Set the corner radius of a node in Figma", { nodeId: z.string().describe("The ID of the node to modify"), radius: z.number().min(0).describe("Corner radius value"), corners: z .array(z.boolean()) .length(4) .optional() .describe( "Optional array of 4 booleans to specify which corners to round [topLeft, topRight, bottomRight, bottomLeft]" ), }, async ({ nodeId, radius, corners }) => { try { const result = await sendCommandToFigma("set_corner_radius", { nodeId, radius, corners: corners || [true, true, true, true], }); const typedResult = result as { name: string }; return { content: [ { type: "text", text: `Set corner radius of node "${typedResult.name}" to ${radius}px`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error setting corner radius: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Define design strategy prompt server.prompt( "design_strategy", "Best practices for working with Figma designs", (extra) => { return { messages: [ { role: "assistant", content: { type: "text", text: `When working with Figma designs, follow these best practices: 1. Start with Document Structure: - First use get_document_info() to understand the current document - Plan your layout hierarchy before creating elements - Create a main container frame for each screen/section 2. Naming Conventions: - Use descriptive, semantic names for all elements - Follow a consistent naming pattern (e.g., "Login Screen", "Logo Container", "Email Input") - Group related elements with meaningful names 3. Layout Hierarchy: - Create parent frames first, then add child elements - For forms/login screens: * Start with the main screen container frame * Create a logo container at the top * Group input fields in their own containers * Place action buttons (login, submit) after inputs * Add secondary elements (forgot password, signup links) last 4. Input Fields Structure: - Create a container frame for each input field - Include a label text above or inside the input - Group related inputs (e.g., username/password) together 5. Element Creation: - Use create_frame() for containers and input fields - Use create_text() for labels, buttons text, and links - Set appropriate colors and styles: * Use fillColor for backgrounds * Use strokeColor for borders * Set proper fontWeight for different text elements 6. Mofifying existing elements: - use set_text_content() to modify text content. 7. Visual Hierarchy: - Position elements in logical reading order (top to bottom) - Maintain consistent spacing between elements - Use appropriate font sizes for different text types: * Larger for headings/welcome text * Medium for input labels * Standard for button text * Smaller for helper text/links 8. Best Practices: - Verify each creation with get_node_info() - Use parentId to maintain proper hierarchy - Group related elements together in frames - Keep consistent spacing and alignment Example Login Screen Structure: - Login Screen (main frame) - Logo Container (frame) - Logo (image/text) - Welcome Text (text) - Input Container (frame) - Email Input (frame) - Email Label (text) - Email Field (frame) - Password Input (frame) - Password Label (text) - Password Field (frame) - Login Button (frame) - Button Text (text) - Helper Links (frame) - Forgot Password (text) - Don't have account (text)`, }, }, ], description: "Best practices for working with Figma designs", }; } ); server.prompt( "read_design_strategy", "Best practices for reading Figma designs", (extra) => { return { messages: [ { role: "assistant", content: { type: "text", text: `When reading Figma designs, follow these best practices: 1. Start with selection: - First use read_my_design() to understand the current selection - If no selection ask user to select single or multiple nodes `, }, }, ], description: "Best practices for reading Figma designs", }; } ); // Text Node Scanning Tool server.tool( "scan_text_nodes", "Scan all text nodes in the selected Figma node", { nodeId: z.string().describe("ID of the node to scan"), }, async ({ nodeId }) => { try { // Initial response to indicate we're starting the process const initialStatus = { type: "text" as const, text: "Starting text node scanning. This may take a moment for large designs...", }; // Use the plugin's scan_text_nodes function with chunking flag const result = await sendCommandToFigma("scan_text_nodes", { nodeId, useChunking: true, // Enable chunking on the plugin side chunkSize: 10 // Process 10 nodes at a time }); // If the result indicates chunking was used, format the response accordingly if (result && typeof result === 'object' && 'chunks' in result) { const typedResult = result as { success: boolean, totalNodes: number, processedNodes: number, chunks: number, textNodes: Array<any> }; const summaryText = ` Scan completed: - Found ${typedResult.totalNodes} text nodes - Processed in ${typedResult.chunks} chunks `; return { content: [ initialStatus, { type: "text" as const, text: summaryText }, { type: "text" as const, text: JSON.stringify(typedResult.textNodes, null, 2) } ], }; } // If chunking wasn't used or wasn't reported in the result format, return the result as is return { content: [ initialStatus, { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error scanning text nodes: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Node Type Scanning Tool server.tool( "scan_nodes_by_types", "Scan for child nodes with specific types in the selected Figma node", { nodeId: z.string().describe("ID of the node to scan"), types: z.array(z.string()).describe("Array of node types to find in the child nodes (e.g. ['COMPONENT', 'FRAME'])") }, async ({ nodeId, types }) => { try { // Initial response to indicate we're starting the process const initialStatus = { type: "text" as const, text: `Starting node type scanning for types: ${types.join(', ')}...`, }; // Use the plugin's scan_nodes_by_types function const result = await sendCommandToFigma("scan_nodes_by_types", { nodeId, types }); // Format the response if (result && typeof result === 'object' && 'matchingNodes' in result) { const typedResult = result as { success: boolean, count: number, matchingNodes: Array<{ id: string, name: string, type: string, bbox: { x: number, y: number, width: number, height: number } }>, searchedTypes: Array<string> }; const summaryText = `Scan completed: Found ${typedResult.count} nodes matching types: ${typedResult.searchedTypes.join(', ')}`; return { content: [ initialStatus, { type: "text" as const, text: summaryText }, { type: "text" as const, text: JSON.stringify(typedResult.matchingNodes, null, 2) } ], }; } // If the result is in an unexpected format, return it as is return { content: [ initialStatus, { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error scanning nodes by types: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Text Replacement Strategy Prompt server.prompt( "text_replacement_strategy", "Systematic approach for replacing text in Figma designs", (extra) => { return { messages: [ { role: "assistant", content: { type: "text", text: `# Intelligent Text Replacement Strategy ## 1. Analyze Design & Identify Structure - Scan text nodes to understand the overall structure of the design - Use AI pattern recognition to identify logical groupings: * Tables (rows, columns, headers, cells) * Lists (items, headers, nested lists) * Card groups (similar cards with recurring text fields) * Forms (labels, input fields, validation text) * Navigation (menu items, breadcrumbs) \`\`\` scan_text_nodes(nodeId: "node-id") get_node_info(nodeId: "node-id") // optional \`\`\` ## 2. Strategic Chunking for Complex Designs - Divide replacement tasks into logical content chunks based on design structure - Use one of these chunking strategies that best fits the design: * **Structural Chunking**: Table rows/columns, list sections, card groups * **Spatial Chunking**: Top-to-bottom, left-to-right in screen areas * **Semantic Chunking**: Content related to the same topic or functionality * **Component-Based Chunking**: Process similar component instances together ## 3. Progressive Replacement with Verification - Create a safe copy of the node for text replacement - Replace text chunk by chunk with continuous progress updates - After each chunk is processed: * Export that section as a small, manageable image * Verify text fits properly and maintain design integrity * Fix issues before proceeding to the next chunk \`\`\` // Clone the node to create a safe copy clone_node(nodeId: "selected-node-id", x: [new-x], y: [new-y]) // Replace text chunk by chunk set_multiple_text_contents( nodeId: "parent-node-id", text: [ { nodeId: "node-id-1", text: "New text 1" }, // More nodes in this chunk... ] ) // Verify chunk with small, targeted image exports export_node_as_image(nodeId: "chunk-node-id", format: "PNG", scale: 0.5) \`\`\` ## 4. Intelligent Handling for Table Data - For tabular content: * Process one row or column at a time * Maintain alignment and spacing between cells * Consider conditional formatting based on cell content * Preserve header/data relationships ## 5. Smart Text Adaptation - Adaptively handle text based on container constraints: * Auto-detect space constraints and adjust text length * Apply line breaks at appropriate linguistic points * Maintain text hierarchy and emphasis * Consider font scaling for critical content that must fit ## 6. Progressive Feedback Loop - Establish a continuous feedback loop during replacement: * Real-time progress updates (0-100%) * Small image exports after each chunk for verification * Issues identified early and resolved incrementally * Quick adjustments applied to subsequent chunks ## 7. Final Verification & Context-Aware QA - After all chunks are processed: * Export the entire design at reduced scale for final verification * Check for cross-chunk consistency issues * Verify proper text flow between different sections * Ensure design harmony across the full composition ## 8. Chunk-Specific Export Scale Guidelines - Scale exports appropriately based on chunk size: * Small chunks (1-5 elements): scale 1.0 * Medium chunks (6-20 elements): scale 0.7 * Large chunks (21-50 elements): scale 0.5 * Very large chunks (50+ elements): scale 0.3 * Full design verification: scale 0.2 ## Sample Chunking Strategy for Common Design Types ### Tables - Process by logical rows (5-10 rows per chunk) - Alternative: Process by column for columnar analysis - Tip: Always include header row in first chunk for reference ### Card Lists - Group 3-5 similar cards per chunk - Process entire cards to maintain internal consistency - Verify text-to-image ratio within cards after each chunk ### Forms - Group related fields (e.g., "Personal Information", "Payment Details") - Process labels and input fields together - Ensure validation messages and hints are updated with their fields ### Navigation & Menus - Process hierarchical levels together (main menu, submenu) - Respect information architecture relationships - Verify menu fit and alignment after replacement ## Best Practices - **Preserve Design Intent**: Always prioritize design integrity - **Structural Consistency**: Maintain alignment, spacing, and hierarchy - **Visual Feedback**: Verify each chunk visually before proceeding - **Incremental Improvement**: Learn from each chunk to improve subsequent ones - **Balance Automation & Control**: Let AI handle repetitive replacements but maintain oversight - **Respect Content Relationships**: Keep related content consistent across chunks Remember that text is never just text—it's a core design element that must work harmoniously with the overall composition. This chunk-based strategy allows you to methodically transform text while maintaining design integrity.`, }, }, ], description: "Systematic approach for replacing text in Figma designs", }; } ); // Set Multiple Text Contents Tool server.tool( "set_multiple_text_contents", "Set multiple text contents parallelly in a node", { nodeId: z .string() .describe("The ID of the node containing the text nodes to replace"), text: z .array( z.object({ nodeId: z.string().describe("The ID of the text node"), text: z.string().describe("The replacement text"), }) ) .describe("Array of text node IDs and their replacement texts"), }, async ({ nodeId, text }, extra) => { try { if (!text || text.length === 0) { return { content: [ { type: "text", text: "No text provided", }, ], }; } // Initial response to indicate we're starting the process const initialStatus = { type: "text" as const, text: `Starting text replacement for ${text.length} nodes. This will be processed in batches of 5...`, }; // Track overall progress let totalProcessed = 0; const totalToProcess = text.length; // Use the plugin's set_multiple_text_contents function with chunking const result = await sendCommandToFigma("set_multiple_text_contents", { nodeId, text, }); // Cast the result to a specific type to work with it safely interface TextReplaceResult { success: boolean; nodeId: string; replacementsApplied?: number; replacementsFailed?: number; totalReplacements?: number; completedInChunks?: number; results?: Array<{ success: boolean; nodeId: string; error?: string; originalText?: string; translatedText?: string; }>; } const typedResult = result as TextReplaceResult; // Format the results for display const success = typedResult.replacementsApplied && typedResult.replacementsApplied > 0; const progressText = ` Text replacement completed: - ${typedResult.replacementsApplied || 0} of ${totalToProcess} successfully updated - ${typedResult.replacementsFailed || 0} failed - Processed in ${typedResult.completedInChunks || 1} batches `; // Detailed results const detailedResults = typedResult.results || []; const failedResults = detailedResults.filter(item => !item.success); // Create the detailed part of the response let detailedResponse = ""; if (failedResults.length > 0) { detailedResponse = `\n\nNodes that failed:\n${failedResults.map(item => `- ${item.nodeId}: ${item.error || "Unknown error"}` ).join('\n')}`; } return { content: [ initialStatus, { type: "text" as const, text: progressText + detailedResponse, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error setting multiple text contents: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Annotation Conversion Strategy Prompt server.prompt( "annotation_conversion_strategy", "Strategy for converting manual annotations to Figma's native annotations", (extra) => { return { messages: [ { role: "assistant", content: { type: "text", text: `# Automatic Annotation Conversion ## Process Overview The process of converting manual annotations (numbered/alphabetical indicators with connected descriptions) to Figma's native annotations: 1. Get selected frame/component information 2. Scan and collect all annotation text nodes 3. Scan target UI elements (components, instances, frames) 4. Match annotations to appropriate UI elements 5. Apply native Figma annotations ## Step 1: Get Selection and Initial Setup First, get the selected frame or component that contains annotations: \`\`\`typescript // Get the selected frame/component const selection = await get_selection(); const selectedNodeId = selection[0].id // Get available annotation categories for later use const annotationData = await get_annotations({ nodeId: selectedNodeId, includeCategories: true }); const categories = annotationData.categories; \`\`\` ## Step 2: Scan Annotation Text Nodes Scan all text nodes to identify annotations and their descriptions: \`\`\`typescript // Get all text nodes in the selection const textNodes = await scan_text_nodes({ nodeId: selectedNodeId }); // Filter and group annotation markers and descriptions // Markers typically have these characteristics: // - Short text content (usually single digit/letter) // - Specific font styles (often bold) // - Located in a container with "Marker" or "Dot" in the name // - Have a clear naming pattern (e.g., "1", "2", "3" or "A", "B", "C") // Identify description nodes // Usually longer text nodes near markers or with matching numbers in path \`\`\` ## Step 3: Scan Target UI Elements Get all potential target elements that annotations might refer to: \`\`\`typescript // Scan for all UI elements that could be annotation targets const targetNodes = await scan_nodes_by_types({ nodeId: selectedNodeId, types: [ "COMPONENT", "INSTANCE", "FRAME" ] }); \`\`\` ## Step 4: Match Annotations to Targets Match each annotation to its target UI element using these strategies in order of priority: 1. **Path-Based Matching**: - Look at the marker's parent container name in the Figma layer hierarchy - Remove any "Marker:" or "Annotation:" prefixes from the parent name - Find UI elements that share the same parent name or have it in their path - This works well when markers are grouped with their target elements 2. **Name-Based Matching**: - Extract key terms from the annotation description - Look for UI elements whose names contain these key terms - Consider both exact matches and semantic similarities - Particularly effective for form fields, buttons, and labeled components 3. **Proximity-Based Matching** (fallback): - Calculate the center point of the marker - Find the closest UI element by measuring distances to element centers - Consider the marker's position relative to nearby elements - Use this method when other matching strategies fail Additional Matching Considerations: - Give higher priority to matches found through path-based matching - Consider the type of UI element when evaluating matches - Take into account the annotation's context and content - Use a combination of strategies for more accurate matching ## Step 5: Apply Native Annotations Convert matched annotations to Figma's native annotations using batch processing: \`\`\`typescript // Prepare annotations array for batch processing const annotationsToApply = Object.values(annotations).map(({ marker, description }) => { // Find target using multiple strategies const target = findTargetByPath(marker, targetNodes) || findTargetByName(description, targetNodes) || findTargetByProximity(marker, targetNodes); if (target) { // Determine appropriate category based on content const category = determineCategory(description.characters, categories); // Determine appropriate additional annotationProperty based on content const annotationProperty = determineProperties(description.characters, target.type); return { nodeId: target.id, labelMarkdown: description.characters, categoryId: category.id, properties: annotationProperty }; } return null; }).filter(Boolean); // Remove null entries // Apply annotations in batches using set_multiple_annotations if (annotationsToApply.length > 0) { await set_multiple_annotations({ nodeId: selectedNodeId, annotations: annotationsToApply }); } \`\`\` This strategy focuses on practical implementation based on real-world usage patterns, emphasizing the importance of handling various UI elements as annotation targets, not just text nodes.` }, }, ], description: "Strategy for converting manual annotations to Figma's native annotations", }; } ); // Instance Slot Filling Strategy Prompt server.prompt( "swap_overrides_instances", "Guide to swap instance overrides between instances", (extra) => { return { messages: [ { role: "assistant", content: { type: "text", text: `# Swap Component Instance and Override Strategy ## Overview This strategy enables transferring content and property overrides from a source instance to one or more target instances in Figma, maintaining design consistency while reducing manual work. ## Step-by-Step Process ### 1. Selection Analysis - Use \`get_selection()\` to identify the parent component or selected instances - For parent components, scan for instances with \`scan_nodes_by_types({ nodeId: "parent-id", types: ["INSTANCE"] })\` - Identify custom slots by name patterns (e.g. "Custom Slot*" or "Instance Slot") or by examining text content - Determine which is the source instance (with content to copy) and which are targets (where to apply content) ### 2. Extract Source Overrides - Use \`get_instance_overrides()\` to extract customizations from the source instance - This captures text content, property values, and style overrides - Command syntax: \`get_instance_overrides({ nodeId: "source-instance-id" })\` - Look for successful response like "Got component information from [instance name]" ### 3. Apply Overrides to Targets - Apply captured overrides using \`set_instance_overrides()\` - Command syntax: \`\`\` set_instance_overrides({ sourceInstanceId: "source-instance-id", targetNodeIds: ["target-id-1", "target-id-2", ...] }) \`\`\` ### 4. Verification - Verify results with \`get_node_info()\` or \`read_my_design()\` - Confirm text content and style overrides have transferred successfully ## Key Tips - Always join the appropriate channel first with \`join_channel()\` - When working with multiple targets, check the full selection with \`get_selection()\` - Preserve component relationships by using instance overrides rather than direct text manipulation`, }, }, ], description: "Strategy for transferring overrides between component instances in Figma", }; } ); // Set Layout Mode Tool server.tool( "set_layout_mode", "Set the layout mode and wrap behavior of a frame in Figma", { nodeId: z.string().describe("The ID of the frame to modify"), layoutMode: z.enum(["NONE", "HORIZONTAL", "VERTICAL"]).describe("Layout mode for the frame"), layoutWrap: z.enum(["NO_WRAP", "WRAP"]).optional().describe("Whether the auto-layout frame wraps its children") }, async ({ nodeId, layoutMode, layoutWrap }) => { try { const result = await sendCommandToFigma("set_layout_mode", { nodeId, layoutMode, layoutWrap: layoutWrap || "NO_WRAP" }); const typedResult = result as { name: string }; return { content: [ { type: "text", text: `Set layout mode of frame "${typedResult.name}" to ${layoutMode}${layoutWrap ? ` with ${layoutWrap}` : ''}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error setting layout mode: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); // Set Padding Tool server.tool( "set_padding", "Set padding values for an auto-layout frame in Figma", { nodeId: z.string().describe("The ID of the frame to modify"), paddingTop: z.number().optional().describe("Top padding value"), paddingRight: z.number().optional().describe("Right padding value"), paddingBottom: z.number().optional().describe("Bottom padding value"), paddingLeft: z.number().optional().describe("Left padding value"), }, async ({ nodeId, paddingTop, paddingRight, paddingBottom, paddingLeft }) => { try { const result = await sendCommandToFigma("set_padding", { nodeId, paddingTop, paddingRight, paddingBottom, paddingLeft, }); const typedResult = result as { name: string }; // Create a message about which padding values were set const paddingMessages = []; if (paddingTop !== undefined) paddingMessages.push(`top: ${paddingTop}`); if (paddingRight !== undefined) paddingMessages.push(`right: ${paddingRight}`); if (paddingBottom !== undefined) paddingMessages.push(`bottom: ${paddingBottom}`); if (paddingLeft !== undefined) paddingMessages.push(`left: ${paddingLeft}`); const paddingText = paddingMessages.length > 0 ? `padding (${paddingMessages.join(', ')})` : "padding"; return { content: [ { type: "text", text: `Set ${paddingText} for frame "${typedResult.name}"`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error setting padding: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); // Set Axis Align Tool server.tool( "set_axis_align", "Set primary and counter axis alignment for an auto-layout frame in Figma", { nodeId: z.string().describe("The ID of the frame to modify"), primaryAxisAlignItems: z .enum(["MIN", "MAX", "CENTER", "SPACE_BETWEEN"]) .optional() .describe("Primary axis alignment (MIN/MAX = left/right in horizontal, top/bottom in vertical). Note: When set to SPACE_BETWEEN, itemSpacing will be ignored as children will be evenly spaced."), counterAxisAlignItems: z .enum(["MIN", "MAX", "CENTER", "BASELINE"]) .optional() .describe("Counter axis alignment (MIN/MAX = top/bottom in horizontal, left/right in vertical)") }, async ({ nodeId, primaryAxisAlignItems, counterAxisAlignItems }) => { try { const result = await sendCommandToFigma("set_axis_align", { nodeId, primaryAxisAlignItems, counterAxisAlignItems }); const typedResult = result as { name: string }; // Create a message about which alignments were set const alignMessages = []; if (primaryAxisAlignItems !== undefined) alignMessages.push(`primary: ${primaryAxisAlignItems}`); if (counterAxisAlignItems !== undefined) alignMessages.push(`counter: ${counterAxisAlignItems}`); const alignText = alignMessages.length > 0 ? `axis alignment (${alignMessages.join(', ')})` : "axis alignment"; return { content: [ { type: "text", text: `Set ${alignText} for frame "${typedResult.name}"`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error setting axis alignment: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); // Set Layout Sizing Tool server.tool( "set_layout_sizing", "Set horizontal and vertical sizing modes for an auto-layout frame in Figma", { nodeId: z.string().describe("The ID of the frame to modify"), layoutSizingHorizontal: z .enum(["FIXED", "HUG", "FILL"]) .optional() .describe("Horizontal sizing mode (HUG for frames/text only, FILL for auto-layout children only)"), layoutSizingVertical: z .enum(["FIXED", "HUG", "FILL"]) .optional() .describe("Vertical sizing mode (HUG for frames/text only, FILL for auto-layout children only)") }, async ({ nodeId, layoutSizingHorizontal, layoutSizingVertical }) => { try { const result = await sendCommandToFigma("set_layout_sizing", { nodeId, layoutSizingHorizontal, layoutSizingVertical }); const typedResult = result as { name: string }; // Create a message about which sizing modes were set const sizingMessages = []; if (layoutSizingHorizontal !== undefined) sizingMessages.push(`horizontal: ${layoutSizingHorizontal}`); if (layoutSizingVertical !== undefined) sizingMessages.push(`vertical: ${layoutSizingVertical}`); const sizingText = sizingMessages.length > 0 ? `layout sizing (${sizingMessages.join(', ')})` : "layout sizing"; return { content: [ { type: "text", text: `Set ${sizingText} for frame "${typedResult.name}"`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error setting layout sizing: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); // Set Item Spacing Tool server.tool( "set_item_spacing", "Set distance between children in an auto-layout frame", { nodeId: z.string().describe("The ID of the frame to modify"), itemSpacing: z.number().describe("Distance between children. Note: This value will be ignored if primaryAxisAlignItems is set to SPACE_BETWEEN.") }, async ({ nodeId, itemSpacing }) => { try { const result = await sendCommandToFigma("set_item_spacing", { nodeId, itemSpacing }); const typedResult = result as { name: string }; return { content: [ { type: "text", text: `Set item spacing to ${itemSpacing} for frame "${typedResult.name}"`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error setting item spacing: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); // A tool to get Figma Prototyping Reactions from multiple nodes server.tool( "get_reactions", "Get Figma Prototyping Reactions from multiple nodes. CRITICAL: The output MUST be processed using the 'reaction_to_connector_strategy' prompt IMMEDIATELY to generate parameters for connector lines via the 'create_connections' tool.", { nodeIds: z.array(z.string()).describe("Array of node IDs to get reactions from"), }, async ({ nodeIds }) => { try { const result = await sendCommandToFigma("get_reactions", { nodeIds }); return { content: [ { type: "text", text: JSON.stringify(result) }, { type: "text", text: "IMPORTANT: You MUST now use the reaction data above and follow the `reaction_to_connector_strategy` prompt to prepare the parameters for the `create_connections` tool call. This is a required next step." } ], followUp: { type: "prompt", prompt: "reaction_to_connector_strategy", }, }; } catch (error) { return { content: [ { type: "text", text: `Error getting reactions: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Create Connectors Tool server.tool( "set_default_connector", "Set a copied connector node as the default connector", { connectorId: z.string().optional().describe("The ID of the connector node to set as default") }, async ({ connectorId }) => { try { const result = await sendCommandToFigma("set_default_connector", { connectorId }); return { content: [ { type: "text", text: `Default connector set: ${JSON.stringify(result)}` } ] }; } catch (error) { return { content: [ { type: "text", text: `Error setting default connector: ${error instanceof Error ? error.message : String(error)}` } ] }; } } ); // Connect Nodes Tool server.tool( "create_connections", "Create connections between nodes using the default connector style", { connections: z.array(z.object({ startNodeId: z.string().describe("ID of the starting node"), endNodeId: z.string().describe("ID of the ending node"), text: z.string().optional().describe("Optional text to display on the connector") })).describe("Array of node connections to create") }, async ({ connections }) => { try { if (!connections || connections.length === 0) { return { content: [ { type: "text", text: "No connections provided" } ] }; } const result = await sendCommandToFigma("create_connections", { connections }); return { content: [ { type: "text", text: `Created ${connections.length} connections: ${JSON.stringify(result)}` } ] }; } catch (error) { return { content: [ { type: "text", text: `Error creating connections: ${error instanceof Error ? error.message : String(error)}` } ] }; } } ); // Strategy for converting Figma prototype reactions to connector lines server.prompt( "reaction_to_connector_strategy", "Strategy for converting Figma prototype reactions to connector lines using the output of 'get_reactions'", (extra) => { return { messages: [ { role: "assistant", content: { type: "text", text: `# Strategy: Convert Figma Prototype Reactions to Connector Lines ## Goal Process the JSON output from the \`get_reactions\` tool to generate an array of connection objects suitable for the \`create_connections\` tool. This visually represents prototype flows as connector lines on the Figma canvas. ## Input Data You will receive JSON data from the \`get_reactions\` tool. This data contains an array of nodes, each with potential reactions. A typical reaction object looks like this: \`\`\`json { "trigger": { "type": "ON_CLICK" }, "action": { "type": "NAVIGATE", "destinationId": "destination-node-id", "navigationTransition": { ... }, "preserveScrollPosition": false } } \`\`\` ## Step-by-Step Process ### 1. Preparation & Context Gathering - **Action:** Call \`read_my_design\` on the relevant node(s) to get context about the nodes involved (names, types, etc.). This helps in generating meaningful connector labels later. - **Action:** Call \`set_default_connector\` **without** the \`connectorId\` parameter. - **Check Result:** Analyze the response from \`set_default_connector\`. - If it confirms a default connector is already set (e.g., "Default connector is already set"), proceed to Step 2. - If it indicates no default connector is set (e.g., "No default connector set..."), you **cannot** proceed with \`create_connections\` yet. Inform the user they need to manually copy a connector from FigJam, paste it onto the current page, select it, and then you can run \`set_default_connector({ connectorId: "SELECTED_NODE_ID" })\` before attempting \`create_connections\`. **Do not proceed to Step 2 until a default connector is confirmed.** ### 2. Filter and Transform Reactions from \`get_reactions\` Output - **Iterate:** Go through the JSON array provided by \`get_reactions\`. For each node in the array: - Iterate through its \`reactions\` array. - **Filter:** Keep only reactions where the \`action\` meets these criteria: - Has a \`type\` that implies a connection (e.g., \`NAVIGATE\`, \`OPEN_OVERLAY\`, \`SWAP_OVERLAY\`). **Ignore** types like \`CHANGE_TO\`, \`CLOSE_OVERLAY\`, etc. - Has a valid \`destinationId\` property. - **Extract:** For each valid reaction, extract the following information: - \`sourceNodeId\`: The ID of the node the reaction belongs to (from the outer loop). - \`destinationNodeId\`: The value of \`action.destinationId\`. - \`actionType\`: The value of \`action.type\`. - \`triggerType\`: The value of \`trigger.type\`. ### 3. Generate Connector Text Labels - **For each extracted connection:** Create a concise, descriptive text label string. - **Combine Information:** Use the \`actionType\`, \`triggerType\`, and potentially the names of the source/destination nodes (obtained from Step 1's \`read_my_design\` or by calling \`get_node_info\` if necessary) to generate the label. - **Example Labels:** - If \`triggerType\` is "ON\_CLICK" and \`actionType\` is "NAVIGATE": "On click, navigate to [Destination Node Name]" - If \`triggerType\` is "ON\_DRAG" and \`actionType\` is "OPEN\_OVERLAY": "On drag, open [Destination Node Name] overlay" - **Keep it brief and informative.** Let this generated string be \`generatedText\`. ### 4. Prepare the \`connections\` Array for \`create_connections\` - **Structure:** Create a JSON array where each element is an object representing a connection. - **Format:** Each object in the array must have the following structure: \`\`\`json { "startNodeId": "sourceNodeId_from_step_2", "endNodeId": "destinationNodeId_from_step_2", "text": "generatedText_from_step_3" } \`\`\` - **Result:** This final array is the value you will pass to the \`connections\` parameter when calling the \`create_connections\` tool. ### 5. Execute Connection Creation - **Action:** Call the \`create_connections\` tool, passing the array generated in Step 4 as the \`connections\` argument. - **Verify:** Check the response from \`create_connections\` to confirm success or failure. This detailed process ensures you correctly interpret the reaction data, prepare the necessary information, and use the appropriate tools to create the connector lines.` }, }, ], description: "Strategy for converting Figma prototype reactions to connector lines using the output of 'get_reactions'", }; } ); // Define command types and parameters type FigmaCommand = | "get_document_info" | "get_selection" | "get_node_info" | "get_nodes_info" | "read_my_design" | "create_rectangle" | "create_frame" | "create_text" | "set_fill_color" | "set_stroke_color" | "move_node" | "resize_node" | "delete_node" | "delete_multiple_nodes" | "get_styles" | "get_local_components" | "create_component_instance" | "get_instance_overrides" | "set_instance_overrides" | "export_node_as_image" | "join" | "set_corner_radius" | "clone_node" | "set_text_content" | "scan_text_nodes" | "set_multiple_text_contents" | "get_annotations" | "set_annotation" | "set_multiple_annotations" | "scan_nodes_by_types" | "set_layout_mode" | "set_padding" | "set_axis_align" | "set_layout_sizing" | "set_item_spacing" | "get_reactions" | "set_default_connector" | "create_connections"; type CommandParams = { get_document_info: Record<string, never>; get_selection: Record<string, never>; get_node_info: { nodeId: string }; get_nodes_info: { nodeIds: string[] }; create_rectangle: { x: number; y: number; width: number; height: number; name?: string; parentId?: string; }; create_frame: { x: number; y: number; width: number; height: number; name?: string; parentId?: string; fillColor?: { r: number; g: number; b: number; a?: number }; strokeColor?: { r: number; g: number; b: number; a?: number }; strokeWeight?: number; }; create_text: { x: number; y: number; text: string; fontSize?: number; fontWeight?: number; fontColor?: { r: number; g: number; b: number; a?: number }; name?: string; parentId?: string; }; set_fill_color: { nodeId: string; r: number; g: number; b: number; a?: number; }; set_stroke_color: { nodeId: string; r: number; g: number; b: number; a?: number; weight?: number; }; move_node: { nodeId: string; x: number; y: number; }; resize_node: { nodeId: string; width: number; height: number; }; delete_node: { nodeId: string; }; delete_multiple_nodes: { nodeIds: string[]; }; get_styles: Record<string, never>; get_local_components: Record<string, never>; get_team_components: Record<string, never>; create_component_instance: { componentKey: string; x: number; y: number; }; get_instance_overrides: { instanceNodeId: string | null; }; set_instance_overrides: { targetNodeIds: string[]; sourceInstanceId: string; }; export_node_as_image: { nodeId: string; format?: "PNG" | "JPG" | "SVG" | "PDF"; scale?: number; }; execute_code: { code: string; }; join: { channel: string; }; set_corner_radius: { nodeId: string; radius: number; corners?: boolean[]; }; clone_node: { nodeId: string; x?: number; y?: number; }; set_text_content: { nodeId: string; text: string; }; scan_text_nodes: { nodeId: string; useChunking: boolean; chunkSize: number; }; set_multiple_text_contents: { nodeId: string; text: Array<{ nodeId: string; text: string }>; }; get_annotations: { nodeId?: string; includeCategories?: boolean; }; set_annotation: { nodeId: string; annotationId?: string; labelMarkdown: string; categoryId?: string; properties?: Array<{ type: string }>; }; set_multiple_annotations: SetMultipleAnnotationsParams; scan_nodes_by_types: { nodeId: string; types: Array<string>; }; get_reactions: { nodeIds: string[] }; set_default_connector: { connectorId?: string | undefined; }; create_connections: { connections: Array<{ startNodeId: string; endNodeId: string; text?: string; }>; }; }; // Helper function to process Figma node responses function processFigmaNodeResponse(result: unknown): any { if (!result || typeof result !== "object") { return result; } // Check if this looks like a node response const resultObj = result as Record<string, unknown>; if ("id" in resultObj && typeof resultObj.id === "string") { // It appears to be a node response, log the details console.info( `Processed Figma node: ${resultObj.name || "Unknown"} (ID: ${resultObj.id })` ); if ("x" in resultObj && "y" in resultObj) { console.debug(`Node position: (${resultObj.x}, ${resultObj.y})`); } if ("width" in resultObj && "height" in resultObj) { console.debug(`Node dimensions: ${resultObj.width}×${resultObj.height}`); } } return result; } // Update the connectToFigma function function connectToFigma(port: number = 3055) { // If already connected, do nothing if (ws && ws.readyState === WebSocket.OPEN) { logger.info('Already connected to Figma'); return; } const wsUrl = serverUrl === 'localhost' ? `${WS_URL}:${port}` : WS_URL; logger.info(`Connecting to Figma socket server at ${wsUrl}...`); ws = new WebSocket(wsUrl); ws.on('open', () => { logger.info('Connected to Figma socket server'); // Reset channel on new connection currentChannel = null; }); ws.on("message", (data: any) => { try { // Define a more specific type with an index signature to allow any property access interface ProgressMessage { message: FigmaResponse | any; type?: string; id?: string; [key: string]: any; // Allow any other properties } const json = JSON.parse(data) as ProgressMessage; // Handle progress updates if (json.type === 'progress_update') { const progressData = json.message.data as CommandProgressUpdate; const requestId = json.id || ''; if (requestId && pendingRequests.has(requestId)) { const request = pendingRequests.get(requestId)!; // Update last activity timestamp request.lastActivity = Date.now(); // Reset the timeout to prevent timeouts during long-running operations clearTimeout(request.timeout); // Create a new timeout request.timeout = setTimeout(() => { if (pendingRequests.has(requestId)) { logger.error(`Request ${requestId} timed out after extended period of inactivity`); pendingRequests.delete(requestId); request.reject(new Error('Request to Figma timed out')); } }, 60000); // 60 second timeout for inactivity // Log progress logger.info(`Progress update for ${progressData.commandType}: ${progressData.progress}% - ${progressData.message}`); // For completed updates, we could resolve the request early if desired if (progressData.status === 'completed' && progressData.progress === 100) { // Optionally resolve early with partial data // request.resolve(progressData.payload); // pendingRequests.delete(requestId); // Instead, just log the completion, wait for final result from Figma logger.info(`Operation ${progressData.commandType} completed, waiting for final result`); } } return; } // Handle regular responses const myResponse = json.message; logger.debug(`Received message: ${JSON.stringify(myResponse)}`); logger.log('myResponse' + JSON.stringify(myResponse)); // Handle response to a request if ( myResponse.id && pendingRequests.has(myResponse.id) && myResponse.result ) { const request = pendingRequests.get(myResponse.id)!; clearTimeout(request.timeout); if (myResponse.error) { logger.error(`Error from Figma: ${myResponse.error}`); request.reject(new Error(myResponse.error)); } else { if (myResponse.result) { request.resolve(myResponse.result); } } pendingRequests.delete(myResponse.id); } else { // Handle broadcast messages or events logger.info(`Received broadcast message: ${JSON.stringify(myResponse)}`); } } catch (error) { logger.error(`Error parsing message: ${error instanceof Error ? error.message : String(error)}`); } }); ws.on('error', (error) => { logger.error(`Socket error: ${error}`); }); ws.on('close', () => { logger.info('Disconnected from Figma socket server'); ws = null; // Reject all pending requests for (const [id, request] of pendingRequests.entries()) { clearTimeout(request.timeout); request.reject(new Error("Connection closed")); pendingRequests.delete(id); } // Attempt to reconnect logger.info('Attempting to reconnect in 2 seconds...'); setTimeout(() => connectToFigma(port), 2000); }); } // Function to join a channel async function joinChannel(channelName: string): Promise<void> { if (!ws || ws.readyState !== WebSocket.OPEN) { throw new Error("Not connected to Figma"); } try { await sendCommandToFigma("join", { channel: channelName }); currentChannel = channelName; logger.info(`Joined channel: ${channelName}`); } catch (error) { logger.error(`Failed to join channel: ${error instanceof Error ? error.message : String(error)}`); throw error; } } // Function to send commands to Figma function sendCommandToFigma( command: FigmaCommand, params: unknown = {}, timeoutMs: number = 30000 ): Promise<unknown> { return new Promise((resolve, reject) => { // If not connected, try to connect first if (!ws || ws.readyState !== WebSocket.OPEN) { connectToFigma(); reject(new Error("Not connected to Figma. Attempting to connect...")); return; } // Check if we need a channel for this command const requiresChannel = command !== "join"; if (requiresChannel && !currentChannel) { reject(new Error("Must join a channel before sending commands")); return; } const id = uuidv4(); const request = { id, type: command === "join" ? "join" : "message", ...(command === "join" ? { channel: (params as any).channel } : { channel: currentChannel }), message: { id, command, params: { ...(params as any), commandId: id, // Include the command ID in params }, }, }; // Set timeout for request const timeout = setTimeout(() => { if (pendingRequests.has(id)) { pendingRequests.delete(id); logger.error(`Request ${id} to Figma timed out after ${timeoutMs / 1000} seconds`); reject(new Error('Request to Figma timed out')); } }, timeoutMs); // Store the promise callbacks to resolve/reject later pendingRequests.set(id, { resolve, reject, timeout, lastActivity: Date.now() }); // Send the request logger.info(`Sending command to Figma: ${command}`); logger.debug(`Request details: ${JSON.stringify(request)}`); ws.send(JSON.stringify(request)); }); } // Update the join_channel tool server.tool( "join_channel", "Join a specific channel to communicate with Figma", { channel: z.string().describe("The name of the channel to join").default(""), }, async ({ channel }) => { try { if (!channel) { // If no channel provided, ask the user for input return { content: [ { type: "text", text: "Please provide a channel name to join:", }, ], followUp: { tool: "join_channel", description: "Join the specified channel", }, }; } await joinChannel(channel); return { content: [ { type: "text", text: `Successfully joined channel: ${channel}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error joining channel: ${error instanceof Error ? error.message : String(error) }`, }, ], }; } } ); // Start the server async function main() { try { // Try to connect to Figma socket server connectToFigma(); } catch (error) { logger.warn(`Could not connect to Figma initially: ${error instanceof Error ? error.message : String(error)}`); logger.warn('Will try to connect when the first command is sent'); } // Start the MCP server with stdio transport const transport = new StdioServerTransport(); await server.connect(transport); logger.info('FigmaMCP server running on stdio'); } // Run the server main().catch(error => { logger.error(`Error starting FigmaMCP server: ${error instanceof Error ? error.message : String(error)}`); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pipethedev/Talk-to-Figma-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server