UNPKG

figma-copilot

Version:

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

1 lines 271 kB
{"version":3,"sources":["../src/talk_to_figma_mcp/server.ts","../src/talk_to_figma_mcp/errors.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport WebSocket from \"ws\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport { \n ErrorCodes, \n CommonErrors, \n createErrorResponse, \n formatErrorForMCP \n} from \"./errors.js\";\n\n// Define TypeScript interfaces for Figma responses\ninterface FigmaResponse {\n id: string;\n result?: any;\n error?: string;\n}\n\n// Define interface for command progress updates\ninterface CommandProgressUpdate {\n type: 'command_progress';\n commandId: string;\n commandType: string;\n status: 'started' | 'in_progress' | 'completed' | 'error';\n progress: number;\n totalItems: number;\n processedItems: number;\n currentChunk?: number;\n totalChunks?: number;\n chunkSize?: number;\n message: string;\n payload?: any;\n timestamp: number;\n}\n\n// Add TypeScript interfaces for component overrides after line 21\ninterface ComponentOverride {\n id: string;\n overriddenFields: string[];\n}\n\n// Update the getInstanceOverridesResult interface to match the plugin implementation\ninterface getInstanceOverridesResult {\n success: boolean;\n message: string;\n sourceInstanceId: string;\n mainComponentId: string;\n overridesCount: number;\n}\n\ninterface setInstanceOverridesResult {\n success: boolean;\n message: string;\n totalCount?: number;\n results?: Array<{\n success: boolean;\n instanceId: string;\n instanceName: string;\n appliedCount?: number;\n message?: string;\n }>;\n}\n\n// Custom logging functions that write to stderr instead of stdout to avoid being captured\nconst logger = {\n info: (message: string) => process.stderr.write(`[INFO] ${message}\\n`),\n debug: (message: string) => process.stderr.write(`[DEBUG] ${message}\\n`),\n warn: (message: string) => process.stderr.write(`[WARN] ${message}\\n`),\n error: (message: string) => process.stderr.write(`[ERROR] ${message}\\n`),\n log: (message: string) => process.stderr.write(`[LOG] ${message}\\n`)\n};\n\n// WebSocket connection and request tracking\nlet ws: WebSocket | null = null;\nconst pendingRequests = new Map<string, {\n resolve: (value: unknown) => void;\n reject: (reason: unknown) => void;\n timeout: ReturnType<typeof setTimeout>;\n lastActivity: number; // Add timestamp for last activity\n}>();\n\n// Track which channel each client is in\nlet currentChannel: string | null = null;\n\n// Create MCP server\nconst server = new McpServer({\n name: \"TalkToFigmaMCP\",\n version: \"1.0.0\",\n});\n\n// Add command line argument parsing\nconst args = process.argv.slice(2);\nconst serverArg = args.find(arg => arg.startsWith('--server='));\nconst serverUrl = serverArg ? serverArg.split('=')[1] : 'localhost';\nconst WS_URL = serverUrl === 'localhost' ? `ws://${serverUrl}` : `wss://${serverUrl}`;\n\n// Document Info Tool\nserver.tool(\n \"get_document_info\",\n \"Get detailed information about the current Figma document\",\n {},\n async () => {\n try {\n const result = await sendCommandToFigma(\"get_document_info\");\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(result)\n }\n ]\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to get document info: ${error instanceof Error ? error.message : String(error)}`,\n {\n suggestions: [\n 'Ensure you are connected to Figma',\n 'Check if a document is open in Figma',\n 'Verify the WebSocket connection is active'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Selection Tool (DEPRECATED - Use get_current_context instead)\nserver.tool(\n \"get_selection\",\n \"[DEPRECATED] Get information about the current selection in Figma. Use 'get_current_context' instead.\",\n {},\n async () => {\n // Redirect to get_current_context with deprecation notice\n console.warn('[DEPRECATION] get_selection is deprecated. Use get_current_context instead.');\n \n try {\n const result = await sendCommandToFigma(\"get_selection\");\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(result)\n },\n {\n type: \"text\",\n text: \"\\n⚠️ DEPRECATION WARNING: get_selection is deprecated. Please use get_current_context() instead for more comprehensive context information.\"\n }\n ]\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to get selection: ${error instanceof Error ? error.message : String(error)}`,\n {\n suggestions: [\n 'Use get_current_context instead of get_selection',\n 'The new tool provides selection plus additional context',\n 'Example: get_current_context()'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Unified Current Context Tool\nserver.tool(\n \"get_current_context\",\n \"Get comprehensive context about the current state including selection, focused slide (if in Slides mode), and optionally document info\",\n {\n includeDocument: z.boolean().optional().describe(\"Include document information (default: false)\"),\n includeSlideDetails: z.boolean().optional().describe(\"Include detailed slide information if in Slides mode (default: true)\"),\n includeSelectionDetails: z.boolean().optional().describe(\"Include detailed selection information (default: false)\")\n },\n async ({ includeDocument = false, includeSlideDetails = true, includeSelectionDetails = false }) => {\n try {\n const context: any = {};\n \n // Always get selection\n try {\n const selection = await sendCommandToFigma(\"get_selection\");\n context.selection = selection;\n } catch (error) {\n context.selection = { error: \"Failed to get selection\", selectionCount: 0 };\n }\n \n // Get connection status to determine editor type\n try {\n const status = await sendCommandToFigma(\"get_connection_status\", {}) as any;\n context.editorType = status.editorType || \"figma\";\n \n // If in Slides mode, get slide-specific info\n if (status.editorType === \"slides\" && includeSlideDetails) {\n // Get focused slide\n try {\n const focusedSlide = await sendCommandToFigma(\"get_focused_slide\", {});\n context.focusedSlide = focusedSlide;\n } catch (error) {\n context.focusedSlide = null;\n }\n \n // Get slides mode\n try {\n const slidesMode = await sendCommandToFigma(\"get_slides_mode\", {});\n context.slidesMode = slidesMode;\n } catch (error) {\n context.slidesMode = null;\n }\n }\n } catch (error) {\n context.editorType = \"unknown\";\n }\n \n // Optionally get document info\n if (includeDocument) {\n try {\n const docInfo = await sendCommandToFigma(\"get_document_info\");\n context.document = docInfo;\n } catch (error) {\n context.document = { error: \"Failed to get document info\" };\n }\n }\n \n // Optionally get detailed selection info\n if (includeSelectionDetails && context.selection && context.selection.selectionCount > 0) {\n try {\n const detailedSelection = await sendCommandToFigma(\"read_my_design\", {});\n context.selectionDetails = detailedSelection;\n } catch (error) {\n context.selectionDetails = null;\n }\n }\n \n return {\n content: [{\n type: \"text\",\n text: JSON.stringify(context, null, 2)\n }]\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to get current context: ${error instanceof Error ? error.message : String(error)}`,\n {\n suggestions: [\n 'Ensure you are connected to Figma',\n 'Check if the Figma plugin is running',\n 'Try with different option parameters'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Read My Design Tool\nserver.tool(\n \"read_my_design\",\n \"Get detailed information about the current selection in Figma, including all node details\",\n {},\n async () => {\n try {\n const result = await sendCommandToFigma(\"read_my_design\", {});\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(result)\n }\n ]\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to read design: ${error instanceof Error ? error.message : String(error)}`,\n {\n suggestions: [\n 'Select one or more nodes in Figma first',\n 'Ensure you are connected to a Figma document',\n 'Check if the Figma plugin is running'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Node Info Tool (DEPRECATED - Use get_nodes instead)\nserver.tool(\n \"get_node_info\",\n \"[DEPRECATED] Get detailed information about a specific node in Figma. Use 'get_nodes' instead.\",\n {\n nodeId: z.string().describe(\"The ID of the node to get information about\"),\n },\n async ({ nodeId }) => {\n // Redirect to get_nodes with deprecation notice\n console.warn('[DEPRECATION] get_node_info is deprecated. Use get_nodes instead.');\n \n try {\n // Call the unified get_nodes tool\n const result = await sendCommandToFigma(\"get_node_info\", { nodeId });\n const filtered = filterFigmaNode(result);\n \n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(filtered)\n },\n {\n type: \"text\",\n text: \"\\n⚠️ DEPRECATION WARNING: get_node_info is deprecated. Please use get_nodes({nodeIds: '\" + nodeId + \"'}) instead.\"\n }\n ]\n };\n } catch (error) {\n // Check if it's a node not found error\n const errorMessage = error instanceof Error ? error.message : String(error);\n if (errorMessage.includes('not found') || errorMessage.includes('does not exist')) {\n return formatErrorForMCP(CommonErrors.nodeNotFound(nodeId));\n }\n \n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to get node info: ${errorMessage}`,\n {\n nodeId,\n suggestions: [\n 'Use get_nodes instead of get_node_info',\n 'Example: get_nodes({nodeIds: \"' + nodeId + '\"})',\n 'The new tool supports both single and multiple nodes'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\nfunction rgbaToHex(color: any): string {\n // skip if color is already hex\n if (color.startsWith('#')) {\n return color;\n }\n\n const r = Math.round(color.r * 255);\n const g = Math.round(color.g * 255);\n const b = Math.round(color.b * 255);\n const a = Math.round(color.a * 255);\n\n 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')}`;\n}\n\nfunction filterFigmaNode(node: any) {\n // Skip VECTOR type nodes\n if (node.type === \"VECTOR\") {\n return null;\n }\n\n const filtered: any = {\n id: node.id,\n name: node.name,\n type: node.type,\n };\n\n if (node.fills && node.fills.length > 0) {\n filtered.fills = node.fills.map((fill: any) => {\n const processedFill = { ...fill };\n\n // Remove boundVariables and imageRef\n delete processedFill.boundVariables;\n delete processedFill.imageRef;\n\n // Process gradientStops if present\n if (processedFill.gradientStops) {\n processedFill.gradientStops = processedFill.gradientStops.map((stop: any) => {\n const processedStop = { ...stop };\n // Convert color to hex if present\n if (processedStop.color) {\n processedStop.color = rgbaToHex(processedStop.color);\n }\n // Remove boundVariables\n delete processedStop.boundVariables;\n return processedStop;\n });\n }\n\n // Convert solid fill colors to hex\n if (processedFill.color) {\n processedFill.color = rgbaToHex(processedFill.color);\n }\n\n return processedFill;\n });\n }\n\n if (node.strokes && node.strokes.length > 0) {\n filtered.strokes = node.strokes.map((stroke: any) => {\n const processedStroke = { ...stroke };\n // Remove boundVariables\n delete processedStroke.boundVariables;\n // Convert color to hex if present\n if (processedStroke.color) {\n processedStroke.color = rgbaToHex(processedStroke.color);\n }\n return processedStroke;\n });\n }\n\n if (node.cornerRadius !== undefined) {\n filtered.cornerRadius = node.cornerRadius;\n }\n\n if (node.absoluteBoundingBox) {\n filtered.absoluteBoundingBox = node.absoluteBoundingBox;\n }\n\n if (node.characters) {\n filtered.characters = node.characters;\n }\n\n if (node.style) {\n filtered.style = {\n fontFamily: node.style.fontFamily,\n fontStyle: node.style.fontStyle,\n fontWeight: node.style.fontWeight,\n fontSize: node.style.fontSize,\n textAlignHorizontal: node.style.textAlignHorizontal,\n letterSpacing: node.style.letterSpacing,\n lineHeightPx: node.style.lineHeightPx\n };\n }\n\n if (node.children) {\n filtered.children = node.children\n .map((child: any) => filterFigmaNode(child))\n .filter((child: any) => child !== null); // Remove null children (VECTOR nodes)\n }\n\n return filtered;\n}\n\n// Unified Get Nodes Tool\nserver.tool(\n \"get_nodes\",\n \"Get detailed information about one or more nodes in Figma. Accepts either a single node ID or array of IDs.\",\n {\n nodeIds: z.any().describe(\"Node ID(s) to retrieve - can be a single string ID or an array of string IDs\"),\n includeChildren: z.boolean().optional().describe(\"Whether to include child nodes (default: true)\"),\n maxDepth: z.number().optional().describe(\"Maximum depth for child traversal (-1 for unlimited, default: -1)\")\n },\n async ({ nodeIds, includeChildren = true, maxDepth = -1 }) => {\n try {\n // Validate and normalize input\n if (!nodeIds) {\n throw new Error(\"nodeIds parameter is required\");\n }\n \n // Handle different input formats including stringified arrays\n let nodeIdArray: string[];\n let isSingleNode = false;\n \n if (typeof nodeIds === 'string') {\n // Check if it's a stringified array\n if (nodeIds.startsWith('[') && nodeIds.endsWith(']')) {\n try {\n // Try to parse as JSON array\n const parsed = JSON.parse(nodeIds);\n if (Array.isArray(parsed)) {\n nodeIdArray = parsed;\n isSingleNode = false;\n } else {\n // Not a valid array, treat as single ID\n isSingleNode = true;\n nodeIdArray = [nodeIds];\n }\n } catch (e) {\n // Failed to parse, treat as single ID\n isSingleNode = true;\n nodeIdArray = [nodeIds];\n }\n } else {\n // Regular single node ID string\n isSingleNode = true;\n nodeIdArray = [nodeIds];\n }\n } else if (Array.isArray(nodeIds)) {\n // Already an array\n nodeIdArray = nodeIds;\n isSingleNode = nodeIdArray.length === 1;\n } else {\n throw new Error(\"nodeIds must be a string or array of strings\");\n }\n \n if (nodeIdArray.length === 0) {\n return {\n content: [{\n type: \"text\",\n text: \"No node IDs provided\"\n }]\n };\n }\n\n // Use different backend commands based on input\n if (nodeIdArray.length === 1) {\n // Single node - use get_node_info\n const result = await sendCommandToFigma(\"get_node_info\", { \n nodeId: nodeIdArray[0],\n includeChildren,\n maxDepth\n });\n \n const filtered = filterFigmaNode(result);\n return {\n content: [{\n type: \"text\",\n text: JSON.stringify(isSingleNode ? filtered : [filtered])\n }]\n };\n } else {\n // Multiple nodes - use get_multiple_nodes_info for efficiency\n const result = await sendCommandToFigma(\"get_multiple_nodes_info\", {\n nodeIds: nodeIdArray,\n includeChildren,\n maxDepth\n }) as any;\n \n // Process results to match expected format\n const processedResults = result.results.map((nodeResult: any) => {\n if (nodeResult.found) {\n return filterFigmaNode(nodeResult.node);\n }\n return null;\n }).filter((node: any) => node !== null);\n \n return {\n content: [{\n type: \"text\",\n text: JSON.stringify(processedResults)\n }]\n };\n }\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n \n // Check for specific error types\n if (errorMessage.includes('not found') || errorMessage.includes('does not exist')) {\n const nodeId = typeof nodeIds === 'string' ? nodeIds : nodeIds[0];\n return formatErrorForMCP(CommonErrors.nodeNotFound(nodeId));\n }\n \n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to get node(s) info: ${errorMessage}`,\n {\n suggestions: [\n 'Verify all node IDs are valid',\n 'Check if nodes exist in the current document',\n 'Ensure you have access to view these nodes'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Nodes Info Tool (DEPRECATED - Use get_nodes instead)\nserver.tool(\n \"get_nodes_info\",\n \"[DEPRECATED] Get detailed information about multiple nodes in Figma. Use 'get_nodes' instead.\",\n {\n nodeIds: z.array(z.string()).describe(\"Array of node IDs to get information about\")\n },\n async ({ nodeIds }) => {\n // Redirect to get_nodes with deprecation notice\n console.warn('[DEPRECATION] get_nodes_info is deprecated. Use get_nodes instead.');\n \n try {\n const results = await Promise.all(\n nodeIds.map(async (nodeId) => {\n const result = await sendCommandToFigma('get_node_info', { nodeId });\n return { nodeId, info: result };\n })\n );\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(results.map((result) => filterFigmaNode(result.info)))\n },\n {\n type: \"text\",\n text: \"\\n⚠️ DEPRECATION WARNING: get_nodes_info is deprecated. Please use get_nodes({nodeIds: [\" + nodeIds.map(id => '\"' + id + '\"').join(', ') + \"]}) instead.\"\n }\n ]\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to get multiple nodes info: ${error instanceof Error ? error.message : String(error)}`,\n {\n suggestions: [\n 'Use get_nodes instead of get_nodes_info',\n 'Example: get_nodes({nodeIds: [\"id1\", \"id2\"]})',\n 'The new tool supports both single and multiple nodes'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n\n// Create Rectangle Tool\nserver.tool(\n \"create_rectangle\",\n \"Create a new rectangle in Figma\",\n {\n x: z.number().describe(\"X position\"),\n y: z.number().describe(\"Y position\"),\n width: z.number().describe(\"Width of the rectangle\"),\n height: z.number().describe(\"Height of the rectangle\"),\n name: z.string().optional().describe(\"Optional name for the rectangle\"),\n parentId: z\n .string()\n .optional()\n .describe(\"Optional parent node ID to append the rectangle to\"),\n },\n async ({ x, y, width, height, name, parentId }) => {\n try {\n const result = await sendCommandToFigma(\"create_rectangle\", {\n x,\n y,\n width,\n height,\n name: name || \"Rectangle\",\n parentId,\n });\n return {\n content: [\n {\n type: \"text\",\n text: `Created rectangle \"${JSON.stringify(result)}\"`,\n },\n ],\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to create rectangle: ${error instanceof Error ? error.message : String(error)}`,\n {\n suggestions: [\n 'Check if the parent node ID is valid',\n 'Ensure coordinates and dimensions are valid numbers',\n 'Verify you have edit access to the document'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Create Frame Tool\nserver.tool(\n \"create_frame\",\n \"Create a new frame in Figma\",\n {\n x: z.number().describe(\"X position\"),\n y: z.number().describe(\"Y position\"),\n width: z.number().describe(\"Width of the frame\"),\n height: z.number().describe(\"Height of the frame\"),\n name: z.string().optional().describe(\"Optional name for the frame\"),\n parentId: z\n .string()\n .optional()\n .describe(\"Optional parent node ID to append the frame to\"),\n fillColor: z\n .object({\n r: z.number().min(0).max(1).describe(\"Red component (0-1)\"),\n g: z.number().min(0).max(1).describe(\"Green component (0-1)\"),\n b: z.number().min(0).max(1).describe(\"Blue component (0-1)\"),\n a: z\n .number()\n .min(0)\n .max(1)\n .optional()\n .describe(\"Alpha component (0-1)\"),\n })\n .optional()\n .describe(\"Fill color in RGBA format\"),\n strokeColor: z\n .object({\n r: z.number().min(0).max(1).describe(\"Red component (0-1)\"),\n g: z.number().min(0).max(1).describe(\"Green component (0-1)\"),\n b: z.number().min(0).max(1).describe(\"Blue component (0-1)\"),\n a: z\n .number()\n .min(0)\n .max(1)\n .optional()\n .describe(\"Alpha component (0-1)\"),\n })\n .optional()\n .describe(\"Stroke color in RGBA format\"),\n strokeWeight: z.number().positive().optional().describe(\"Stroke weight\"),\n layoutMode: z.enum([\"NONE\", \"HORIZONTAL\", \"VERTICAL\"]).optional().describe(\"Auto-layout mode for the frame\"),\n layoutWrap: z.enum([\"NO_WRAP\", \"WRAP\"]).optional().describe(\"Whether the auto-layout frame wraps its children\"),\n paddingTop: z.number().optional().describe(\"Top padding for auto-layout frame\"),\n paddingRight: z.number().optional().describe(\"Right padding for auto-layout frame\"),\n paddingBottom: z.number().optional().describe(\"Bottom padding for auto-layout frame\"),\n paddingLeft: z.number().optional().describe(\"Left padding for auto-layout frame\"),\n primaryAxisAlignItems: z\n .enum([\"MIN\", \"MAX\", \"CENTER\", \"SPACE_BETWEEN\"])\n .optional()\n .describe(\"Primary axis alignment for auto-layout frame. Note: When set to SPACE_BETWEEN, itemSpacing will be ignored as children will be evenly spaced.\"),\n counterAxisAlignItems: z.enum([\"MIN\", \"MAX\", \"CENTER\", \"BASELINE\"]).optional().describe(\"Counter axis alignment for auto-layout frame\"),\n layoutSizingHorizontal: z.enum([\"FIXED\", \"HUG\", \"FILL\"]).optional().describe(\"Horizontal sizing mode for auto-layout frame\"),\n layoutSizingVertical: z.enum([\"FIXED\", \"HUG\", \"FILL\"]).optional().describe(\"Vertical sizing mode for auto-layout frame\"),\n itemSpacing: z\n .number()\n .optional()\n .describe(\"Distance between children in auto-layout frame. Note: This value will be ignored if primaryAxisAlignItems is set to SPACE_BETWEEN.\")\n },\n async ({\n x,\n y,\n width,\n height,\n name,\n parentId,\n fillColor,\n strokeColor,\n strokeWeight,\n layoutMode,\n layoutWrap,\n paddingTop,\n paddingRight,\n paddingBottom,\n paddingLeft,\n primaryAxisAlignItems,\n counterAxisAlignItems,\n layoutSizingHorizontal,\n layoutSizingVertical,\n itemSpacing\n }) => {\n try {\n const result = await sendCommandToFigma(\"create_frame\", {\n x,\n y,\n width,\n height,\n name: name || \"Frame\",\n parentId,\n fillColor: fillColor || { r: 1, g: 1, b: 1, a: 1 },\n strokeColor: strokeColor,\n strokeWeight: strokeWeight,\n layoutMode,\n layoutWrap,\n paddingTop,\n paddingRight,\n paddingBottom,\n paddingLeft,\n primaryAxisAlignItems,\n counterAxisAlignItems,\n layoutSizingHorizontal,\n layoutSizingVertical,\n itemSpacing\n });\n const typedResult = result as { name: string; id: string };\n return {\n content: [\n {\n type: \"text\",\n text: `Created frame \"${typedResult.name}\" with ID: ${typedResult.id}. Use the ID as the parentId to appendChild inside this frame.`,\n },\n ],\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to create frame: ${error instanceof Error ? error.message : String(error)}`,\n {\n suggestions: [\n 'Check if the parent node ID is valid',\n 'Ensure coordinates and dimensions are valid numbers',\n 'Verify you have edit access to the document'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Create Text Tool\nserver.tool(\n \"create_text\",\n \"Create a new text element in Figma\",\n {\n x: z.number().describe(\"X position\"),\n y: z.number().describe(\"Y position\"),\n text: z.string().describe(\"Text content\"),\n fontSize: z.number().optional().describe(\"Font size (default: 14)\"),\n fontWeight: z\n .number()\n .optional()\n .describe(\"Font weight (e.g., 400 for Regular, 700 for Bold)\"),\n fontColor: z\n .object({\n r: z.number().min(0).max(1).describe(\"Red component (0-1)\"),\n g: z.number().min(0).max(1).describe(\"Green component (0-1)\"),\n b: z.number().min(0).max(1).describe(\"Blue component (0-1)\"),\n a: z\n .number()\n .min(0)\n .max(1)\n .optional()\n .describe(\"Alpha component (0-1)\"),\n })\n .optional()\n .describe(\"Font color in RGBA format\"),\n name: z\n .string()\n .optional()\n .describe(\"Semantic layer name for the text node\"),\n parentId: z\n .string()\n .optional()\n .describe(\"Optional parent node ID to append the text to\"),\n },\n async ({ x, y, text, fontSize, fontWeight, fontColor, name, parentId }) => {\n try {\n const result = await sendCommandToFigma(\"create_text\", {\n x,\n y,\n text,\n fontSize: fontSize || 14,\n fontWeight: fontWeight || 400,\n fontColor: fontColor || { r: 0, g: 0, b: 0, a: 1 },\n name: name || \"Text\",\n parentId,\n });\n const typedResult = result as { name: string; id: string };\n return {\n content: [\n {\n type: \"text\",\n text: `Created text \"${typedResult.name}\" with ID: ${typedResult.id}`,\n },\n ],\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to create text: ${error instanceof Error ? error.message : String(error)}`,\n {\n suggestions: [\n 'Check if the parent node ID is valid',\n 'Ensure coordinates and dimensions are valid numbers',\n 'Verify you have edit access to the document',\n 'Use update_text_preserve_formatting to maintain text styles'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Set Fill Color Tool\nserver.tool(\n \"set_fill_color\",\n \"Set the fill color of a node in Figma can be TextNode or FrameNode\",\n {\n nodeId: z.string().describe(\"The ID of the node to modify\"),\n r: z.number().min(0).max(1).describe(\"Red component (0-1)\"),\n g: z.number().min(0).max(1).describe(\"Green component (0-1)\"),\n b: z.number().min(0).max(1).describe(\"Blue component (0-1)\"),\n a: z.number().min(0).max(1).optional().describe(\"Alpha component (0-1)\"),\n },\n async ({ nodeId, r, g, b, a }) => {\n try {\n const result = await sendCommandToFigma(\"set_fill_color\", {\n nodeId,\n color: { r, g, b, a: a || 1 },\n });\n const typedResult = result as { name: string };\n return {\n content: [\n {\n type: \"text\",\n text: `Set fill color of node \"${typedResult.name\n }\" to RGBA(${r}, ${g}, ${b}, ${a || 1})`,\n },\n ],\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to set fill color: ${error instanceof Error ? error.message : String(error)}`,\n {\n nodeId,\n suggestions: [\n 'Verify the node ID is valid',\n 'Check parameter values are in the correct format',\n 'Ensure the node supports this operation'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Set Stroke Color Tool\nserver.tool(\n \"set_stroke_color\",\n \"Set the stroke color of a node in Figma\",\n {\n nodeId: z.string().describe(\"The ID of the node to modify\"),\n r: z.number().min(0).max(1).describe(\"Red component (0-1)\"),\n g: z.number().min(0).max(1).describe(\"Green component (0-1)\"),\n b: z.number().min(0).max(1).describe(\"Blue component (0-1)\"),\n a: z.number().min(0).max(1).optional().describe(\"Alpha component (0-1)\"),\n weight: z.number().positive().optional().describe(\"Stroke weight\"),\n },\n async ({ nodeId, r, g, b, a, weight }) => {\n try {\n const result = await sendCommandToFigma(\"set_stroke_color\", {\n nodeId,\n color: { r, g, b, a: a || 1 },\n weight: weight || 1,\n });\n const typedResult = result as { name: string };\n return {\n content: [\n {\n type: \"text\",\n text: `Set stroke color of node \"${typedResult.name\n }\" to RGBA(${r}, ${g}, ${b}, ${a || 1}) with weight ${weight || 1}`,\n },\n ],\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to set stroke color: ${error instanceof Error ? error.message : String(error)}`,\n {\n nodeId,\n suggestions: [\n 'Verify the node ID is valid',\n 'Check parameter values are in the correct format',\n 'Ensure the node supports this operation'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Move Node Tool\nserver.tool(\n \"move_node\",\n \"Move a node to a new position in Figma\",\n {\n nodeId: z.string().describe(\"The ID of the node to move\"),\n x: z.number().describe(\"New X position\"),\n y: z.number().describe(\"New Y position\"),\n },\n async ({ nodeId, x, y }) => {\n try {\n const result = await sendCommandToFigma(\"move_node\", { nodeId, x, y });\n const typedResult = result as { name: string };\n return {\n content: [\n {\n type: \"text\",\n text: `Moved node \"${typedResult.name}\" to position (${x}, ${y})`,\n },\n ],\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to move node: ${error instanceof Error ? error.message : String(error)}`,\n {\n nodeId,\n suggestions: [\n 'Verify the node ID is valid',\n 'Ensure x and y coordinates are valid numbers',\n 'Check if the node is locked or constrained'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Clone Node Tool\nserver.tool(\n \"clone_node\",\n \"Clone an existing node in Figma\",\n {\n nodeId: z.string().describe(\"The ID of the node to clone\"),\n x: z.number().optional().describe(\"New X position for the clone\"),\n y: z.number().optional().describe(\"New Y position for the clone\")\n },\n async ({ nodeId, x, y }) => {\n try {\n const result = await sendCommandToFigma('clone_node', { nodeId, x, y });\n const typedResult = result as { name: string, id: string };\n return {\n content: [\n {\n type: \"text\",\n text: `Cloned node \"${typedResult.name}\" with new ID: ${typedResult.id}${x !== undefined && y !== undefined ? ` at position (${x}, ${y})` : ''}`\n }\n ]\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to clone node: ${error instanceof Error ? error.message : String(error)}`,\n {\n nodeId,\n suggestions: [\n 'Verify the source node ID is valid',\n 'Ensure you have permission to duplicate the node',\n 'Check if the node type supports cloning'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Resize Node Tool\nserver.tool(\n \"resize_node\",\n \"Resize a node in Figma\",\n {\n nodeId: z.string().describe(\"The ID of the node to resize\"),\n width: z.number().positive().describe(\"New width\"),\n height: z.number().positive().describe(\"New height\"),\n },\n async ({ nodeId, width, height }) => {\n try {\n const result = await sendCommandToFigma(\"resize_node\", {\n nodeId,\n width,\n height,\n });\n const typedResult = result as { name: string };\n return {\n content: [\n {\n type: \"text\",\n text: `Resized node \"${typedResult.name}\" to width ${width} and height ${height}`,\n },\n ],\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to resize node: ${error instanceof Error ? error.message : String(error)}`,\n {\n nodeId,\n suggestions: [\n 'Verify the node ID is valid',\n 'Ensure width and height are positive numbers',\n 'Check if the node has size constraints'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Delete Node Tool\nserver.tool(\n \"delete_node\",\n \"Delete a node from Figma\",\n {\n nodeId: z.string().describe(\"The ID of the node to delete\"),\n },\n async ({ nodeId }) => {\n try {\n await sendCommandToFigma(\"delete_node\", { nodeId });\n return {\n content: [\n {\n type: \"text\",\n text: `Deleted node with ID: ${nodeId}`,\n },\n ],\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to delete node: ${error instanceof Error ? error.message : String(error)}`,\n {\n nodeId,\n suggestions: [\n 'Verify the node ID is valid',\n 'Ensure you have permission to delete the node',\n 'Check if the node is locked or protected'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Delete Multiple Nodes Tool\nserver.tool(\n \"delete_multiple_nodes\",\n \"Delete multiple nodes from Figma at once\",\n {\n nodeIds: z.array(z.string()).describe(\"Array of node IDs to delete\"),\n },\n async ({ nodeIds }) => {\n try {\n const result = await sendCommandToFigma(\"delete_multiple_nodes\", { nodeIds });\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(result)\n }\n ]\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.BATCH_PARTIAL_FAILURE,\n `Failed to delete multiple nodes: ${error instanceof Error ? error.message : String(error)}`,\n {\n suggestions: [\n 'Verify all node IDs in the batch are valid',\n 'Check if some operations may have partially succeeded',\n 'Ensure you have permissions for all items'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Export Node as Image Tool\nserver.tool(\n \"export_node_as_image\",\n \"Export a node as an image from Figma\",\n {\n nodeId: z.string().describe(\"The ID of the node to export\"),\n format: z\n .enum([\"PNG\", \"JPG\", \"SVG\", \"PDF\"])\n .optional()\n .describe(\"Export format\"),\n scale: z.number().positive().optional().describe(\"Export scale\"),\n },\n async ({ nodeId, format, scale }) => {\n try {\n const result = await sendCommandToFigma(\"export_node_as_image\", {\n nodeId,\n format: format || \"PNG\",\n scale: scale || 1,\n });\n const typedResult = result as { imageData: string; mimeType: string };\n\n return {\n content: [\n {\n type: \"image\",\n data: typedResult.imageData,\n mimeType: typedResult.mimeType || \"image/png\",\n },\n ],\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to export node as image: ${error instanceof Error ? error.message : String(error)}`,\n {\n nodeId,\n suggestions: [\n 'Verify the node ID is valid',\n 'Check if the node is visible and not empty',\n 'Ensure the export format is supported (PNG, JPG, SVG, PDF)'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Set Text Content Tool\nserver.tool(\n \"set_text_content\",\n \"Set the text content of an existing text node in Figma\",\n {\n nodeId: z.string().describe(\"The ID of the text node to modify\"),\n text: z.string().describe(\"New text content\"),\n },\n async ({ nodeId, text }) => {\n try {\n const result = await sendCommandToFigma(\"set_text_content\", {\n nodeId,\n text,\n });\n const typedResult = result as { name: string };\n return {\n content: [\n {\n type: \"text\",\n text: `Updated text content of node \"${typedResult.name}\" to \"${text}\"`,\n },\n ],\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to set text content: ${error instanceof Error ? error.message : String(error)}`,\n {\n nodeId,\n suggestions: [\n 'Verify the node ID is valid',\n 'Check parameter values are in the correct format',\n 'Ensure the node supports this operation'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Get Styles Tool\nserver.tool(\n \"get_styles\",\n \"Get all styles from the current Figma document\",\n {},\n async () => {\n try {\n const result = await sendCommandToFigma(\"get_styles\");\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(result)\n }\n ]\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to get styles: ${error instanceof Error ? error.message : String(error)}`,\n {\n suggestions: [\n 'Verify the node ID is correct',\n 'Check if the node exists in the current document',\n 'Ensure you have read access'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Get Local Components Tool\nserver.tool(\n \"get_local_components\",\n \"Get all local components from the Figma document\",\n {},\n async () => {\n try {\n const result = await sendCommandToFigma(\"get_local_components\");\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(result)\n }\n ]\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.COMPONENT_NOT_FOUND,\n `Failed to get local components: ${error instanceof Error ? error.message : String(error)}`,\n {\n suggestions: [\n 'Verify the node ID is correct',\n 'Check if the node exists in the current document',\n 'Ensure you have read access'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Get Annotations Tool\nserver.tool(\n \"get_annotations\",\n \"Get all annotations in the current document or specific node\",\n {\n nodeId: z.string().optional().describe(\"Optional node ID to get annotations for specific node\"),\n includeCategories: z.boolean().optional().default(true).describe(\"Whether to include category information\")\n },\n async ({ nodeId, includeCategories }) => {\n try {\n const result = await sendCommandToFigma(\"get_annotations\", {\n nodeId,\n includeCategories\n });\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(result)\n }\n ]\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to get annotations: ${error instanceof Error ? error.message : String(error)}`,\n {\n nodeId,\n suggestions: [\n 'Verify the node ID is correct',\n 'Check if the node exists in the current document',\n 'Ensure you have read access'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Set Annotation Tool\nserver.tool(\n \"set_annotation\",\n \"Create or update an annotation\",\n {\n nodeId: z.string().describe(\"The ID of the node to annotate\"),\n annotationId: z.string().optional().describe(\"The ID of the annotation to update (if updating existing annotation)\"),\n labelMarkdown: z.string().describe(\"The annotation text in markdown format\"),\n categoryId: z.string().optional().describe(\"The ID of the annotation category\"),\n properties: z.array(z.object({\n type: z.string()\n })).optional().describe(\"Additional properties for the annotation\")\n },\n async ({ nodeId, annotationId, labelMarkdown, categoryId, properties }) => {\n try {\n const result = await sendCommandToFigma(\"set_annotation\", {\n nodeId,\n annotationId,\n labelMarkdown,\n categoryId,\n properties\n });\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(result)\n }\n ]\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to set annotation: ${error instanceof Error ? error.message : String(error)}`,\n {\n nodeId,\n suggestions: [\n 'Verify the node ID is valid',\n 'Check parameter values are in the correct format',\n 'Ensure the node supports this operation'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\ninterface SetMultipleAnnotationsParams {\n nodeId: string;\n annotations: Array<{\n nodeId: string;\n labelMarkdown: string;\n categoryId?: string;\n annotationId?: string;\n properties?: Array<{ type: string }>;\n }>;\n}\n\n// Set Multiple Annotations Tool\nserver.tool(\n \"set_multiple_annotations\",\n \"Set multiple annotations parallelly in a node\",\n {\n nodeId: z\n .string()\n .describe(\"The ID of the node containing the elements to annotate\"),\n annotations: z\n .array(\n z.object({\n nodeId: z.string().describe(\"The ID of the node to annotate\"),\n labelMarkdown: z.string().describe(\"The annotation text in markdown format\"),\n categoryId: z.string().optional().describe(\"The ID of the annotation category\"),\n annotationId: z.string().optional().describe(\"The ID of the annotation to update (if updating existing annotation)\"),\n properties: z.array(z.object({\n type: z.string()\n })).optional().describe(\"Additional properties for the annotation\")\n })\n )\n .describe(\"Array of annotations to apply\"),\n },\n async ({ nodeId, annotations }, extra) => {\n try {\n if (!annotations || annotations.length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: \"No annotations provided\",\n },\n ],\n };\n }\n\n // Initial response to indicate we're starting the process\n const initialStatus = {\n type: \"text\" as const,\n text: `Starting annotation process for ${annotations.length} nodes. This will be processed in batches of 5...`,\n };\n\n // Track overall progress\n let totalProcessed = 0;\n const totalToProcess = annotations.length;\n\n // Use the plugin's set_multiple_annotations function with chunking\n const result = await sendCommandToFigma(\"set_multiple_annotations\", {\n nodeId,\n annotations,\n });\n\n // Cast the result to a specific type to work with it safely\n interface AnnotationResult {\n success: boolean;\n nodeId: string;\n annotationsApplied?: number;\n annotationsFailed?: number;\n totalAnnotations?: number;\n completedInChunks?: number;\n results?: Array<{\n success: boolean;\n nodeId: string;\n error?: string;\n annotationId?: string;\n }>;\n }\n\n const typedResult = result as AnnotationResult;\n\n // Format the results for display\n const success = typedResult.annotationsApplied && typedResult.annotationsApplied > 0;\n const progressText = `\n Annotation process completed:\n - ${typedResult.annotationsApplied || 0} of ${totalToProcess} successfully applied\n - ${typedResult.annotationsFailed || 0} failed\n - Processed in ${typedResult.completedInChunks || 1} batches\n `;\n\n // Detailed results\n const detailedResults = typedResult.results || [];\n const failedResults = detailedResults.filter(item => !item.success);\n\n // Create the detailed part of the response\n let detailedResponse = \"\";\n if (failedResults.length > 0) {\n detailedResponse = `\\n\\nNodes that failed:\\n${failedResults.map(item =>\n `- ${item.nodeId}: ${item.error || \"Unknown error\"}`\n ).join('\\n')}`;\n }\n\n return {\n content: [\n initialStatus,\n {\n type: \"text\" as const,\n text: progressText + detailedResponse,\n },\n ],\n };\n } catch (error) {\n const errorResponse = createErrorResponse(\n ErrorCodes.OPERATION_FAILED,\n `Failed to set multiple annotations: ${error instanceof Error ? error.message : String(error)}`,\n {\n nodeId,\n suggestions: [\n 'Verify the node ID is valid',\n 'Check parameter values are in the correct format',\n 'Ensure the node supports this operation'\n ]\n }\n );\n return formatErrorForMCP(errorResponse);\n }\n }\n);\n\n// Create Component Instance Tool\nserver.tool(\n \"create_component_instance\",\n \"Create an instance of a component in Figma\",\n {\n componentKey: z.string().describe(\"Key of the component to instantiate\"),\n x: z.number().describe(\"X position\"),\n y: z.number().describe(\"Y position\"),\n