UNPKG

figma-copilot

Version:

Enhanced Figma MCP server with improved font handling and additional features. Unofficial tool, not affiliated with Figma, Inc.

1,527 lines (1,523 loc) 153 kB
#!/usr/bin/env node // src/talk_to_figma_mcp/server.ts 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"; // src/talk_to_figma_mcp/errors.ts var ErrorCodes = { // Connection errors CONNECTION_FAILED: "CONNECTION_FAILED", WEBSOCKET_NOT_CONNECTED: "WEBSOCKET_NOT_CONNECTED", CHANNEL_NOT_JOINED: "CHANNEL_NOT_JOINED", // Tool errors TOOL_DEPRECATED: "TOOL_DEPRECATED", TOOL_NOT_FOUND: "TOOL_NOT_FOUND", // Node operation errors NODE_NOT_FOUND: "NODE_NOT_FOUND", NODE_ACCESS_DENIED: "NODE_ACCESS_DENIED", INVALID_NODE_TYPE: "INVALID_NODE_TYPE", // Operation errors OPERATION_TIMEOUT: "OPERATION_TIMEOUT", OPERATION_FAILED: "OPERATION_FAILED", INVALID_PARAMETERS: "INVALID_PARAMETERS", // Batch operation errors BATCH_PARTIAL_FAILURE: "BATCH_PARTIAL_FAILURE", BATCH_SIZE_EXCEEDED: "BATCH_SIZE_EXCEEDED", // Font/text errors MIXED_FONTS_ERROR: "MIXED_FONTS_ERROR", FONT_NOT_AVAILABLE: "FONT_NOT_AVAILABLE", // Component errors COMPONENT_NOT_FOUND: "COMPONENT_NOT_FOUND", INVALID_COMPONENT_OVERRIDE: "INVALID_COMPONENT_OVERRIDE", // Generic errors UNKNOWN_ERROR: "UNKNOWN_ERROR", INTERNAL_ERROR: "INTERNAL_ERROR" }; function createErrorResponse(code, message, options) { const error = { code, message }; if (options?.nodeId) { error.nodeId = options.nodeId; } if (options?.suggestions && options.suggestions.length > 0) { error.suggestions = options.suggestions; } const response = { error }; if (options?.partialResults && options.partialResults.length > 0) { response.partialResults = options.partialResults; } return response; } var CommonErrors = { connectionFailed: (details) => createErrorResponse( ErrorCodes.CONNECTION_FAILED, `Failed to connect to Figma${details ? `: ${details}` : ""}`, { suggestions: [ "Ensure the Figma plugin is running", "Check if the WebSocket server is accessible", "Verify you have joined the correct channel" ] } ), nodeNotFound: (nodeId) => createErrorResponse( ErrorCodes.NODE_NOT_FOUND, `Node with ID "${nodeId}" not found`, { nodeId, suggestions: [ "Verify the node ID is correct", "Check if the node still exists in the document", "Ensure you have access to the node" ] } ), invalidParameters: (details) => createErrorResponse( ErrorCodes.INVALID_PARAMETERS, `Invalid parameters: ${details}`, { suggestions: [ "Check the parameter types and values", "Refer to the tool documentation for correct usage" ] } ), operationTimeout: (operation, nodeId) => createErrorResponse( ErrorCodes.OPERATION_TIMEOUT, `Operation "${operation}" timed out`, { nodeId, suggestions: [ "Try with a smaller selection or fewer nodes", "Use options like maxDepth or timeout to limit scope", "Consider breaking the operation into smaller chunks" ] } ), mixedFonts: (nodeId) => createErrorResponse( ErrorCodes.MIXED_FONTS_ERROR, "Text node contains mixed fonts", { nodeId, suggestions: [ "Use update_text_preserve_formatting to maintain text styles", "Apply consistent font family before updating", "Use set_multiple_text_contents_with_styles for styled updates" ] } ) }; function formatErrorForMCP(error) { return { content: [ { type: "text", text: JSON.stringify(error, null, 2) } ], isError: true }; } // src/talk_to_figma_mcp/server.ts var logger = { info: (message) => process.stderr.write(`[INFO] ${message} `), debug: (message) => process.stderr.write(`[DEBUG] ${message} `), warn: (message) => process.stderr.write(`[WARN] ${message} `), error: (message) => process.stderr.write(`[ERROR] ${message} `), log: (message) => process.stderr.write(`[LOG] ${message} `) }; var ws = null; var pendingRequests = /* @__PURE__ */ new Map(); var currentChannel = null; var server = new McpServer({ name: "TalkToFigmaMCP", version: "1.0.0" }); var args = process.argv.slice(2); var serverArg = args.find((arg) => arg.startsWith("--server=")); var serverUrl = serverArg ? serverArg.split("=")[1] : "localhost"; var WS_URL = serverUrl === "localhost" ? `ws://${serverUrl}` : `wss://${serverUrl}`; 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) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to get document info: ${error instanceof Error ? error.message : String(error)}`, { suggestions: [ "Ensure you are connected to Figma", "Check if a document is open in Figma", "Verify the WebSocket connection is active" ] } ); return formatErrorForMCP(errorResponse); } } ); server.tool( "get_selection", "[DEPRECATED] Get information about the current selection in Figma. Use 'get_current_context' instead.", {}, async () => { console.warn("[DEPRECATION] get_selection is deprecated. Use get_current_context instead."); try { const result = await sendCommandToFigma("get_selection"); return { content: [ { type: "text", text: JSON.stringify(result) }, { type: "text", text: "\n\u26A0\uFE0F DEPRECATION WARNING: get_selection is deprecated. Please use get_current_context() instead for more comprehensive context information." } ] }; } catch (error) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to get selection: ${error instanceof Error ? error.message : String(error)}`, { suggestions: [ "Use get_current_context instead of get_selection", "The new tool provides selection plus additional context", "Example: get_current_context()" ] } ); return formatErrorForMCP(errorResponse); } } ); server.tool( "get_current_context", "Get comprehensive context about the current state including selection, focused slide (if in Slides mode), and optionally document info", { includeDocument: z.boolean().optional().describe("Include document information (default: false)"), includeSlideDetails: z.boolean().optional().describe("Include detailed slide information if in Slides mode (default: true)"), includeSelectionDetails: z.boolean().optional().describe("Include detailed selection information (default: false)") }, async ({ includeDocument = false, includeSlideDetails = true, includeSelectionDetails = false }) => { try { const context = {}; try { const selection = await sendCommandToFigma("get_selection"); context.selection = selection; } catch (error) { context.selection = { error: "Failed to get selection", selectionCount: 0 }; } try { const status = await sendCommandToFigma("get_connection_status", {}); context.editorType = status.editorType || "figma"; if (status.editorType === "slides" && includeSlideDetails) { try { const focusedSlide = await sendCommandToFigma("get_focused_slide", {}); context.focusedSlide = focusedSlide; } catch (error) { context.focusedSlide = null; } try { const slidesMode = await sendCommandToFigma("get_slides_mode", {}); context.slidesMode = slidesMode; } catch (error) { context.slidesMode = null; } } } catch (error) { context.editorType = "unknown"; } if (includeDocument) { try { const docInfo = await sendCommandToFigma("get_document_info"); context.document = docInfo; } catch (error) { context.document = { error: "Failed to get document info" }; } } if (includeSelectionDetails && context.selection && context.selection.selectionCount > 0) { try { const detailedSelection = await sendCommandToFigma("read_my_design", {}); context.selectionDetails = detailedSelection; } catch (error) { context.selectionDetails = null; } } return { content: [{ type: "text", text: JSON.stringify(context, null, 2) }] }; } catch (error) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to get current context: ${error instanceof Error ? error.message : String(error)}`, { suggestions: [ "Ensure you are connected to Figma", "Check if the Figma plugin is running", "Try with different option parameters" ] } ); return formatErrorForMCP(errorResponse); } } ); 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) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to read design: ${error instanceof Error ? error.message : String(error)}`, { suggestions: [ "Select one or more nodes in Figma first", "Ensure you are connected to a Figma document", "Check if the Figma plugin is running" ] } ); return formatErrorForMCP(errorResponse); } } ); server.tool( "get_node_info", "[DEPRECATED] Get detailed information about a specific node in Figma. Use 'get_nodes' instead.", { nodeId: z.string().describe("The ID of the node to get information about") }, async ({ nodeId }) => { console.warn("[DEPRECATION] get_node_info is deprecated. Use get_nodes instead."); try { const result = await sendCommandToFigma("get_node_info", { nodeId }); const filtered = filterFigmaNode(result); return { content: [ { type: "text", text: JSON.stringify(filtered) }, { type: "text", text: "\n\u26A0\uFE0F DEPRECATION WARNING: get_node_info is deprecated. Please use get_nodes({nodeIds: '" + nodeId + "'}) instead." } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes("not found") || errorMessage.includes("does not exist")) { return formatErrorForMCP(CommonErrors.nodeNotFound(nodeId)); } const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to get node info: ${errorMessage}`, { nodeId, suggestions: [ "Use get_nodes instead of get_node_info", 'Example: get_nodes({nodeIds: "' + nodeId + '"})', "The new tool supports both single and multiple nodes" ] } ); return formatErrorForMCP(errorResponse); } } ); function rgbaToHex(color) { 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) { if (node.type === "VECTOR") { return null; } const filtered = { id: node.id, name: node.name, type: node.type }; if (node.fills && node.fills.length > 0) { filtered.fills = node.fills.map((fill) => { const processedFill = { ...fill }; delete processedFill.boundVariables; delete processedFill.imageRef; if (processedFill.gradientStops) { processedFill.gradientStops = processedFill.gradientStops.map((stop) => { const processedStop = { ...stop }; if (processedStop.color) { processedStop.color = rgbaToHex(processedStop.color); } delete processedStop.boundVariables; return processedStop; }); } if (processedFill.color) { processedFill.color = rgbaToHex(processedFill.color); } return processedFill; }); } if (node.strokes && node.strokes.length > 0) { filtered.strokes = node.strokes.map((stroke) => { const processedStroke = { ...stroke }; delete processedStroke.boundVariables; if (processedStroke.color) { processedStroke.color = rgbaToHex(processedStroke.color); } return processedStroke; }); } if (node.cornerRadius !== void 0) { 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) => filterFigmaNode(child)).filter((child) => child !== null); } return filtered; } server.tool( "get_nodes", "Get detailed information about one or more nodes in Figma. Accepts either a single node ID or array of IDs.", { nodeIds: z.any().describe("Node ID(s) to retrieve - can be a single string ID or an array of string IDs"), includeChildren: z.boolean().optional().describe("Whether to include child nodes (default: true)"), maxDepth: z.number().optional().describe("Maximum depth for child traversal (-1 for unlimited, default: -1)") }, async ({ nodeIds, includeChildren = true, maxDepth = -1 }) => { try { if (!nodeIds) { throw new Error("nodeIds parameter is required"); } let nodeIdArray; let isSingleNode = false; if (typeof nodeIds === "string") { if (nodeIds.startsWith("[") && nodeIds.endsWith("]")) { try { const parsed = JSON.parse(nodeIds); if (Array.isArray(parsed)) { nodeIdArray = parsed; isSingleNode = false; } else { isSingleNode = true; nodeIdArray = [nodeIds]; } } catch (e) { isSingleNode = true; nodeIdArray = [nodeIds]; } } else { isSingleNode = true; nodeIdArray = [nodeIds]; } } else if (Array.isArray(nodeIds)) { nodeIdArray = nodeIds; isSingleNode = nodeIdArray.length === 1; } else { throw new Error("nodeIds must be a string or array of strings"); } if (nodeIdArray.length === 0) { return { content: [{ type: "text", text: "No node IDs provided" }] }; } if (nodeIdArray.length === 1) { const result = await sendCommandToFigma("get_node_info", { nodeId: nodeIdArray[0], includeChildren, maxDepth }); const filtered = filterFigmaNode(result); return { content: [{ type: "text", text: JSON.stringify(isSingleNode ? filtered : [filtered]) }] }; } else { const result = await sendCommandToFigma("get_multiple_nodes_info", { nodeIds: nodeIdArray, includeChildren, maxDepth }); const processedResults = result.results.map((nodeResult) => { if (nodeResult.found) { return filterFigmaNode(nodeResult.node); } return null; }).filter((node) => node !== null); return { content: [{ type: "text", text: JSON.stringify(processedResults) }] }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes("not found") || errorMessage.includes("does not exist")) { const nodeId = typeof nodeIds === "string" ? nodeIds : nodeIds[0]; return formatErrorForMCP(CommonErrors.nodeNotFound(nodeId)); } const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to get node(s) info: ${errorMessage}`, { suggestions: [ "Verify all node IDs are valid", "Check if nodes exist in the current document", "Ensure you have access to view these nodes" ] } ); return formatErrorForMCP(errorResponse); } } ); server.tool( "get_nodes_info", "[DEPRECATED] Get detailed information about multiple nodes in Figma. Use 'get_nodes' instead.", { nodeIds: z.array(z.string()).describe("Array of node IDs to get information about") }, async ({ nodeIds }) => { console.warn("[DEPRECATION] get_nodes_info is deprecated. Use get_nodes instead."); 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))) }, { type: "text", text: "\n\u26A0\uFE0F DEPRECATION WARNING: get_nodes_info is deprecated. Please use get_nodes({nodeIds: [" + nodeIds.map((id) => '"' + id + '"').join(", ") + "]}) instead." } ] }; } catch (error) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to get multiple nodes info: ${error instanceof Error ? error.message : String(error)}`, { suggestions: [ "Use get_nodes instead of get_nodes_info", 'Example: get_nodes({nodeIds: ["id1", "id2"]})', "The new tool supports both single and multiple nodes" ] } ); return formatErrorForMCP(errorResponse); } } ); 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) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to create rectangle: ${error instanceof Error ? error.message : String(error)}`, { suggestions: [ "Check if the parent node ID is valid", "Ensure coordinates and dimensions are valid numbers", "Verify you have edit access to the document" ] } ); return formatErrorForMCP(errorResponse); } } ); 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, strokeWeight, layoutMode, layoutWrap, paddingTop, paddingRight, paddingBottom, paddingLeft, primaryAxisAlignItems, counterAxisAlignItems, layoutSizingHorizontal, layoutSizingVertical, itemSpacing }); const typedResult = result; 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) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to create frame: ${error instanceof Error ? error.message : String(error)}`, { suggestions: [ "Check if the parent node ID is valid", "Ensure coordinates and dimensions are valid numbers", "Verify you have edit access to the document" ] } ); return formatErrorForMCP(errorResponse); } } ); 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; return { content: [ { type: "text", text: `Created text "${typedResult.name}" with ID: ${typedResult.id}` } ] }; } catch (error) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to create text: ${error instanceof Error ? error.message : String(error)}`, { suggestions: [ "Check if the parent node ID is valid", "Ensure coordinates and dimensions are valid numbers", "Verify you have edit access to the document", "Use update_text_preserve_formatting to maintain text styles" ] } ); return formatErrorForMCP(errorResponse); } } ); 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; return { content: [ { type: "text", text: `Set fill color of node "${typedResult.name}" to RGBA(${r}, ${g}, ${b}, ${a || 1})` } ] }; } catch (error) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to set fill color: ${error instanceof Error ? error.message : String(error)}`, { nodeId, suggestions: [ "Verify the node ID is valid", "Check parameter values are in the correct format", "Ensure the node supports this operation" ] } ); return formatErrorForMCP(errorResponse); } } ); 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; 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) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to set stroke color: ${error instanceof Error ? error.message : String(error)}`, { nodeId, suggestions: [ "Verify the node ID is valid", "Check parameter values are in the correct format", "Ensure the node supports this operation" ] } ); return formatErrorForMCP(errorResponse); } } ); 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; return { content: [ { type: "text", text: `Moved node "${typedResult.name}" to position (${x}, ${y})` } ] }; } catch (error) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to move node: ${error instanceof Error ? error.message : String(error)}`, { nodeId, suggestions: [ "Verify the node ID is valid", "Ensure x and y coordinates are valid numbers", "Check if the node is locked or constrained" ] } ); return formatErrorForMCP(errorResponse); } } ); 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; return { content: [ { type: "text", text: `Cloned node "${typedResult.name}" with new ID: ${typedResult.id}${x !== void 0 && y !== void 0 ? ` at position (${x}, ${y})` : ""}` } ] }; } catch (error) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to clone node: ${error instanceof Error ? error.message : String(error)}`, { nodeId, suggestions: [ "Verify the source node ID is valid", "Ensure you have permission to duplicate the node", "Check if the node type supports cloning" ] } ); return formatErrorForMCP(errorResponse); } } ); 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; return { content: [ { type: "text", text: `Resized node "${typedResult.name}" to width ${width} and height ${height}` } ] }; } catch (error) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to resize node: ${error instanceof Error ? error.message : String(error)}`, { nodeId, suggestions: [ "Verify the node ID is valid", "Ensure width and height are positive numbers", "Check if the node has size constraints" ] } ); return formatErrorForMCP(errorResponse); } } ); 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) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to delete node: ${error instanceof Error ? error.message : String(error)}`, { nodeId, suggestions: [ "Verify the node ID is valid", "Ensure you have permission to delete the node", "Check if the node is locked or protected" ] } ); return formatErrorForMCP(errorResponse); } } ); 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) { const errorResponse = createErrorResponse( ErrorCodes.BATCH_PARTIAL_FAILURE, `Failed to delete multiple nodes: ${error instanceof Error ? error.message : String(error)}`, { suggestions: [ "Verify all node IDs in the batch are valid", "Check if some operations may have partially succeeded", "Ensure you have permissions for all items" ] } ); return formatErrorForMCP(errorResponse); } } ); 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; return { content: [ { type: "image", data: typedResult.imageData, mimeType: typedResult.mimeType || "image/png" } ] }; } catch (error) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to export node as image: ${error instanceof Error ? error.message : String(error)}`, { nodeId, suggestions: [ "Verify the node ID is valid", "Check if the node is visible and not empty", "Ensure the export format is supported (PNG, JPG, SVG, PDF)" ] } ); return formatErrorForMCP(errorResponse); } } ); 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; return { content: [ { type: "text", text: `Updated text content of node "${typedResult.name}" to "${text}"` } ] }; } catch (error) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to set text content: ${error instanceof Error ? error.message : String(error)}`, { nodeId, suggestions: [ "Verify the node ID is valid", "Check parameter values are in the correct format", "Ensure the node supports this operation" ] } ); return formatErrorForMCP(errorResponse); } } ); 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) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to get styles: ${error instanceof Error ? error.message : String(error)}`, { suggestions: [ "Verify the node ID is correct", "Check if the node exists in the current document", "Ensure you have read access" ] } ); return formatErrorForMCP(errorResponse); } } ); 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) { const errorResponse = createErrorResponse( ErrorCodes.COMPONENT_NOT_FOUND, `Failed to get local components: ${error instanceof Error ? error.message : String(error)}`, { suggestions: [ "Verify the node ID is correct", "Check if the node exists in the current document", "Ensure you have read access" ] } ); return formatErrorForMCP(errorResponse); } } ); 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) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to get annotations: ${error instanceof Error ? error.message : String(error)}`, { nodeId, suggestions: [ "Verify the node ID is correct", "Check if the node exists in the current document", "Ensure you have read access" ] } ); return formatErrorForMCP(errorResponse); } } ); 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) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to set annotation: ${error instanceof Error ? error.message : String(error)}`, { nodeId, suggestions: [ "Verify the node ID is valid", "Check parameter values are in the correct format", "Ensure the node supports this operation" ] } ); return formatErrorForMCP(errorResponse); } } ); 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" } ] }; } const initialStatus = { type: "text", text: `Starting annotation process for ${annotations.length} nodes. This will be processed in batches of 5...` }; let totalProcessed = 0; const totalToProcess = annotations.length; const result = await sendCommandToFigma("set_multiple_annotations", { nodeId, annotations }); const typedResult = result; 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 `; const detailedResults = typedResult.results || []; const failedResults = detailedResults.filter((item) => !item.success); let detailedResponse = ""; if (failedResults.length > 0) { detailedResponse = ` Nodes that failed: ${failedResults.map( (item) => `- ${item.nodeId}: ${item.error || "Unknown error"}` ).join("\n")}`; } return { content: [ initialStatus, { type: "text", text: progressText + detailedResponse } ] }; } catch (error) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to set multiple annotations: ${error instanceof Error ? error.message : String(error)}`, { nodeId, suggestions: [ "Verify the node ID is valid", "Check parameter values are in the correct format", "Ensure the node supports this operation" ] } ); return formatErrorForMCP(errorResponse); } } ); 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; return { content: [ { type: "text", text: JSON.stringify(typedResult) } ] }; } catch (error) { const errorResponse = createErrorResponse( ErrorCodes.COMPONENT_NOT_FOUND, `Failed to create component instance: ${error instanceof Error ? error.message : String(error)}`, { suggestions: [ "Check if the parent node ID is valid", "Ensure coordinates and dimensions are valid numbers", "Verify you have edit access to the document" ] } ); return formatErrorForMCP(errorResponse); } } ); 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; return { content: [ { type: "text", text: typedResult.success ? `Successfully got instance overrides: ${typedResult.message}` : `Failed to get instance overrides: ${typedResult.message}` } ] }; } catch (error) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to copy instance overrides: ${error instanceof Error ? error.message : String(error)}`, { nodeId, suggestions: [ "Verify the node ID is valid", "Ensure the node is a component instance", "Check if the instance has any overrides" ] } ); return formatErrorForMCP(errorResponse); } } ); 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, targetNodeIds: targetNodeIds || [] }); const typedResult = result; 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) { const errorResponse = createErrorResponse( ErrorCodes.OPERATION_FAILED, `Failed to set instance overrides: ${error instanceof Error ? error.message : String(error)}`, { suggestions: [ "Verify the node ID is valid", "Check parameter values are in the correct format", "Ensure the node supports this operation" ] } ); return formatErrorForMCP(errorResponse); } } ); 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; return { content: [ { type: "text", text: `Set corner radius of node "${typedResult.name}" to ${radius}px` } ] }; } catch (error) { const errorResponse = createErrorResponse(