This is page 2 of 3. Use http://codebase.md/sonnylazuardi/cursor-talk-to-figma-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitignore ├── bun.lock ├── Dockerfile ├── DRAGME.md ├── LICENSE ├── package.json ├── readme.md ├── scripts │ └── setup.sh ├── smithery.yaml ├── src │ ├── cursor_mcp_plugin │ │ ├── code.js │ │ ├── manifest.json │ │ ├── setcharacters.js │ │ └── ui.html │ ├── socket.ts │ └── talk_to_figma_mcp │ ├── bun.lock │ ├── package.json │ ├── server.ts │ └── tsconfig.json ├── tsconfig.json └── tsup.config.ts ``` # Files -------------------------------------------------------------------------------- /src/talk_to_figma_mcp/server.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { z } from "zod"; 6 | import WebSocket from "ws"; 7 | import { v4 as uuidv4 } from "uuid"; 8 | 9 | // Define TypeScript interfaces for Figma responses 10 | interface FigmaResponse { 11 | id: string; 12 | result?: any; 13 | error?: string; 14 | } 15 | 16 | // Define interface for command progress updates 17 | interface CommandProgressUpdate { 18 | type: 'command_progress'; 19 | commandId: string; 20 | commandType: string; 21 | status: 'started' | 'in_progress' | 'completed' | 'error'; 22 | progress: number; 23 | totalItems: number; 24 | processedItems: number; 25 | currentChunk?: number; 26 | totalChunks?: number; 27 | chunkSize?: number; 28 | message: string; 29 | payload?: any; 30 | timestamp: number; 31 | } 32 | 33 | // Update the getInstanceOverridesResult interface to match the plugin implementation 34 | interface getInstanceOverridesResult { 35 | success: boolean; 36 | message: string; 37 | sourceInstanceId: string; 38 | mainComponentId: string; 39 | overridesCount: number; 40 | } 41 | 42 | interface setInstanceOverridesResult { 43 | success: boolean; 44 | message: string; 45 | totalCount?: number; 46 | results?: Array<{ 47 | success: boolean; 48 | instanceId: string; 49 | instanceName: string; 50 | appliedCount?: number; 51 | message?: string; 52 | }>; 53 | } 54 | 55 | // Custom logging functions that write to stderr instead of stdout to avoid being captured 56 | const logger = { 57 | info: (message: string) => process.stderr.write(`[INFO] ${message}\n`), 58 | debug: (message: string) => process.stderr.write(`[DEBUG] ${message}\n`), 59 | warn: (message: string) => process.stderr.write(`[WARN] ${message}\n`), 60 | error: (message: string) => process.stderr.write(`[ERROR] ${message}\n`), 61 | log: (message: string) => process.stderr.write(`[LOG] ${message}\n`) 62 | }; 63 | 64 | // WebSocket connection and request tracking 65 | let ws: WebSocket | null = null; 66 | const pendingRequests = new Map<string, { 67 | resolve: (value: unknown) => void; 68 | reject: (reason: unknown) => void; 69 | timeout: ReturnType<typeof setTimeout>; 70 | lastActivity: number; // Add timestamp for last activity 71 | }>(); 72 | 73 | // Track which channel each client is in 74 | let currentChannel: string | null = null; 75 | 76 | // Create MCP server 77 | const server = new McpServer({ 78 | name: "TalkToFigmaMCP", 79 | version: "1.0.0", 80 | }); 81 | 82 | // Add command line argument parsing 83 | const args = process.argv.slice(2); 84 | const serverArg = args.find(arg => arg.startsWith('--server=')); 85 | const serverUrl = serverArg ? serverArg.split('=')[1] : 'localhost'; 86 | const WS_URL = serverUrl === 'localhost' ? `ws://${serverUrl}` : `wss://${serverUrl}`; 87 | 88 | // Document Info Tool 89 | server.tool( 90 | "get_document_info", 91 | "Get detailed information about the current Figma document", 92 | {}, 93 | async () => { 94 | try { 95 | const result = await sendCommandToFigma("get_document_info"); 96 | return { 97 | content: [ 98 | { 99 | type: "text", 100 | text: JSON.stringify(result) 101 | } 102 | ] 103 | }; 104 | } catch (error) { 105 | return { 106 | content: [ 107 | { 108 | type: "text", 109 | text: `Error getting document info: ${error instanceof Error ? error.message : String(error) 110 | }`, 111 | }, 112 | ], 113 | }; 114 | } 115 | } 116 | ); 117 | 118 | // Selection Tool 119 | server.tool( 120 | "get_selection", 121 | "Get information about the current selection in Figma", 122 | {}, 123 | async () => { 124 | try { 125 | const result = await sendCommandToFigma("get_selection"); 126 | return { 127 | content: [ 128 | { 129 | type: "text", 130 | text: JSON.stringify(result) 131 | } 132 | ] 133 | }; 134 | } catch (error) { 135 | return { 136 | content: [ 137 | { 138 | type: "text", 139 | text: `Error getting selection: ${error instanceof Error ? error.message : String(error) 140 | }`, 141 | }, 142 | ], 143 | }; 144 | } 145 | } 146 | ); 147 | 148 | // Read My Design Tool 149 | server.tool( 150 | "read_my_design", 151 | "Get detailed information about the current selection in Figma, including all node details", 152 | {}, 153 | async () => { 154 | try { 155 | const result = await sendCommandToFigma("read_my_design", {}); 156 | return { 157 | content: [ 158 | { 159 | type: "text", 160 | text: JSON.stringify(result) 161 | } 162 | ] 163 | }; 164 | } catch (error) { 165 | return { 166 | content: [ 167 | { 168 | type: "text", 169 | text: `Error getting node info: ${error instanceof Error ? error.message : String(error) 170 | }`, 171 | }, 172 | ], 173 | }; 174 | } 175 | } 176 | ); 177 | 178 | // Node Info Tool 179 | server.tool( 180 | "get_node_info", 181 | "Get detailed information about a specific node in Figma", 182 | { 183 | nodeId: z.string().describe("The ID of the node to get information about"), 184 | }, 185 | async ({ nodeId }: any) => { 186 | try { 187 | const result = await sendCommandToFigma("get_node_info", { nodeId }); 188 | return { 189 | content: [ 190 | { 191 | type: "text", 192 | text: JSON.stringify(filterFigmaNode(result)) 193 | } 194 | ] 195 | }; 196 | } catch (error) { 197 | return { 198 | content: [ 199 | { 200 | type: "text", 201 | text: `Error getting node info: ${error instanceof Error ? error.message : String(error) 202 | }`, 203 | }, 204 | ], 205 | }; 206 | } 207 | } 208 | ); 209 | 210 | function rgbaToHex(color: any): string { 211 | // skip if color is already hex 212 | if (color.startsWith('#')) { 213 | return color; 214 | } 215 | 216 | const r = Math.round(color.r * 255); 217 | const g = Math.round(color.g * 255); 218 | const b = Math.round(color.b * 255); 219 | const a = Math.round(color.a * 255); 220 | 221 | 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')}`; 222 | } 223 | 224 | function filterFigmaNode(node: any) { 225 | // Skip VECTOR type nodes 226 | if (node.type === "VECTOR") { 227 | return null; 228 | } 229 | 230 | const filtered: any = { 231 | id: node.id, 232 | name: node.name, 233 | type: node.type, 234 | }; 235 | 236 | if (node.fills && node.fills.length > 0) { 237 | filtered.fills = node.fills.map((fill: any) => { 238 | const processedFill = { ...fill }; 239 | 240 | // Remove boundVariables and imageRef 241 | delete processedFill.boundVariables; 242 | delete processedFill.imageRef; 243 | 244 | // Process gradientStops if present 245 | if (processedFill.gradientStops) { 246 | processedFill.gradientStops = processedFill.gradientStops.map((stop: any) => { 247 | const processedStop = { ...stop }; 248 | // Convert color to hex if present 249 | if (processedStop.color) { 250 | processedStop.color = rgbaToHex(processedStop.color); 251 | } 252 | // Remove boundVariables 253 | delete processedStop.boundVariables; 254 | return processedStop; 255 | }); 256 | } 257 | 258 | // Convert solid fill colors to hex 259 | if (processedFill.color) { 260 | processedFill.color = rgbaToHex(processedFill.color); 261 | } 262 | 263 | return processedFill; 264 | }); 265 | } 266 | 267 | if (node.strokes && node.strokes.length > 0) { 268 | filtered.strokes = node.strokes.map((stroke: any) => { 269 | const processedStroke = { ...stroke }; 270 | // Remove boundVariables 271 | delete processedStroke.boundVariables; 272 | // Convert color to hex if present 273 | if (processedStroke.color) { 274 | processedStroke.color = rgbaToHex(processedStroke.color); 275 | } 276 | return processedStroke; 277 | }); 278 | } 279 | 280 | if (node.cornerRadius !== undefined) { 281 | filtered.cornerRadius = node.cornerRadius; 282 | } 283 | 284 | if (node.absoluteBoundingBox) { 285 | filtered.absoluteBoundingBox = node.absoluteBoundingBox; 286 | } 287 | 288 | if (node.characters) { 289 | filtered.characters = node.characters; 290 | } 291 | 292 | if (node.style) { 293 | filtered.style = { 294 | fontFamily: node.style.fontFamily, 295 | fontStyle: node.style.fontStyle, 296 | fontWeight: node.style.fontWeight, 297 | fontSize: node.style.fontSize, 298 | textAlignHorizontal: node.style.textAlignHorizontal, 299 | letterSpacing: node.style.letterSpacing, 300 | lineHeightPx: node.style.lineHeightPx 301 | }; 302 | } 303 | 304 | if (node.children) { 305 | filtered.children = node.children 306 | .map((child: any) => filterFigmaNode(child)) 307 | .filter((child: any) => child !== null); // Remove null children (VECTOR nodes) 308 | } 309 | 310 | return filtered; 311 | } 312 | 313 | // Nodes Info Tool 314 | server.tool( 315 | "get_nodes_info", 316 | "Get detailed information about multiple nodes in Figma", 317 | { 318 | nodeIds: z.array(z.string()).describe("Array of node IDs to get information about") 319 | }, 320 | async ({ nodeIds }: any) => { 321 | try { 322 | const results = await Promise.all( 323 | nodeIds.map(async (nodeId: any) => { 324 | const result = await sendCommandToFigma('get_node_info', { nodeId }); 325 | return { nodeId, info: result }; 326 | }) 327 | ); 328 | return { 329 | content: [ 330 | { 331 | type: "text", 332 | text: JSON.stringify(results.map((result) => filterFigmaNode(result.info))) 333 | } 334 | ] 335 | }; 336 | } catch (error) { 337 | return { 338 | content: [ 339 | { 340 | type: "text", 341 | text: `Error getting nodes info: ${error instanceof Error ? error.message : String(error) 342 | }`, 343 | }, 344 | ], 345 | }; 346 | } 347 | } 348 | ); 349 | 350 | 351 | // Create Rectangle Tool 352 | server.tool( 353 | "create_rectangle", 354 | "Create a new rectangle in Figma", 355 | { 356 | x: z.number().describe("X position"), 357 | y: z.number().describe("Y position"), 358 | width: z.number().describe("Width of the rectangle"), 359 | height: z.number().describe("Height of the rectangle"), 360 | name: z.string().optional().describe("Optional name for the rectangle"), 361 | parentId: z 362 | .string() 363 | .optional() 364 | .describe("Optional parent node ID to append the rectangle to"), 365 | }, 366 | async ({ x, y, width, height, name, parentId }: any) => { 367 | try { 368 | const result = await sendCommandToFigma("create_rectangle", { 369 | x, 370 | y, 371 | width, 372 | height, 373 | name: name || "Rectangle", 374 | parentId, 375 | }); 376 | return { 377 | content: [ 378 | { 379 | type: "text", 380 | text: `Created rectangle "${JSON.stringify(result)}"`, 381 | }, 382 | ], 383 | }; 384 | } catch (error) { 385 | return { 386 | content: [ 387 | { 388 | type: "text", 389 | text: `Error creating rectangle: ${error instanceof Error ? error.message : String(error) 390 | }`, 391 | }, 392 | ], 393 | }; 394 | } 395 | } 396 | ); 397 | 398 | // Create Frame Tool 399 | server.tool( 400 | "create_frame", 401 | "Create a new frame in Figma", 402 | { 403 | x: z.number().describe("X position"), 404 | y: z.number().describe("Y position"), 405 | width: z.number().describe("Width of the frame"), 406 | height: z.number().describe("Height of the frame"), 407 | name: z.string().optional().describe("Optional name for the frame"), 408 | parentId: z 409 | .string() 410 | .optional() 411 | .describe("Optional parent node ID to append the frame to"), 412 | fillColor: z 413 | .object({ 414 | r: z.number().min(0).max(1).describe("Red component (0-1)"), 415 | g: z.number().min(0).max(1).describe("Green component (0-1)"), 416 | b: z.number().min(0).max(1).describe("Blue component (0-1)"), 417 | a: z 418 | .number() 419 | .min(0) 420 | .max(1) 421 | .optional() 422 | .describe("Alpha component (0-1)"), 423 | }) 424 | .optional() 425 | .describe("Fill color in RGBA format"), 426 | strokeColor: z 427 | .object({ 428 | r: z.number().min(0).max(1).describe("Red component (0-1)"), 429 | g: z.number().min(0).max(1).describe("Green component (0-1)"), 430 | b: z.number().min(0).max(1).describe("Blue component (0-1)"), 431 | a: z 432 | .number() 433 | .min(0) 434 | .max(1) 435 | .optional() 436 | .describe("Alpha component (0-1)"), 437 | }) 438 | .optional() 439 | .describe("Stroke color in RGBA format"), 440 | strokeWeight: z.number().positive().optional().describe("Stroke weight"), 441 | layoutMode: z.enum(["NONE", "HORIZONTAL", "VERTICAL"]).optional().describe("Auto-layout mode for the frame"), 442 | layoutWrap: z.enum(["NO_WRAP", "WRAP"]).optional().describe("Whether the auto-layout frame wraps its children"), 443 | paddingTop: z.number().optional().describe("Top padding for auto-layout frame"), 444 | paddingRight: z.number().optional().describe("Right padding for auto-layout frame"), 445 | paddingBottom: z.number().optional().describe("Bottom padding for auto-layout frame"), 446 | paddingLeft: z.number().optional().describe("Left padding for auto-layout frame"), 447 | primaryAxisAlignItems: z 448 | .enum(["MIN", "MAX", "CENTER", "SPACE_BETWEEN"]) 449 | .optional() 450 | .describe("Primary axis alignment for auto-layout frame. Note: When set to SPACE_BETWEEN, itemSpacing will be ignored as children will be evenly spaced."), 451 | counterAxisAlignItems: z.enum(["MIN", "MAX", "CENTER", "BASELINE"]).optional().describe("Counter axis alignment for auto-layout frame"), 452 | layoutSizingHorizontal: z.enum(["FIXED", "HUG", "FILL"]).optional().describe("Horizontal sizing mode for auto-layout frame"), 453 | layoutSizingVertical: z.enum(["FIXED", "HUG", "FILL"]).optional().describe("Vertical sizing mode for auto-layout frame"), 454 | itemSpacing: z 455 | .number() 456 | .optional() 457 | .describe("Distance between children in auto-layout frame. Note: This value will be ignored if primaryAxisAlignItems is set to SPACE_BETWEEN.") 458 | }, 459 | async ({ 460 | x, 461 | y, 462 | width, 463 | height, 464 | name, 465 | parentId, 466 | fillColor, 467 | strokeColor, 468 | strokeWeight, 469 | layoutMode, 470 | layoutWrap, 471 | paddingTop, 472 | paddingRight, 473 | paddingBottom, 474 | paddingLeft, 475 | primaryAxisAlignItems, 476 | counterAxisAlignItems, 477 | layoutSizingHorizontal, 478 | layoutSizingVertical, 479 | itemSpacing 480 | }: any) => { 481 | try { 482 | const result = await sendCommandToFigma("create_frame", { 483 | x, 484 | y, 485 | width, 486 | height, 487 | name: name || "Frame", 488 | parentId, 489 | fillColor: fillColor || { r: 1, g: 1, b: 1, a: 1 }, 490 | strokeColor: strokeColor, 491 | strokeWeight: strokeWeight, 492 | layoutMode, 493 | layoutWrap, 494 | paddingTop, 495 | paddingRight, 496 | paddingBottom, 497 | paddingLeft, 498 | primaryAxisAlignItems, 499 | counterAxisAlignItems, 500 | layoutSizingHorizontal, 501 | layoutSizingVertical, 502 | itemSpacing 503 | }); 504 | const typedResult = result as { name: string; id: string }; 505 | return { 506 | content: [ 507 | { 508 | type: "text", 509 | text: `Created frame "${typedResult.name}" with ID: ${typedResult.id}. Use the ID as the parentId to appendChild inside this frame.`, 510 | }, 511 | ], 512 | }; 513 | } catch (error) { 514 | return { 515 | content: [ 516 | { 517 | type: "text", 518 | text: `Error creating frame: ${error instanceof Error ? error.message : String(error) 519 | }`, 520 | }, 521 | ], 522 | }; 523 | } 524 | } 525 | ); 526 | 527 | // Create Text Tool 528 | server.tool( 529 | "create_text", 530 | "Create a new text element in Figma", 531 | { 532 | x: z.number().describe("X position"), 533 | y: z.number().describe("Y position"), 534 | text: z.string().describe("Text content"), 535 | fontSize: z.number().optional().describe("Font size (default: 14)"), 536 | fontWeight: z 537 | .number() 538 | .optional() 539 | .describe("Font weight (e.g., 400 for Regular, 700 for Bold)"), 540 | fontColor: z 541 | .object({ 542 | r: z.number().min(0).max(1).describe("Red component (0-1)"), 543 | g: z.number().min(0).max(1).describe("Green component (0-1)"), 544 | b: z.number().min(0).max(1).describe("Blue component (0-1)"), 545 | a: z 546 | .number() 547 | .min(0) 548 | .max(1) 549 | .optional() 550 | .describe("Alpha component (0-1)"), 551 | }) 552 | .optional() 553 | .describe("Font color in RGBA format"), 554 | name: z 555 | .string() 556 | .optional() 557 | .describe("Semantic layer name for the text node"), 558 | parentId: z 559 | .string() 560 | .optional() 561 | .describe("Optional parent node ID to append the text to"), 562 | }, 563 | async ({ x, y, text, fontSize, fontWeight, fontColor, name, parentId }: any) => { 564 | try { 565 | const result = await sendCommandToFigma("create_text", { 566 | x, 567 | y, 568 | text, 569 | fontSize: fontSize || 14, 570 | fontWeight: fontWeight || 400, 571 | fontColor: fontColor || { r: 0, g: 0, b: 0, a: 1 }, 572 | name: name || "Text", 573 | parentId, 574 | }); 575 | const typedResult = result as { name: string; id: string }; 576 | return { 577 | content: [ 578 | { 579 | type: "text", 580 | text: `Created text "${typedResult.name}" with ID: ${typedResult.id}`, 581 | }, 582 | ], 583 | }; 584 | } catch (error) { 585 | return { 586 | content: [ 587 | { 588 | type: "text", 589 | text: `Error creating text: ${error instanceof Error ? error.message : String(error) 590 | }`, 591 | }, 592 | ], 593 | }; 594 | } 595 | } 596 | ); 597 | 598 | // Set Fill Color Tool 599 | server.tool( 600 | "set_fill_color", 601 | "Set the fill color of a node in Figma can be TextNode or FrameNode", 602 | { 603 | nodeId: z.string().describe("The ID of the node to modify"), 604 | r: z.number().min(0).max(1).describe("Red component (0-1)"), 605 | g: z.number().min(0).max(1).describe("Green component (0-1)"), 606 | b: z.number().min(0).max(1).describe("Blue component (0-1)"), 607 | a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)"), 608 | }, 609 | async ({ nodeId, r, g, b, a }: any) => { 610 | try { 611 | const result = await sendCommandToFigma("set_fill_color", { 612 | nodeId, 613 | color: { r, g, b, a: a || 1 }, 614 | }); 615 | const typedResult = result as { name: string }; 616 | return { 617 | content: [ 618 | { 619 | type: "text", 620 | text: `Set fill color of node "${typedResult.name 621 | }" to RGBA(${r}, ${g}, ${b}, ${a || 1})`, 622 | }, 623 | ], 624 | }; 625 | } catch (error) { 626 | return { 627 | content: [ 628 | { 629 | type: "text", 630 | text: `Error setting fill color: ${error instanceof Error ? error.message : String(error) 631 | }`, 632 | }, 633 | ], 634 | }; 635 | } 636 | } 637 | ); 638 | 639 | // Set Stroke Color Tool 640 | server.tool( 641 | "set_stroke_color", 642 | "Set the stroke color of a node in Figma", 643 | { 644 | nodeId: z.string().describe("The ID of the node to modify"), 645 | r: z.number().min(0).max(1).describe("Red component (0-1)"), 646 | g: z.number().min(0).max(1).describe("Green component (0-1)"), 647 | b: z.number().min(0).max(1).describe("Blue component (0-1)"), 648 | a: z.number().min(0).max(1).optional().describe("Alpha component (0-1)"), 649 | weight: z.number().positive().optional().describe("Stroke weight"), 650 | }, 651 | async ({ nodeId, r, g, b, a, weight }: any) => { 652 | try { 653 | const result = await sendCommandToFigma("set_stroke_color", { 654 | nodeId, 655 | color: { r, g, b, a: a || 1 }, 656 | weight: weight || 1, 657 | }); 658 | const typedResult = result as { name: string }; 659 | return { 660 | content: [ 661 | { 662 | type: "text", 663 | text: `Set stroke color of node "${typedResult.name 664 | }" to RGBA(${r}, ${g}, ${b}, ${a || 1}) with weight ${weight || 1}`, 665 | }, 666 | ], 667 | }; 668 | } catch (error) { 669 | return { 670 | content: [ 671 | { 672 | type: "text", 673 | text: `Error setting stroke color: ${error instanceof Error ? error.message : String(error) 674 | }`, 675 | }, 676 | ], 677 | }; 678 | } 679 | } 680 | ); 681 | 682 | // Move Node Tool 683 | server.tool( 684 | "move_node", 685 | "Move a node to a new position in Figma", 686 | { 687 | nodeId: z.string().describe("The ID of the node to move"), 688 | x: z.number().describe("New X position"), 689 | y: z.number().describe("New Y position"), 690 | }, 691 | async ({ nodeId, x, y }: any) => { 692 | try { 693 | const result = await sendCommandToFigma("move_node", { nodeId, x, y }); 694 | const typedResult = result as { name: string }; 695 | return { 696 | content: [ 697 | { 698 | type: "text", 699 | text: `Moved node "${typedResult.name}" to position (${x}, ${y})`, 700 | }, 701 | ], 702 | }; 703 | } catch (error) { 704 | return { 705 | content: [ 706 | { 707 | type: "text", 708 | text: `Error moving node: ${error instanceof Error ? error.message : String(error) 709 | }`, 710 | }, 711 | ], 712 | }; 713 | } 714 | } 715 | ); 716 | 717 | // Clone Node Tool 718 | server.tool( 719 | "clone_node", 720 | "Clone an existing node in Figma", 721 | { 722 | nodeId: z.string().describe("The ID of the node to clone"), 723 | x: z.number().optional().describe("New X position for the clone"), 724 | y: z.number().optional().describe("New Y position for the clone") 725 | }, 726 | async ({ nodeId, x, y }: any) => { 727 | try { 728 | const result = await sendCommandToFigma('clone_node', { nodeId, x, y }); 729 | const typedResult = result as { name: string, id: string }; 730 | return { 731 | content: [ 732 | { 733 | type: "text", 734 | text: `Cloned node "${typedResult.name}" with new ID: ${typedResult.id}${x !== undefined && y !== undefined ? ` at position (${x}, ${y})` : ''}` 735 | } 736 | ] 737 | }; 738 | } catch (error) { 739 | return { 740 | content: [ 741 | { 742 | type: "text", 743 | text: `Error cloning node: ${error instanceof Error ? error.message : String(error)}` 744 | } 745 | ] 746 | }; 747 | } 748 | } 749 | ); 750 | 751 | // Resize Node Tool 752 | server.tool( 753 | "resize_node", 754 | "Resize a node in Figma", 755 | { 756 | nodeId: z.string().describe("The ID of the node to resize"), 757 | width: z.number().positive().describe("New width"), 758 | height: z.number().positive().describe("New height"), 759 | }, 760 | async ({ nodeId, width, height }: any) => { 761 | try { 762 | const result = await sendCommandToFigma("resize_node", { 763 | nodeId, 764 | width, 765 | height, 766 | }); 767 | const typedResult = result as { name: string }; 768 | return { 769 | content: [ 770 | { 771 | type: "text", 772 | text: `Resized node "${typedResult.name}" to width ${width} and height ${height}`, 773 | }, 774 | ], 775 | }; 776 | } catch (error) { 777 | return { 778 | content: [ 779 | { 780 | type: "text", 781 | text: `Error resizing node: ${error instanceof Error ? error.message : String(error) 782 | }`, 783 | }, 784 | ], 785 | }; 786 | } 787 | } 788 | ); 789 | 790 | // Delete Node Tool 791 | server.tool( 792 | "delete_node", 793 | "Delete a node from Figma", 794 | { 795 | nodeId: z.string().describe("The ID of the node to delete"), 796 | }, 797 | async ({ nodeId }: any) => { 798 | try { 799 | await sendCommandToFigma("delete_node", { nodeId }); 800 | return { 801 | content: [ 802 | { 803 | type: "text", 804 | text: `Deleted node with ID: ${nodeId}`, 805 | }, 806 | ], 807 | }; 808 | } catch (error) { 809 | return { 810 | content: [ 811 | { 812 | type: "text", 813 | text: `Error deleting node: ${error instanceof Error ? error.message : String(error) 814 | }`, 815 | }, 816 | ], 817 | }; 818 | } 819 | } 820 | ); 821 | 822 | // Delete Multiple Nodes Tool 823 | server.tool( 824 | "delete_multiple_nodes", 825 | "Delete multiple nodes from Figma at once", 826 | { 827 | nodeIds: z.array(z.string()).describe("Array of node IDs to delete"), 828 | }, 829 | async ({ nodeIds }: any) => { 830 | try { 831 | const result = await sendCommandToFigma("delete_multiple_nodes", { nodeIds }); 832 | return { 833 | content: [ 834 | { 835 | type: "text", 836 | text: JSON.stringify(result) 837 | } 838 | ] 839 | }; 840 | } catch (error) { 841 | return { 842 | content: [ 843 | { 844 | type: "text", 845 | text: `Error deleting multiple nodes: ${error instanceof Error ? error.message : String(error) 846 | }`, 847 | }, 848 | ], 849 | }; 850 | } 851 | } 852 | ); 853 | 854 | // Export Node as Image Tool 855 | server.tool( 856 | "export_node_as_image", 857 | "Export a node as an image from Figma", 858 | { 859 | nodeId: z.string().describe("The ID of the node to export"), 860 | format: z 861 | .enum(["PNG", "JPG", "SVG", "PDF"]) 862 | .optional() 863 | .describe("Export format"), 864 | scale: z.number().positive().optional().describe("Export scale"), 865 | }, 866 | async ({ nodeId, format, scale }: any) => { 867 | try { 868 | const result = await sendCommandToFigma("export_node_as_image", { 869 | nodeId, 870 | format: format || "PNG", 871 | scale: scale || 1, 872 | }); 873 | const typedResult = result as { imageData: string; mimeType: string }; 874 | 875 | return { 876 | content: [ 877 | { 878 | type: "image", 879 | data: typedResult.imageData, 880 | mimeType: typedResult.mimeType || "image/png", 881 | }, 882 | ], 883 | }; 884 | } catch (error) { 885 | return { 886 | content: [ 887 | { 888 | type: "text", 889 | text: `Error exporting node as image: ${error instanceof Error ? error.message : String(error) 890 | }`, 891 | }, 892 | ], 893 | }; 894 | } 895 | } 896 | ); 897 | 898 | // Set Text Content Tool 899 | server.tool( 900 | "set_text_content", 901 | "Set the text content of an existing text node in Figma", 902 | { 903 | nodeId: z.string().describe("The ID of the text node to modify"), 904 | text: z.string().describe("New text content"), 905 | }, 906 | async ({ nodeId, text }: any) => { 907 | try { 908 | const result = await sendCommandToFigma("set_text_content", { 909 | nodeId, 910 | text, 911 | }); 912 | const typedResult = result as { name: string }; 913 | return { 914 | content: [ 915 | { 916 | type: "text", 917 | text: `Updated text content of node "${typedResult.name}" to "${text}"`, 918 | }, 919 | ], 920 | }; 921 | } catch (error) { 922 | return { 923 | content: [ 924 | { 925 | type: "text", 926 | text: `Error setting text content: ${error instanceof Error ? error.message : String(error) 927 | }`, 928 | }, 929 | ], 930 | }; 931 | } 932 | } 933 | ); 934 | 935 | // Get Styles Tool 936 | server.tool( 937 | "get_styles", 938 | "Get all styles from the current Figma document", 939 | {}, 940 | async () => { 941 | try { 942 | const result = await sendCommandToFigma("get_styles"); 943 | return { 944 | content: [ 945 | { 946 | type: "text", 947 | text: JSON.stringify(result) 948 | } 949 | ] 950 | }; 951 | } catch (error) { 952 | return { 953 | content: [ 954 | { 955 | type: "text", 956 | text: `Error getting styles: ${error instanceof Error ? error.message : String(error) 957 | }`, 958 | }, 959 | ], 960 | }; 961 | } 962 | } 963 | ); 964 | 965 | // Get Local Components Tool 966 | server.tool( 967 | "get_local_components", 968 | "Get all local components from the Figma document", 969 | {}, 970 | async () => { 971 | try { 972 | const result = await sendCommandToFigma("get_local_components"); 973 | return { 974 | content: [ 975 | { 976 | type: "text", 977 | text: JSON.stringify(result) 978 | } 979 | ] 980 | }; 981 | } catch (error) { 982 | return { 983 | content: [ 984 | { 985 | type: "text", 986 | text: `Error getting local components: ${error instanceof Error ? error.message : String(error) 987 | }`, 988 | }, 989 | ], 990 | }; 991 | } 992 | } 993 | ); 994 | 995 | // Get Annotations Tool 996 | server.tool( 997 | "get_annotations", 998 | "Get all annotations in the current document or specific node", 999 | { 1000 | nodeId: z.string().describe("node ID to get annotations for specific node"), 1001 | includeCategories: z.boolean().optional().default(true).describe("Whether to include category information") 1002 | }, 1003 | async ({ nodeId, includeCategories }: any) => { 1004 | try { 1005 | const result = await sendCommandToFigma("get_annotations", { 1006 | nodeId, 1007 | includeCategories 1008 | }); 1009 | return { 1010 | content: [ 1011 | { 1012 | type: "text", 1013 | text: JSON.stringify(result) 1014 | } 1015 | ] 1016 | }; 1017 | } catch (error) { 1018 | return { 1019 | content: [ 1020 | { 1021 | type: "text", 1022 | text: `Error getting annotations: ${error instanceof Error ? error.message : String(error)}` 1023 | } 1024 | ] 1025 | }; 1026 | } 1027 | } 1028 | ); 1029 | 1030 | // Set Annotation Tool 1031 | server.tool( 1032 | "set_annotation", 1033 | "Create or update an annotation", 1034 | { 1035 | nodeId: z.string().describe("The ID of the node to annotate"), 1036 | annotationId: z.string().optional().describe("The ID of the annotation to update (if updating existing annotation)"), 1037 | labelMarkdown: z.string().describe("The annotation text in markdown format"), 1038 | categoryId: z.string().optional().describe("The ID of the annotation category"), 1039 | properties: z.array(z.object({ 1040 | type: z.string() 1041 | })).optional().describe("Additional properties for the annotation") 1042 | }, 1043 | async ({ nodeId, annotationId, labelMarkdown, categoryId, properties }: any) => { 1044 | try { 1045 | const result = await sendCommandToFigma("set_annotation", { 1046 | nodeId, 1047 | annotationId, 1048 | labelMarkdown, 1049 | categoryId, 1050 | properties 1051 | }); 1052 | return { 1053 | content: [ 1054 | { 1055 | type: "text", 1056 | text: JSON.stringify(result) 1057 | } 1058 | ] 1059 | }; 1060 | } catch (error) { 1061 | return { 1062 | content: [ 1063 | { 1064 | type: "text", 1065 | text: `Error setting annotation: ${error instanceof Error ? error.message : String(error)}` 1066 | } 1067 | ] 1068 | }; 1069 | } 1070 | } 1071 | ); 1072 | 1073 | interface SetMultipleAnnotationsParams { 1074 | nodeId: string; 1075 | annotations: Array<{ 1076 | nodeId: string; 1077 | labelMarkdown: string; 1078 | categoryId?: string; 1079 | annotationId?: string; 1080 | properties?: Array<{ type: string }>; 1081 | }>; 1082 | } 1083 | 1084 | // Set Multiple Annotations Tool 1085 | server.tool( 1086 | "set_multiple_annotations", 1087 | "Set multiple annotations parallelly in a node", 1088 | { 1089 | nodeId: z 1090 | .string() 1091 | .describe("The ID of the node containing the elements to annotate"), 1092 | annotations: z 1093 | .array( 1094 | z.object({ 1095 | nodeId: z.string().describe("The ID of the node to annotate"), 1096 | labelMarkdown: z.string().describe("The annotation text in markdown format"), 1097 | categoryId: z.string().optional().describe("The ID of the annotation category"), 1098 | annotationId: z.string().optional().describe("The ID of the annotation to update (if updating existing annotation)"), 1099 | properties: z.array(z.object({ 1100 | type: z.string() 1101 | })).optional().describe("Additional properties for the annotation") 1102 | }) 1103 | ) 1104 | .describe("Array of annotations to apply"), 1105 | }, 1106 | async ({ nodeId, annotations }: any) => { 1107 | try { 1108 | if (!annotations || annotations.length === 0) { 1109 | return { 1110 | content: [ 1111 | { 1112 | type: "text", 1113 | text: "No annotations provided", 1114 | }, 1115 | ], 1116 | }; 1117 | } 1118 | 1119 | // Initial response to indicate we're starting the process 1120 | const initialStatus = { 1121 | type: "text" as const, 1122 | text: `Starting annotation process for ${annotations.length} nodes. This will be processed in batches of 5...`, 1123 | }; 1124 | 1125 | // Track overall progress 1126 | let totalProcessed = 0; 1127 | const totalToProcess = annotations.length; 1128 | 1129 | // Use the plugin's set_multiple_annotations function with chunking 1130 | const result = await sendCommandToFigma("set_multiple_annotations", { 1131 | nodeId, 1132 | annotations, 1133 | }); 1134 | 1135 | // Cast the result to a specific type to work with it safely 1136 | interface AnnotationResult { 1137 | success: boolean; 1138 | nodeId: string; 1139 | annotationsApplied?: number; 1140 | annotationsFailed?: number; 1141 | totalAnnotations?: number; 1142 | completedInChunks?: number; 1143 | results?: Array<{ 1144 | success: boolean; 1145 | nodeId: string; 1146 | error?: string; 1147 | annotationId?: string; 1148 | }>; 1149 | } 1150 | 1151 | const typedResult = result as AnnotationResult; 1152 | 1153 | // Format the results for display 1154 | const success = typedResult.annotationsApplied && typedResult.annotationsApplied > 0; 1155 | const progressText = ` 1156 | Annotation process completed: 1157 | - ${typedResult.annotationsApplied || 0} of ${totalToProcess} successfully applied 1158 | - ${typedResult.annotationsFailed || 0} failed 1159 | - Processed in ${typedResult.completedInChunks || 1} batches 1160 | `; 1161 | 1162 | // Detailed results 1163 | const detailedResults = typedResult.results || []; 1164 | const failedResults = detailedResults.filter(item => !item.success); 1165 | 1166 | // Create the detailed part of the response 1167 | let detailedResponse = ""; 1168 | if (failedResults.length > 0) { 1169 | detailedResponse = `\n\nNodes that failed:\n${failedResults.map(item => 1170 | `- ${item.nodeId}: ${item.error || "Unknown error"}` 1171 | ).join('\n')}`; 1172 | } 1173 | 1174 | return { 1175 | content: [ 1176 | initialStatus, 1177 | { 1178 | type: "text" as const, 1179 | text: progressText + detailedResponse, 1180 | }, 1181 | ], 1182 | }; 1183 | } catch (error) { 1184 | return { 1185 | content: [ 1186 | { 1187 | type: "text", 1188 | text: `Error setting multiple annotations: ${error instanceof Error ? error.message : String(error) 1189 | }`, 1190 | }, 1191 | ], 1192 | }; 1193 | } 1194 | } 1195 | ); 1196 | 1197 | // Create Component Instance Tool 1198 | server.tool( 1199 | "create_component_instance", 1200 | "Create an instance of a component in Figma", 1201 | { 1202 | componentKey: z.string().describe("Key of the component to instantiate"), 1203 | x: z.number().describe("X position"), 1204 | y: z.number().describe("Y position"), 1205 | }, 1206 | async ({ componentKey, x, y }: any) => { 1207 | try { 1208 | const result = await sendCommandToFigma("create_component_instance", { 1209 | componentKey, 1210 | x, 1211 | y, 1212 | }); 1213 | const typedResult = result as any; 1214 | return { 1215 | content: [ 1216 | { 1217 | type: "text", 1218 | text: JSON.stringify(typedResult), 1219 | } 1220 | ] 1221 | } 1222 | } catch (error) { 1223 | return { 1224 | content: [ 1225 | { 1226 | type: "text", 1227 | text: `Error creating component instance: ${error instanceof Error ? error.message : String(error) 1228 | }`, 1229 | }, 1230 | ], 1231 | }; 1232 | } 1233 | } 1234 | ); 1235 | 1236 | // Copy Instance Overrides Tool 1237 | server.tool( 1238 | "get_instance_overrides", 1239 | "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.", 1240 | { 1241 | nodeId: z.string().optional().describe("Optional ID of the component instance to get overrides from. If not provided, currently selected instance will be used."), 1242 | }, 1243 | async ({ nodeId }: any) => { 1244 | try { 1245 | const result = await sendCommandToFigma("get_instance_overrides", { 1246 | instanceNodeId: nodeId || null 1247 | }); 1248 | const typedResult = result as getInstanceOverridesResult; 1249 | 1250 | return { 1251 | content: [ 1252 | { 1253 | type: "text", 1254 | text: typedResult.success 1255 | ? `Successfully got instance overrides: ${typedResult.message}` 1256 | : `Failed to get instance overrides: ${typedResult.message}` 1257 | } 1258 | ] 1259 | }; 1260 | } catch (error) { 1261 | return { 1262 | content: [ 1263 | { 1264 | type: "text", 1265 | text: `Error copying instance overrides: ${error instanceof Error ? error.message : String(error)}` 1266 | } 1267 | ] 1268 | }; 1269 | } 1270 | } 1271 | ); 1272 | 1273 | // Set Instance Overrides Tool 1274 | server.tool( 1275 | "set_instance_overrides", 1276 | "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.", 1277 | { 1278 | sourceInstanceId: z.string().describe("ID of the source component instance"), 1279 | targetNodeIds: z.array(z.string()).describe("Array of target instance IDs. Currently selected instances will be used.") 1280 | }, 1281 | async ({ sourceInstanceId, targetNodeIds }: any) => { 1282 | try { 1283 | const result = await sendCommandToFigma("set_instance_overrides", { 1284 | sourceInstanceId: sourceInstanceId, 1285 | targetNodeIds: targetNodeIds || [] 1286 | }); 1287 | const typedResult = result as setInstanceOverridesResult; 1288 | 1289 | if (typedResult.success) { 1290 | const successCount = typedResult.results?.filter(r => r.success).length || 0; 1291 | return { 1292 | content: [ 1293 | { 1294 | type: "text", 1295 | text: `Successfully applied ${typedResult.totalCount || 0} overrides to ${successCount} instances.` 1296 | } 1297 | ] 1298 | }; 1299 | } else { 1300 | return { 1301 | content: [ 1302 | { 1303 | type: "text", 1304 | text: `Failed to set instance overrides: ${typedResult.message}` 1305 | } 1306 | ] 1307 | }; 1308 | } 1309 | } catch (error) { 1310 | return { 1311 | content: [ 1312 | { 1313 | type: "text", 1314 | text: `Error setting instance overrides: ${error instanceof Error ? error.message : String(error)}` 1315 | } 1316 | ] 1317 | }; 1318 | } 1319 | } 1320 | ); 1321 | 1322 | 1323 | // Set Corner Radius Tool 1324 | server.tool( 1325 | "set_corner_radius", 1326 | "Set the corner radius of a node in Figma", 1327 | { 1328 | nodeId: z.string().describe("The ID of the node to modify"), 1329 | radius: z.number().min(0).describe("Corner radius value"), 1330 | corners: z 1331 | .array(z.boolean()) 1332 | .length(4) 1333 | .optional() 1334 | .describe( 1335 | "Optional array of 4 booleans to specify which corners to round [topLeft, topRight, bottomRight, bottomLeft]" 1336 | ), 1337 | }, 1338 | async ({ nodeId, radius, corners }: any) => { 1339 | try { 1340 | const result = await sendCommandToFigma("set_corner_radius", { 1341 | nodeId, 1342 | radius, 1343 | corners: corners || [true, true, true, true], 1344 | }); 1345 | const typedResult = result as { name: string }; 1346 | return { 1347 | content: [ 1348 | { 1349 | type: "text", 1350 | text: `Set corner radius of node "${typedResult.name}" to ${radius}px`, 1351 | }, 1352 | ], 1353 | }; 1354 | } catch (error) { 1355 | return { 1356 | content: [ 1357 | { 1358 | type: "text", 1359 | text: `Error setting corner radius: ${error instanceof Error ? error.message : String(error) 1360 | }`, 1361 | }, 1362 | ], 1363 | }; 1364 | } 1365 | } 1366 | ); 1367 | 1368 | // Define design strategy prompt 1369 | server.prompt( 1370 | "design_strategy", 1371 | "Best practices for working with Figma designs", 1372 | (extra) => { 1373 | return { 1374 | messages: [ 1375 | { 1376 | role: "assistant", 1377 | content: { 1378 | type: "text", 1379 | text: `When working with Figma designs, follow these best practices: 1380 | 1381 | 1. Start with Document Structure: 1382 | - First use get_document_info() to understand the current document 1383 | - Plan your layout hierarchy before creating elements 1384 | - Create a main container frame for each screen/section 1385 | 1386 | 2. Naming Conventions: 1387 | - Use descriptive, semantic names for all elements 1388 | - Follow a consistent naming pattern (e.g., "Login Screen", "Logo Container", "Email Input") 1389 | - Group related elements with meaningful names 1390 | 1391 | 3. Layout Hierarchy: 1392 | - Create parent frames first, then add child elements 1393 | - For forms/login screens: 1394 | * Start with the main screen container frame 1395 | * Create a logo container at the top 1396 | * Group input fields in their own containers 1397 | * Place action buttons (login, submit) after inputs 1398 | * Add secondary elements (forgot password, signup links) last 1399 | 1400 | 4. Input Fields Structure: 1401 | - Create a container frame for each input field 1402 | - Include a label text above or inside the input 1403 | - Group related inputs (e.g., username/password) together 1404 | 1405 | 5. Element Creation: 1406 | - Use create_frame() for containers and input fields 1407 | - Use create_text() for labels, buttons text, and links 1408 | - Set appropriate colors and styles: 1409 | * Use fillColor for backgrounds 1410 | * Use strokeColor for borders 1411 | * Set proper fontWeight for different text elements 1412 | 1413 | 6. Mofifying existing elements: 1414 | - use set_text_content() to modify text content. 1415 | 1416 | 7. Visual Hierarchy: 1417 | - Position elements in logical reading order (top to bottom) 1418 | - Maintain consistent spacing between elements 1419 | - Use appropriate font sizes for different text types: 1420 | * Larger for headings/welcome text 1421 | * Medium for input labels 1422 | * Standard for button text 1423 | * Smaller for helper text/links 1424 | 1425 | 8. Best Practices: 1426 | - Verify each creation with get_node_info() 1427 | - Use parentId to maintain proper hierarchy 1428 | - Group related elements together in frames 1429 | - Keep consistent spacing and alignment 1430 | 1431 | Example Login Screen Structure: 1432 | - Login Screen (main frame) 1433 | - Logo Container (frame) 1434 | - Logo (image/text) 1435 | - Welcome Text (text) 1436 | - Input Container (frame) 1437 | - Email Input (frame) 1438 | - Email Label (text) 1439 | - Email Field (frame) 1440 | - Password Input (frame) 1441 | - Password Label (text) 1442 | - Password Field (frame) 1443 | - Login Button (frame) 1444 | - Button Text (text) 1445 | - Helper Links (frame) 1446 | - Forgot Password (text) 1447 | - Don't have account (text)`, 1448 | }, 1449 | }, 1450 | ], 1451 | description: "Best practices for working with Figma designs", 1452 | }; 1453 | } 1454 | ); 1455 | 1456 | server.prompt( 1457 | "read_design_strategy", 1458 | "Best practices for reading Figma designs", 1459 | (extra) => { 1460 | return { 1461 | messages: [ 1462 | { 1463 | role: "assistant", 1464 | content: { 1465 | type: "text", 1466 | text: `When reading Figma designs, follow these best practices: 1467 | 1468 | 1. Start with selection: 1469 | - First use read_my_design() to understand the current selection 1470 | - If no selection ask user to select single or multiple nodes 1471 | `, 1472 | }, 1473 | }, 1474 | ], 1475 | description: "Best practices for reading Figma designs", 1476 | }; 1477 | } 1478 | ); 1479 | 1480 | // Text Node Scanning Tool 1481 | server.tool( 1482 | "scan_text_nodes", 1483 | "Scan all text nodes in the selected Figma node", 1484 | { 1485 | nodeId: z.string().describe("ID of the node to scan"), 1486 | }, 1487 | async ({ nodeId }: any) => { 1488 | try { 1489 | // Initial response to indicate we're starting the process 1490 | const initialStatus = { 1491 | type: "text" as const, 1492 | text: "Starting text node scanning. This may take a moment for large designs...", 1493 | }; 1494 | 1495 | // Use the plugin's scan_text_nodes function with chunking flag 1496 | const result = await sendCommandToFigma("scan_text_nodes", { 1497 | nodeId, 1498 | useChunking: true, // Enable chunking on the plugin side 1499 | chunkSize: 10 // Process 10 nodes at a time 1500 | }); 1501 | 1502 | // If the result indicates chunking was used, format the response accordingly 1503 | if (result && typeof result === 'object' && 'chunks' in result) { 1504 | const typedResult = result as { 1505 | success: boolean, 1506 | totalNodes: number, 1507 | processedNodes: number, 1508 | chunks: number, 1509 | textNodes: Array<any> 1510 | }; 1511 | 1512 | const summaryText = ` 1513 | Scan completed: 1514 | - Found ${typedResult.totalNodes} text nodes 1515 | - Processed in ${typedResult.chunks} chunks 1516 | `; 1517 | 1518 | return { 1519 | content: [ 1520 | initialStatus, 1521 | { 1522 | type: "text" as const, 1523 | text: summaryText 1524 | }, 1525 | { 1526 | type: "text" as const, 1527 | text: JSON.stringify(typedResult.textNodes, null, 2) 1528 | } 1529 | ], 1530 | }; 1531 | } 1532 | 1533 | // If chunking wasn't used or wasn't reported in the result format, return the result as is 1534 | return { 1535 | content: [ 1536 | initialStatus, 1537 | { 1538 | type: "text", 1539 | text: JSON.stringify(result, null, 2), 1540 | }, 1541 | ], 1542 | }; 1543 | } catch (error) { 1544 | return { 1545 | content: [ 1546 | { 1547 | type: "text", 1548 | text: `Error scanning text nodes: ${error instanceof Error ? error.message : String(error) 1549 | }`, 1550 | }, 1551 | ], 1552 | }; 1553 | } 1554 | } 1555 | ); 1556 | 1557 | // Node Type Scanning Tool 1558 | server.tool( 1559 | "scan_nodes_by_types", 1560 | "Scan for child nodes with specific types in the selected Figma node", 1561 | { 1562 | nodeId: z.string().describe("ID of the node to scan"), 1563 | types: z.array(z.string()).describe("Array of node types to find in the child nodes (e.g. ['COMPONENT', 'FRAME'])") 1564 | }, 1565 | async ({ nodeId, types }: any) => { 1566 | try { 1567 | // Initial response to indicate we're starting the process 1568 | const initialStatus = { 1569 | type: "text" as const, 1570 | text: `Starting node type scanning for types: ${types.join(', ')}...`, 1571 | }; 1572 | 1573 | // Use the plugin's scan_nodes_by_types function 1574 | const result = await sendCommandToFigma("scan_nodes_by_types", { 1575 | nodeId, 1576 | types 1577 | }); 1578 | 1579 | // Format the response 1580 | if (result && typeof result === 'object' && 'matchingNodes' in result) { 1581 | const typedResult = result as { 1582 | success: boolean, 1583 | count: number, 1584 | matchingNodes: Array<{ 1585 | id: string, 1586 | name: string, 1587 | type: string, 1588 | bbox: { 1589 | x: number, 1590 | y: number, 1591 | width: number, 1592 | height: number 1593 | } 1594 | }>, 1595 | searchedTypes: Array<string> 1596 | }; 1597 | 1598 | const summaryText = `Scan completed: Found ${typedResult.count} nodes matching types: ${typedResult.searchedTypes.join(', ')}`; 1599 | 1600 | return { 1601 | content: [ 1602 | initialStatus, 1603 | { 1604 | type: "text" as const, 1605 | text: summaryText 1606 | }, 1607 | { 1608 | type: "text" as const, 1609 | text: JSON.stringify(typedResult.matchingNodes, null, 2) 1610 | } 1611 | ], 1612 | }; 1613 | } 1614 | 1615 | // If the result is in an unexpected format, return it as is 1616 | return { 1617 | content: [ 1618 | initialStatus, 1619 | { 1620 | type: "text", 1621 | text: JSON.stringify(result, null, 2), 1622 | }, 1623 | ], 1624 | }; 1625 | } catch (error) { 1626 | return { 1627 | content: [ 1628 | { 1629 | type: "text", 1630 | text: `Error scanning nodes by types: ${error instanceof Error ? error.message : String(error) 1631 | }`, 1632 | }, 1633 | ], 1634 | }; 1635 | } 1636 | } 1637 | ); 1638 | 1639 | // Text Replacement Strategy Prompt 1640 | server.prompt( 1641 | "text_replacement_strategy", 1642 | "Systematic approach for replacing text in Figma designs", 1643 | (extra) => { 1644 | return { 1645 | messages: [ 1646 | { 1647 | role: "assistant", 1648 | content: { 1649 | type: "text", 1650 | text: `# Intelligent Text Replacement Strategy 1651 | 1652 | ## 1. Analyze Design & Identify Structure 1653 | - Scan text nodes to understand the overall structure of the design 1654 | - Use AI pattern recognition to identify logical groupings: 1655 | * Tables (rows, columns, headers, cells) 1656 | * Lists (items, headers, nested lists) 1657 | * Card groups (similar cards with recurring text fields) 1658 | * Forms (labels, input fields, validation text) 1659 | * Navigation (menu items, breadcrumbs) 1660 | \`\`\` 1661 | scan_text_nodes(nodeId: "node-id") 1662 | get_node_info(nodeId: "node-id") // optional 1663 | \`\`\` 1664 | 1665 | ## 2. Strategic Chunking for Complex Designs 1666 | - Divide replacement tasks into logical content chunks based on design structure 1667 | - Use one of these chunking strategies that best fits the design: 1668 | * **Structural Chunking**: Table rows/columns, list sections, card groups 1669 | * **Spatial Chunking**: Top-to-bottom, left-to-right in screen areas 1670 | * **Semantic Chunking**: Content related to the same topic or functionality 1671 | * **Component-Based Chunking**: Process similar component instances together 1672 | 1673 | ## 3. Progressive Replacement with Verification 1674 | - Create a safe copy of the node for text replacement 1675 | - Replace text chunk by chunk with continuous progress updates 1676 | - After each chunk is processed: 1677 | * Export that section as a small, manageable image 1678 | * Verify text fits properly and maintain design integrity 1679 | * Fix issues before proceeding to the next chunk 1680 | 1681 | \`\`\` 1682 | // Clone the node to create a safe copy 1683 | clone_node(nodeId: "selected-node-id", x: [new-x], y: [new-y]) 1684 | 1685 | // Replace text chunk by chunk 1686 | set_multiple_text_contents( 1687 | nodeId: "parent-node-id", 1688 | text: [ 1689 | { nodeId: "node-id-1", text: "New text 1" }, 1690 | // More nodes in this chunk... 1691 | ] 1692 | ) 1693 | 1694 | // Verify chunk with small, targeted image exports 1695 | export_node_as_image(nodeId: "chunk-node-id", format: "PNG", scale: 0.5) 1696 | \`\`\` 1697 | 1698 | ## 4. Intelligent Handling for Table Data 1699 | - For tabular content: 1700 | * Process one row or column at a time 1701 | * Maintain alignment and spacing between cells 1702 | * Consider conditional formatting based on cell content 1703 | * Preserve header/data relationships 1704 | 1705 | ## 5. Smart Text Adaptation 1706 | - Adaptively handle text based on container constraints: 1707 | * Auto-detect space constraints and adjust text length 1708 | * Apply line breaks at appropriate linguistic points 1709 | * Maintain text hierarchy and emphasis 1710 | * Consider font scaling for critical content that must fit 1711 | 1712 | ## 6. Progressive Feedback Loop 1713 | - Establish a continuous feedback loop during replacement: 1714 | * Real-time progress updates (0-100%) 1715 | * Small image exports after each chunk for verification 1716 | * Issues identified early and resolved incrementally 1717 | * Quick adjustments applied to subsequent chunks 1718 | 1719 | ## 7. Final Verification & Context-Aware QA 1720 | - After all chunks are processed: 1721 | * Export the entire design at reduced scale for final verification 1722 | * Check for cross-chunk consistency issues 1723 | * Verify proper text flow between different sections 1724 | * Ensure design harmony across the full composition 1725 | 1726 | ## 8. Chunk-Specific Export Scale Guidelines 1727 | - Scale exports appropriately based on chunk size: 1728 | * Small chunks (1-5 elements): scale 1.0 1729 | * Medium chunks (6-20 elements): scale 0.7 1730 | * Large chunks (21-50 elements): scale 0.5 1731 | * Very large chunks (50+ elements): scale 0.3 1732 | * Full design verification: scale 0.2 1733 | 1734 | ## Sample Chunking Strategy for Common Design Types 1735 | 1736 | ### Tables 1737 | - Process by logical rows (5-10 rows per chunk) 1738 | - Alternative: Process by column for columnar analysis 1739 | - Tip: Always include header row in first chunk for reference 1740 | 1741 | ### Card Lists 1742 | - Group 3-5 similar cards per chunk 1743 | - Process entire cards to maintain internal consistency 1744 | - Verify text-to-image ratio within cards after each chunk 1745 | 1746 | ### Forms 1747 | - Group related fields (e.g., "Personal Information", "Payment Details") 1748 | - Process labels and input fields together 1749 | - Ensure validation messages and hints are updated with their fields 1750 | 1751 | ### Navigation & Menus 1752 | - Process hierarchical levels together (main menu, submenu) 1753 | - Respect information architecture relationships 1754 | - Verify menu fit and alignment after replacement 1755 | 1756 | ## Best Practices 1757 | - **Preserve Design Intent**: Always prioritize design integrity 1758 | - **Structural Consistency**: Maintain alignment, spacing, and hierarchy 1759 | - **Visual Feedback**: Verify each chunk visually before proceeding 1760 | - **Incremental Improvement**: Learn from each chunk to improve subsequent ones 1761 | - **Balance Automation & Control**: Let AI handle repetitive replacements but maintain oversight 1762 | - **Respect Content Relationships**: Keep related content consistent across chunks 1763 | 1764 | 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.`, 1765 | }, 1766 | }, 1767 | ], 1768 | description: "Systematic approach for replacing text in Figma designs", 1769 | }; 1770 | } 1771 | ); 1772 | 1773 | // Set Multiple Text Contents Tool 1774 | server.tool( 1775 | "set_multiple_text_contents", 1776 | "Set multiple text contents parallelly in a node", 1777 | { 1778 | nodeId: z 1779 | .string() 1780 | .describe("The ID of the node containing the text nodes to replace"), 1781 | text: z 1782 | .array( 1783 | z.object({ 1784 | nodeId: z.string().describe("The ID of the text node"), 1785 | text: z.string().describe("The replacement text"), 1786 | }) 1787 | ) 1788 | .describe("Array of text node IDs and their replacement texts"), 1789 | }, 1790 | async ({ nodeId, text }: any) => { 1791 | try { 1792 | if (!text || text.length === 0) { 1793 | return { 1794 | content: [ 1795 | { 1796 | type: "text", 1797 | text: "No text provided", 1798 | }, 1799 | ], 1800 | }; 1801 | } 1802 | 1803 | // Initial response to indicate we're starting the process 1804 | const initialStatus = { 1805 | type: "text" as const, 1806 | text: `Starting text replacement for ${text.length} nodes. This will be processed in batches of 5...`, 1807 | }; 1808 | 1809 | // Track overall progress 1810 | let totalProcessed = 0; 1811 | const totalToProcess = text.length; 1812 | 1813 | // Use the plugin's set_multiple_text_contents function with chunking 1814 | const result = await sendCommandToFigma("set_multiple_text_contents", { 1815 | nodeId, 1816 | text, 1817 | }); 1818 | 1819 | // Cast the result to a specific type to work with it safely 1820 | interface TextReplaceResult { 1821 | success: boolean; 1822 | nodeId: string; 1823 | replacementsApplied?: number; 1824 | replacementsFailed?: number; 1825 | totalReplacements?: number; 1826 | completedInChunks?: number; 1827 | results?: Array<{ 1828 | success: boolean; 1829 | nodeId: string; 1830 | error?: string; 1831 | originalText?: string; 1832 | translatedText?: string; 1833 | }>; 1834 | } 1835 | 1836 | const typedResult = result as TextReplaceResult; 1837 | 1838 | // Format the results for display 1839 | const success = typedResult.replacementsApplied && typedResult.replacementsApplied > 0; 1840 | const progressText = ` 1841 | Text replacement completed: 1842 | - ${typedResult.replacementsApplied || 0} of ${totalToProcess} successfully updated 1843 | - ${typedResult.replacementsFailed || 0} failed 1844 | - Processed in ${typedResult.completedInChunks || 1} batches 1845 | `; 1846 | 1847 | // Detailed results 1848 | const detailedResults = typedResult.results || []; 1849 | const failedResults = detailedResults.filter(item => !item.success); 1850 | 1851 | // Create the detailed part of the response 1852 | let detailedResponse = ""; 1853 | if (failedResults.length > 0) { 1854 | detailedResponse = `\n\nNodes that failed:\n${failedResults.map(item => 1855 | `- ${item.nodeId}: ${item.error || "Unknown error"}` 1856 | ).join('\n')}`; 1857 | } 1858 | 1859 | return { 1860 | content: [ 1861 | initialStatus, 1862 | { 1863 | type: "text" as const, 1864 | text: progressText + detailedResponse, 1865 | }, 1866 | ], 1867 | }; 1868 | } catch (error) { 1869 | return { 1870 | content: [ 1871 | { 1872 | type: "text", 1873 | text: `Error setting multiple text contents: ${error instanceof Error ? error.message : String(error) 1874 | }`, 1875 | }, 1876 | ], 1877 | }; 1878 | } 1879 | } 1880 | ); 1881 | 1882 | // Annotation Conversion Strategy Prompt 1883 | server.prompt( 1884 | "annotation_conversion_strategy", 1885 | "Strategy for converting manual annotations to Figma's native annotations", 1886 | (extra) => { 1887 | return { 1888 | messages: [ 1889 | { 1890 | role: "assistant", 1891 | content: { 1892 | type: "text", 1893 | text: `# Automatic Annotation Conversion 1894 | 1895 | ## Process Overview 1896 | 1897 | The process of converting manual annotations (numbered/alphabetical indicators with connected descriptions) to Figma's native annotations: 1898 | 1899 | 1. Get selected frame/component information 1900 | 2. Scan and collect all annotation text nodes 1901 | 3. Scan target UI elements (components, instances, frames) 1902 | 4. Match annotations to appropriate UI elements 1903 | 5. Apply native Figma annotations 1904 | 1905 | ## Step 1: Get Selection and Initial Setup 1906 | 1907 | First, get the selected frame or component that contains annotations: 1908 | 1909 | \`\`\`typescript 1910 | // Get the selected frame/component 1911 | const selection = await get_selection(); 1912 | const selectedNodeId = selection[0].id 1913 | 1914 | // Get available annotation categories for later use 1915 | const annotationData = await get_annotations({ 1916 | nodeId: selectedNodeId, 1917 | includeCategories: true 1918 | }); 1919 | const categories = annotationData.categories; 1920 | \`\`\` 1921 | 1922 | ## Step 2: Scan Annotation Text Nodes 1923 | 1924 | Scan all text nodes to identify annotations and their descriptions: 1925 | 1926 | \`\`\`typescript 1927 | // Get all text nodes in the selection 1928 | const textNodes = await scan_text_nodes({ 1929 | nodeId: selectedNodeId 1930 | }); 1931 | 1932 | // Filter and group annotation markers and descriptions 1933 | 1934 | // Markers typically have these characteristics: 1935 | // - Short text content (usually single digit/letter) 1936 | // - Specific font styles (often bold) 1937 | // - Located in a container with "Marker" or "Dot" in the name 1938 | // - Have a clear naming pattern (e.g., "1", "2", "3" or "A", "B", "C") 1939 | 1940 | 1941 | // Identify description nodes 1942 | // Usually longer text nodes near markers or with matching numbers in path 1943 | 1944 | \`\`\` 1945 | 1946 | ## Step 3: Scan Target UI Elements 1947 | 1948 | Get all potential target elements that annotations might refer to: 1949 | 1950 | \`\`\`typescript 1951 | // Scan for all UI elements that could be annotation targets 1952 | const targetNodes = await scan_nodes_by_types({ 1953 | nodeId: selectedNodeId, 1954 | types: [ 1955 | "COMPONENT", 1956 | "INSTANCE", 1957 | "FRAME" 1958 | ] 1959 | }); 1960 | \`\`\` 1961 | 1962 | ## Step 4: Match Annotations to Targets 1963 | 1964 | Match each annotation to its target UI element using these strategies in order of priority: 1965 | 1966 | 1. **Path-Based Matching**: 1967 | - Look at the marker's parent container name in the Figma layer hierarchy 1968 | - Remove any "Marker:" or "Annotation:" prefixes from the parent name 1969 | - Find UI elements that share the same parent name or have it in their path 1970 | - This works well when markers are grouped with their target elements 1971 | 1972 | 2. **Name-Based Matching**: 1973 | - Extract key terms from the annotation description 1974 | - Look for UI elements whose names contain these key terms 1975 | - Consider both exact matches and semantic similarities 1976 | - Particularly effective for form fields, buttons, and labeled components 1977 | 1978 | 3. **Proximity-Based Matching** (fallback): 1979 | - Calculate the center point of the marker 1980 | - Find the closest UI element by measuring distances to element centers 1981 | - Consider the marker's position relative to nearby elements 1982 | - Use this method when other matching strategies fail 1983 | 1984 | Additional Matching Considerations: 1985 | - Give higher priority to matches found through path-based matching 1986 | - Consider the type of UI element when evaluating matches 1987 | - Take into account the annotation's context and content 1988 | - Use a combination of strategies for more accurate matching 1989 | 1990 | ## Step 5: Apply Native Annotations 1991 | 1992 | Convert matched annotations to Figma's native annotations using batch processing: 1993 | 1994 | \`\`\`typescript 1995 | // Prepare annotations array for batch processing 1996 | const annotationsToApply = Object.values(annotations).map(({ marker, description }) => { 1997 | // Find target using multiple strategies 1998 | const target = 1999 | findTargetByPath(marker, targetNodes) || 2000 | findTargetByName(description, targetNodes) || 2001 | findTargetByProximity(marker, targetNodes); 2002 | 2003 | if (target) { 2004 | // Determine appropriate category based on content 2005 | const category = determineCategory(description.characters, categories); 2006 | 2007 | // Determine appropriate additional annotationProperty based on content 2008 | const annotationProperty = determineProperties(description.characters, target.type); 2009 | 2010 | return { 2011 | nodeId: target.id, 2012 | labelMarkdown: description.characters, 2013 | categoryId: category.id, 2014 | properties: annotationProperty 2015 | }; 2016 | } 2017 | return null; 2018 | }).filter(Boolean); // Remove null entries 2019 | 2020 | // Apply annotations in batches using set_multiple_annotations 2021 | if (annotationsToApply.length > 0) { 2022 | await set_multiple_annotations({ 2023 | nodeId: selectedNodeId, 2024 | annotations: annotationsToApply 2025 | }); 2026 | } 2027 | \`\`\` 2028 | 2029 | 2030 | 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.` 2031 | }, 2032 | }, 2033 | ], 2034 | description: "Strategy for converting manual annotations to Figma's native annotations", 2035 | }; 2036 | } 2037 | ); 2038 | 2039 | // Instance Slot Filling Strategy Prompt 2040 | server.prompt( 2041 | "swap_overrides_instances", 2042 | "Guide to swap instance overrides between instances", 2043 | (extra) => { 2044 | return { 2045 | messages: [ 2046 | { 2047 | role: "assistant", 2048 | content: { 2049 | type: "text", 2050 | text: `# Swap Component Instance and Override Strategy 2051 | 2052 | ## Overview 2053 | 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. 2054 | 2055 | ## Step-by-Step Process 2056 | 2057 | ### 1. Selection Analysis 2058 | - Use \`get_selection()\` to identify the parent component or selected instances 2059 | - For parent components, scan for instances with \`scan_nodes_by_types({ nodeId: "parent-id", types: ["INSTANCE"] })\` 2060 | - Identify custom slots by name patterns (e.g. "Custom Slot*" or "Instance Slot") or by examining text content 2061 | - Determine which is the source instance (with content to copy) and which are targets (where to apply content) 2062 | 2063 | ### 2. Extract Source Overrides 2064 | - Use \`get_instance_overrides()\` to extract customizations from the source instance 2065 | - This captures text content, property values, and style overrides 2066 | - Command syntax: \`get_instance_overrides({ nodeId: "source-instance-id" })\` 2067 | - Look for successful response like "Got component information from [instance name]" 2068 | 2069 | ### 3. Apply Overrides to Targets 2070 | - Apply captured overrides using \`set_instance_overrides()\` 2071 | - Command syntax: 2072 | \`\`\` 2073 | set_instance_overrides({ 2074 | sourceInstanceId: "source-instance-id", 2075 | targetNodeIds: ["target-id-1", "target-id-2", ...] 2076 | }) 2077 | \`\`\` 2078 | 2079 | ### 4. Verification 2080 | - Verify results with \`get_node_info()\` or \`read_my_design()\` 2081 | - Confirm text content and style overrides have transferred successfully 2082 | 2083 | ## Key Tips 2084 | - Always join the appropriate channel first with \`join_channel()\` 2085 | - When working with multiple targets, check the full selection with \`get_selection()\` 2086 | - Preserve component relationships by using instance overrides rather than direct text manipulation`, 2087 | }, 2088 | }, 2089 | ], 2090 | description: "Strategy for transferring overrides between component instances in Figma", 2091 | }; 2092 | } 2093 | ); 2094 | 2095 | // Set Layout Mode Tool 2096 | server.tool( 2097 | "set_layout_mode", 2098 | "Set the layout mode and wrap behavior of a frame in Figma", 2099 | { 2100 | nodeId: z.string().describe("The ID of the frame to modify"), 2101 | layoutMode: z.enum(["NONE", "HORIZONTAL", "VERTICAL"]).describe("Layout mode for the frame"), 2102 | layoutWrap: z.enum(["NO_WRAP", "WRAP"]).optional().describe("Whether the auto-layout frame wraps its children") 2103 | }, 2104 | async ({ nodeId, layoutMode, layoutWrap }: any) => { 2105 | try { 2106 | const result = await sendCommandToFigma("set_layout_mode", { 2107 | nodeId, 2108 | layoutMode, 2109 | layoutWrap: layoutWrap || "NO_WRAP" 2110 | }); 2111 | const typedResult = result as { name: string }; 2112 | return { 2113 | content: [ 2114 | { 2115 | type: "text", 2116 | text: `Set layout mode of frame "${typedResult.name}" to ${layoutMode}${layoutWrap ? ` with ${layoutWrap}` : ''}`, 2117 | }, 2118 | ], 2119 | }; 2120 | } catch (error) { 2121 | return { 2122 | content: [ 2123 | { 2124 | type: "text", 2125 | text: `Error setting layout mode: ${error instanceof Error ? error.message : String(error)}`, 2126 | }, 2127 | ], 2128 | }; 2129 | } 2130 | } 2131 | ); 2132 | 2133 | // Set Padding Tool 2134 | server.tool( 2135 | "set_padding", 2136 | "Set padding values for an auto-layout frame in Figma", 2137 | { 2138 | nodeId: z.string().describe("The ID of the frame to modify"), 2139 | paddingTop: z.number().optional().describe("Top padding value"), 2140 | paddingRight: z.number().optional().describe("Right padding value"), 2141 | paddingBottom: z.number().optional().describe("Bottom padding value"), 2142 | paddingLeft: z.number().optional().describe("Left padding value"), 2143 | }, 2144 | async ({ nodeId, paddingTop, paddingRight, paddingBottom, paddingLeft }: any) => { 2145 | try { 2146 | const result = await sendCommandToFigma("set_padding", { 2147 | nodeId, 2148 | paddingTop, 2149 | paddingRight, 2150 | paddingBottom, 2151 | paddingLeft, 2152 | }); 2153 | const typedResult = result as { name: string }; 2154 | 2155 | // Create a message about which padding values were set 2156 | const paddingMessages = []; 2157 | if (paddingTop !== undefined) paddingMessages.push(`top: ${paddingTop}`); 2158 | if (paddingRight !== undefined) paddingMessages.push(`right: ${paddingRight}`); 2159 | if (paddingBottom !== undefined) paddingMessages.push(`bottom: ${paddingBottom}`); 2160 | if (paddingLeft !== undefined) paddingMessages.push(`left: ${paddingLeft}`); 2161 | 2162 | const paddingText = paddingMessages.length > 0 2163 | ? `padding (${paddingMessages.join(', ')})` 2164 | : "padding"; 2165 | 2166 | return { 2167 | content: [ 2168 | { 2169 | type: "text", 2170 | text: `Set ${paddingText} for frame "${typedResult.name}"`, 2171 | }, 2172 | ], 2173 | }; 2174 | } catch (error) { 2175 | return { 2176 | content: [ 2177 | { 2178 | type: "text", 2179 | text: `Error setting padding: ${error instanceof Error ? error.message : String(error)}`, 2180 | }, 2181 | ], 2182 | }; 2183 | } 2184 | } 2185 | ); 2186 | 2187 | // Set Axis Align Tool 2188 | server.tool( 2189 | "set_axis_align", 2190 | "Set primary and counter axis alignment for an auto-layout frame in Figma", 2191 | { 2192 | nodeId: z.string().describe("The ID of the frame to modify"), 2193 | primaryAxisAlignItems: z 2194 | .enum(["MIN", "MAX", "CENTER", "SPACE_BETWEEN"]) 2195 | .optional() 2196 | .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."), 2197 | counterAxisAlignItems: z 2198 | .enum(["MIN", "MAX", "CENTER", "BASELINE"]) 2199 | .optional() 2200 | .describe("Counter axis alignment (MIN/MAX = top/bottom in horizontal, left/right in vertical)") 2201 | }, 2202 | async ({ nodeId, primaryAxisAlignItems, counterAxisAlignItems }: any) => { 2203 | try { 2204 | const result = await sendCommandToFigma("set_axis_align", { 2205 | nodeId, 2206 | primaryAxisAlignItems, 2207 | counterAxisAlignItems 2208 | }); 2209 | const typedResult = result as { name: string }; 2210 | 2211 | // Create a message about which alignments were set 2212 | const alignMessages = []; 2213 | if (primaryAxisAlignItems !== undefined) alignMessages.push(`primary: ${primaryAxisAlignItems}`); 2214 | if (counterAxisAlignItems !== undefined) alignMessages.push(`counter: ${counterAxisAlignItems}`); 2215 | 2216 | const alignText = alignMessages.length > 0 2217 | ? `axis alignment (${alignMessages.join(', ')})` 2218 | : "axis alignment"; 2219 | 2220 | return { 2221 | content: [ 2222 | { 2223 | type: "text", 2224 | text: `Set ${alignText} for frame "${typedResult.name}"`, 2225 | }, 2226 | ], 2227 | }; 2228 | } catch (error) { 2229 | return { 2230 | content: [ 2231 | { 2232 | type: "text", 2233 | text: `Error setting axis alignment: ${error instanceof Error ? error.message : String(error)}`, 2234 | }, 2235 | ], 2236 | }; 2237 | } 2238 | } 2239 | ); 2240 | 2241 | // Set Layout Sizing Tool 2242 | server.tool( 2243 | "set_layout_sizing", 2244 | "Set horizontal and vertical sizing modes for an auto-layout frame in Figma", 2245 | { 2246 | nodeId: z.string().describe("The ID of the frame to modify"), 2247 | layoutSizingHorizontal: z 2248 | .enum(["FIXED", "HUG", "FILL"]) 2249 | .optional() 2250 | .describe("Horizontal sizing mode (HUG for frames/text only, FILL for auto-layout children only)"), 2251 | layoutSizingVertical: z 2252 | .enum(["FIXED", "HUG", "FILL"]) 2253 | .optional() 2254 | .describe("Vertical sizing mode (HUG for frames/text only, FILL for auto-layout children only)") 2255 | }, 2256 | async ({ nodeId, layoutSizingHorizontal, layoutSizingVertical }: any) => { 2257 | try { 2258 | const result = await sendCommandToFigma("set_layout_sizing", { 2259 | nodeId, 2260 | layoutSizingHorizontal, 2261 | layoutSizingVertical 2262 | }); 2263 | const typedResult = result as { name: string }; 2264 | 2265 | // Create a message about which sizing modes were set 2266 | const sizingMessages = []; 2267 | if (layoutSizingHorizontal !== undefined) sizingMessages.push(`horizontal: ${layoutSizingHorizontal}`); 2268 | if (layoutSizingVertical !== undefined) sizingMessages.push(`vertical: ${layoutSizingVertical}`); 2269 | 2270 | const sizingText = sizingMessages.length > 0 2271 | ? `layout sizing (${sizingMessages.join(', ')})` 2272 | : "layout sizing"; 2273 | 2274 | return { 2275 | content: [ 2276 | { 2277 | type: "text", 2278 | text: `Set ${sizingText} for frame "${typedResult.name}"`, 2279 | }, 2280 | ], 2281 | }; 2282 | } catch (error) { 2283 | return { 2284 | content: [ 2285 | { 2286 | type: "text", 2287 | text: `Error setting layout sizing: ${error instanceof Error ? error.message : String(error)}`, 2288 | }, 2289 | ], 2290 | }; 2291 | } 2292 | } 2293 | ); 2294 | 2295 | // Set Item Spacing Tool 2296 | server.tool( 2297 | "set_item_spacing", 2298 | "Set distance between children in an auto-layout frame", 2299 | { 2300 | nodeId: z.string().describe("The ID of the frame to modify"), 2301 | itemSpacing: z.number().optional().describe("Distance between children. Note: This value will be ignored if primaryAxisAlignItems is set to SPACE_BETWEEN."), 2302 | counterAxisSpacing: z.number().optional().describe("Distance between wrapped rows/columns. Only works when layoutWrap is set to WRAP.") 2303 | }, 2304 | async ({ nodeId, itemSpacing, counterAxisSpacing}: any) => { 2305 | try { 2306 | const params: any = { nodeId }; 2307 | if (itemSpacing !== undefined) params.itemSpacing = itemSpacing; 2308 | if (counterAxisSpacing !== undefined) params.counterAxisSpacing = counterAxisSpacing; 2309 | 2310 | const result = await sendCommandToFigma("set_item_spacing", params); 2311 | const typedResult = result as { name: string, itemSpacing?: number, counterAxisSpacing?: number }; 2312 | 2313 | let message = `Updated spacing for frame "${typedResult.name}":`; 2314 | if (itemSpacing !== undefined) message += ` itemSpacing=${itemSpacing}`; 2315 | if (counterAxisSpacing !== undefined) message += ` counterAxisSpacing=${counterAxisSpacing}`; 2316 | 2317 | return { 2318 | content: [ 2319 | { 2320 | type: "text", 2321 | text: message, 2322 | }, 2323 | ], 2324 | }; 2325 | } catch (error) { 2326 | return { 2327 | content: [ 2328 | { 2329 | type: "text", 2330 | text: `Error setting spacing: ${error instanceof Error ? error.message : String(error)}`, 2331 | }, 2332 | ], 2333 | }; 2334 | } 2335 | } 2336 | ); 2337 | 2338 | // A tool to get Figma Prototyping Reactions from multiple nodes 2339 | server.tool( 2340 | "get_reactions", 2341 | "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.", 2342 | { 2343 | nodeIds: z.array(z.string()).describe("Array of node IDs to get reactions from"), 2344 | }, 2345 | async ({ nodeIds }: any) => { 2346 | try { 2347 | const result = await sendCommandToFigma("get_reactions", { nodeIds }); 2348 | return { 2349 | content: [ 2350 | { 2351 | type: "text", 2352 | text: JSON.stringify(result) 2353 | }, 2354 | { 2355 | type: "text", 2356 | 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." 2357 | } 2358 | ], 2359 | followUp: { 2360 | type: "prompt", 2361 | prompt: "reaction_to_connector_strategy", 2362 | }, 2363 | }; 2364 | } catch (error) { 2365 | return { 2366 | content: [ 2367 | { 2368 | type: "text", 2369 | text: `Error getting reactions: ${error instanceof Error ? error.message : String(error) 2370 | }`, 2371 | }, 2372 | ], 2373 | }; 2374 | } 2375 | } 2376 | ); 2377 | 2378 | // Create Connectors Tool 2379 | server.tool( 2380 | "set_default_connector", 2381 | "Set a copied connector node as the default connector", 2382 | { 2383 | connectorId: z.string().optional().describe("The ID of the connector node to set as default") 2384 | }, 2385 | async ({ connectorId }: any) => { 2386 | try { 2387 | const result = await sendCommandToFigma("set_default_connector", { 2388 | connectorId 2389 | }); 2390 | 2391 | return { 2392 | content: [ 2393 | { 2394 | type: "text", 2395 | text: `Default connector set: ${JSON.stringify(result)}` 2396 | } 2397 | ] 2398 | }; 2399 | } catch (error) { 2400 | return { 2401 | content: [ 2402 | { 2403 | type: "text", 2404 | text: `Error setting default connector: ${error instanceof Error ? error.message : String(error)}` 2405 | } 2406 | ] 2407 | }; 2408 | } 2409 | } 2410 | ); 2411 | 2412 | // Connect Nodes Tool 2413 | server.tool( 2414 | "create_connections", 2415 | "Create connections between nodes using the default connector style", 2416 | { 2417 | connections: z.array(z.object({ 2418 | startNodeId: z.string().describe("ID of the starting node"), 2419 | endNodeId: z.string().describe("ID of the ending node"), 2420 | text: z.string().optional().describe("Optional text to display on the connector") 2421 | })).describe("Array of node connections to create") 2422 | }, 2423 | async ({ connections }: any) => { 2424 | try { 2425 | if (!connections || connections.length === 0) { 2426 | return { 2427 | content: [ 2428 | { 2429 | type: "text", 2430 | text: "No connections provided" 2431 | } 2432 | ] 2433 | }; 2434 | } 2435 | 2436 | const result = await sendCommandToFigma("create_connections", { 2437 | connections 2438 | }); 2439 | 2440 | return { 2441 | content: [ 2442 | { 2443 | type: "text", 2444 | text: `Created ${connections.length} connections: ${JSON.stringify(result)}` 2445 | } 2446 | ] 2447 | }; 2448 | } catch (error) { 2449 | return { 2450 | content: [ 2451 | { 2452 | type: "text", 2453 | text: `Error creating connections: ${error instanceof Error ? error.message : String(error)}` 2454 | } 2455 | ] 2456 | }; 2457 | } 2458 | } 2459 | ); 2460 | 2461 | // Set Focus Tool 2462 | server.tool( 2463 | "set_focus", 2464 | "Set focus on a specific node in Figma by selecting it and scrolling viewport to it", 2465 | { 2466 | nodeId: z.string().describe("The ID of the node to focus on"), 2467 | }, 2468 | async ({ nodeId }: any) => { 2469 | try { 2470 | const result = await sendCommandToFigma("set_focus", { nodeId }); 2471 | const typedResult = result as { name: string; id: string }; 2472 | return { 2473 | content: [ 2474 | { 2475 | type: "text", 2476 | text: `Focused on node "${typedResult.name}" (ID: ${typedResult.id})`, 2477 | }, 2478 | ], 2479 | }; 2480 | } catch (error) { 2481 | return { 2482 | content: [ 2483 | { 2484 | type: "text", 2485 | text: `Error setting focus: ${error instanceof Error ? error.message : String(error)}`, 2486 | }, 2487 | ], 2488 | }; 2489 | } 2490 | } 2491 | ); 2492 | 2493 | // Set Selections Tool 2494 | server.tool( 2495 | "set_selections", 2496 | "Set selection to multiple nodes in Figma and scroll viewport to show them", 2497 | { 2498 | nodeIds: z.array(z.string()).describe("Array of node IDs to select"), 2499 | }, 2500 | async ({ nodeIds }: any) => { 2501 | try { 2502 | const result = await sendCommandToFigma("set_selections", { nodeIds }); 2503 | const typedResult = result as { selectedNodes: Array<{ name: string; id: string }>; count: number }; 2504 | return { 2505 | content: [ 2506 | { 2507 | type: "text", 2508 | text: `Selected ${typedResult.count} nodes: ${typedResult.selectedNodes.map(node => `"${node.name}" (${node.id})`).join(', ')}`, 2509 | }, 2510 | ], 2511 | }; 2512 | } catch (error) { 2513 | return { 2514 | content: [ 2515 | { 2516 | type: "text", 2517 | text: `Error setting selections: ${error instanceof Error ? error.message : String(error)}`, 2518 | }, 2519 | ], 2520 | }; 2521 | } 2522 | } 2523 | ); 2524 | 2525 | // Strategy for converting Figma prototype reactions to connector lines 2526 | server.prompt( 2527 | "reaction_to_connector_strategy", 2528 | "Strategy for converting Figma prototype reactions to connector lines using the output of 'get_reactions'", 2529 | (extra) => { 2530 | return { 2531 | messages: [ 2532 | { 2533 | role: "assistant", 2534 | content: { 2535 | type: "text", 2536 | text: `# Strategy: Convert Figma Prototype Reactions to Connector Lines 2537 | 2538 | ## Goal 2539 | 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. 2540 | 2541 | ## Input Data 2542 | 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: 2543 | \`\`\`json 2544 | { 2545 | "trigger": { "type": "ON_CLICK" }, 2546 | "action": { 2547 | "type": "NAVIGATE", 2548 | "destinationId": "destination-node-id", 2549 | "navigationTransition": { ... }, 2550 | "preserveScrollPosition": false 2551 | } 2552 | } 2553 | \`\`\` 2554 | 2555 | ## Step-by-Step Process 2556 | 2557 | ### 1. Preparation & Context Gathering 2558 | - **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. 2559 | - **Action:** Call \`set_default_connector\` **without** the \`connectorId\` parameter. 2560 | - **Check Result:** Analyze the response from \`set_default_connector\`. 2561 | - If it confirms a default connector is already set (e.g., "Default connector is already set"), proceed to Step 2. 2562 | - 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.** 2563 | 2564 | ### 2. Filter and Transform Reactions from \`get_reactions\` Output 2565 | - **Iterate:** Go through the JSON array provided by \`get_reactions\`. For each node in the array: 2566 | - Iterate through its \`reactions\` array. 2567 | - **Filter:** Keep only reactions where the \`action\` meets these criteria: 2568 | - Has a \`type\` that implies a connection (e.g., \`NAVIGATE\`, \`OPEN_OVERLAY\`, \`SWAP_OVERLAY\`). **Ignore** types like \`CHANGE_TO\`, \`CLOSE_OVERLAY\`, etc. 2569 | - Has a valid \`destinationId\` property. 2570 | - **Extract:** For each valid reaction, extract the following information: 2571 | - \`sourceNodeId\`: The ID of the node the reaction belongs to (from the outer loop). 2572 | - \`destinationNodeId\`: The value of \`action.destinationId\`. 2573 | - \`actionType\`: The value of \`action.type\`. 2574 | - \`triggerType\`: The value of \`trigger.type\`. 2575 | 2576 | ### 3. Generate Connector Text Labels 2577 | - **For each extracted connection:** Create a concise, descriptive text label string. 2578 | - **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. 2579 | - **Example Labels:** 2580 | - If \`triggerType\` is "ON\_CLICK" and \`actionType\` is "NAVIGATE": "On click, navigate to [Destination Node Name]" 2581 | - If \`triggerType\` is "ON\_DRAG" and \`actionType\` is "OPEN\_OVERLAY": "On drag, open [Destination Node Name] overlay" 2582 | - **Keep it brief and informative.** Let this generated string be \`generatedText\`. 2583 | 2584 | ### 4. Prepare the \`connections\` Array for \`create_connections\` 2585 | - **Structure:** Create a JSON array where each element is an object representing a connection. 2586 | - **Format:** Each object in the array must have the following structure: 2587 | \`\`\`json 2588 | { 2589 | "startNodeId": "sourceNodeId_from_step_2", 2590 | "endNodeId": "destinationNodeId_from_step_2", 2591 | "text": "generatedText_from_step_3" 2592 | } 2593 | \`\`\` 2594 | - **Result:** This final array is the value you will pass to the \`connections\` parameter when calling the \`create_connections\` tool. 2595 | 2596 | ### 5. Execute Connection Creation 2597 | - **Action:** Call the \`create_connections\` tool, passing the array generated in Step 4 as the \`connections\` argument. 2598 | - **Verify:** Check the response from \`create_connections\` to confirm success or failure. 2599 | 2600 | This detailed process ensures you correctly interpret the reaction data, prepare the necessary information, and use the appropriate tools to create the connector lines.` 2601 | }, 2602 | }, 2603 | ], 2604 | description: "Strategy for converting Figma prototype reactions to connector lines using the output of 'get_reactions'", 2605 | }; 2606 | } 2607 | ); 2608 | 2609 | 2610 | // Define command types and parameters 2611 | type FigmaCommand = 2612 | | "get_document_info" 2613 | | "get_selection" 2614 | | "get_node_info" 2615 | | "get_nodes_info" 2616 | | "read_my_design" 2617 | | "create_rectangle" 2618 | | "create_frame" 2619 | | "create_text" 2620 | | "set_fill_color" 2621 | | "set_stroke_color" 2622 | | "move_node" 2623 | | "resize_node" 2624 | | "delete_node" 2625 | | "delete_multiple_nodes" 2626 | | "get_styles" 2627 | | "get_local_components" 2628 | | "create_component_instance" 2629 | | "get_instance_overrides" 2630 | | "set_instance_overrides" 2631 | | "export_node_as_image" 2632 | | "join" 2633 | | "set_corner_radius" 2634 | | "clone_node" 2635 | | "set_text_content" 2636 | | "scan_text_nodes" 2637 | | "set_multiple_text_contents" 2638 | | "get_annotations" 2639 | | "set_annotation" 2640 | | "set_multiple_annotations" 2641 | | "scan_nodes_by_types" 2642 | | "set_layout_mode" 2643 | | "set_padding" 2644 | | "set_axis_align" 2645 | | "set_layout_sizing" 2646 | | "set_item_spacing" 2647 | | "get_reactions" 2648 | | "set_default_connector" 2649 | | "create_connections" 2650 | | "set_focus" 2651 | | "set_selections"; 2652 | 2653 | type CommandParams = { 2654 | get_document_info: Record<string, never>; 2655 | get_selection: Record<string, never>; 2656 | get_node_info: { nodeId: string }; 2657 | get_nodes_info: { nodeIds: string[] }; 2658 | create_rectangle: { 2659 | x: number; 2660 | y: number; 2661 | width: number; 2662 | height: number; 2663 | name?: string; 2664 | parentId?: string; 2665 | }; 2666 | create_frame: { 2667 | x: number; 2668 | y: number; 2669 | width: number; 2670 | height: number; 2671 | name?: string; 2672 | parentId?: string; 2673 | fillColor?: { r: number; g: number; b: number; a?: number }; 2674 | strokeColor?: { r: number; g: number; b: number; a?: number }; 2675 | strokeWeight?: number; 2676 | }; 2677 | create_text: { 2678 | x: number; 2679 | y: number; 2680 | text: string; 2681 | fontSize?: number; 2682 | fontWeight?: number; 2683 | fontColor?: { r: number; g: number; b: number; a?: number }; 2684 | name?: string; 2685 | parentId?: string; 2686 | }; 2687 | set_fill_color: { 2688 | nodeId: string; 2689 | r: number; 2690 | g: number; 2691 | b: number; 2692 | a?: number; 2693 | }; 2694 | set_stroke_color: { 2695 | nodeId: string; 2696 | r: number; 2697 | g: number; 2698 | b: number; 2699 | a?: number; 2700 | weight?: number; 2701 | }; 2702 | move_node: { 2703 | nodeId: string; 2704 | x: number; 2705 | y: number; 2706 | }; 2707 | resize_node: { 2708 | nodeId: string; 2709 | width: number; 2710 | height: number; 2711 | }; 2712 | delete_node: { 2713 | nodeId: string; 2714 | }; 2715 | delete_multiple_nodes: { 2716 | nodeIds: string[]; 2717 | }; 2718 | get_styles: Record<string, never>; 2719 | get_local_components: Record<string, never>; 2720 | get_team_components: Record<string, never>; 2721 | create_component_instance: { 2722 | componentKey: string; 2723 | x: number; 2724 | y: number; 2725 | }; 2726 | get_instance_overrides: { 2727 | instanceNodeId: string | null; 2728 | }; 2729 | set_instance_overrides: { 2730 | targetNodeIds: string[]; 2731 | sourceInstanceId: string; 2732 | }; 2733 | export_node_as_image: { 2734 | nodeId: string; 2735 | format?: "PNG" | "JPG" | "SVG" | "PDF"; 2736 | scale?: number; 2737 | }; 2738 | execute_code: { 2739 | code: string; 2740 | }; 2741 | join: { 2742 | channel: string; 2743 | }; 2744 | set_corner_radius: { 2745 | nodeId: string; 2746 | radius: number; 2747 | corners?: boolean[]; 2748 | }; 2749 | clone_node: { 2750 | nodeId: string; 2751 | x?: number; 2752 | y?: number; 2753 | }; 2754 | set_text_content: { 2755 | nodeId: string; 2756 | text: string; 2757 | }; 2758 | scan_text_nodes: { 2759 | nodeId: string; 2760 | useChunking: boolean; 2761 | chunkSize: number; 2762 | }; 2763 | set_multiple_text_contents: { 2764 | nodeId: string; 2765 | text: Array<{ nodeId: string; text: string }>; 2766 | }; 2767 | get_annotations: { 2768 | nodeId?: string; 2769 | includeCategories?: boolean; 2770 | }; 2771 | set_annotation: { 2772 | nodeId: string; 2773 | annotationId?: string; 2774 | labelMarkdown: string; 2775 | categoryId?: string; 2776 | properties?: Array<{ type: string }>; 2777 | }; 2778 | set_multiple_annotations: SetMultipleAnnotationsParams; 2779 | scan_nodes_by_types: { 2780 | nodeId: string; 2781 | types: Array<string>; 2782 | }; 2783 | get_reactions: { nodeIds: string[] }; 2784 | set_default_connector: { 2785 | connectorId?: string | undefined; 2786 | }; 2787 | create_connections: { 2788 | connections: Array<{ 2789 | startNodeId: string; 2790 | endNodeId: string; 2791 | text?: string; 2792 | }>; 2793 | }; 2794 | set_focus: { 2795 | nodeId: string; 2796 | }; 2797 | set_selections: { 2798 | nodeIds: string[]; 2799 | }; 2800 | 2801 | }; 2802 | 2803 | 2804 | // Helper function to process Figma node responses 2805 | function processFigmaNodeResponse(result: unknown): any { 2806 | if (!result || typeof result !== "object") { 2807 | return result; 2808 | } 2809 | 2810 | // Check if this looks like a node response 2811 | const resultObj = result as Record<string, unknown>; 2812 | if ("id" in resultObj && typeof resultObj.id === "string") { 2813 | // It appears to be a node response, log the details 2814 | console.info( 2815 | `Processed Figma node: ${resultObj.name || "Unknown"} (ID: ${resultObj.id 2816 | })` 2817 | ); 2818 | 2819 | if ("x" in resultObj && "y" in resultObj) { 2820 | console.debug(`Node position: (${resultObj.x}, ${resultObj.y})`); 2821 | } 2822 | 2823 | if ("width" in resultObj && "height" in resultObj) { 2824 | console.debug(`Node dimensions: ${resultObj.width}×${resultObj.height}`); 2825 | } 2826 | } 2827 | 2828 | return result; 2829 | } 2830 | 2831 | // Update the connectToFigma function 2832 | function connectToFigma(port: number = 3055) { 2833 | // If already connected, do nothing 2834 | if (ws && ws.readyState === WebSocket.OPEN) { 2835 | logger.info('Already connected to Figma'); 2836 | return; 2837 | } 2838 | 2839 | const wsUrl = serverUrl === 'localhost' ? `${WS_URL}:${port}` : WS_URL; 2840 | logger.info(`Connecting to Figma socket server at ${wsUrl}...`); 2841 | ws = new WebSocket(wsUrl); 2842 | 2843 | ws.on('open', () => { 2844 | logger.info('Connected to Figma socket server'); 2845 | // Reset channel on new connection 2846 | currentChannel = null; 2847 | }); 2848 | 2849 | ws.on("message", (data: any) => { 2850 | try { 2851 | // Define a more specific type with an index signature to allow any property access 2852 | interface ProgressMessage { 2853 | message: FigmaResponse | any; 2854 | type?: string; 2855 | id?: string; 2856 | [key: string]: any; // Allow any other properties 2857 | } 2858 | 2859 | const json = JSON.parse(data) as ProgressMessage; 2860 | 2861 | // Handle progress updates 2862 | if (json.type === 'progress_update') { 2863 | const progressData = json.message.data as CommandProgressUpdate; 2864 | const requestId = json.id || ''; 2865 | 2866 | if (requestId && pendingRequests.has(requestId)) { 2867 | const request = pendingRequests.get(requestId)!; 2868 | 2869 | // Update last activity timestamp 2870 | request.lastActivity = Date.now(); 2871 | 2872 | // Reset the timeout to prevent timeouts during long-running operations 2873 | clearTimeout(request.timeout); 2874 | 2875 | // Create a new timeout 2876 | request.timeout = setTimeout(() => { 2877 | if (pendingRequests.has(requestId)) { 2878 | logger.error(`Request ${requestId} timed out after extended period of inactivity`); 2879 | pendingRequests.delete(requestId); 2880 | request.reject(new Error('Request to Figma timed out')); 2881 | } 2882 | }, 60000); // 60 second timeout for inactivity 2883 | 2884 | // Log progress 2885 | logger.info(`Progress update for ${progressData.commandType}: ${progressData.progress}% - ${progressData.message}`); 2886 | 2887 | // For completed updates, we could resolve the request early if desired 2888 | if (progressData.status === 'completed' && progressData.progress === 100) { 2889 | // Optionally resolve early with partial data 2890 | // request.resolve(progressData.payload); 2891 | // pendingRequests.delete(requestId); 2892 | 2893 | // Instead, just log the completion, wait for final result from Figma 2894 | logger.info(`Operation ${progressData.commandType} completed, waiting for final result`); 2895 | } 2896 | } 2897 | return; 2898 | } 2899 | 2900 | // Handle regular responses 2901 | const myResponse = json.message; 2902 | logger.debug(`Received message: ${JSON.stringify(myResponse)}`); 2903 | logger.log('myResponse' + JSON.stringify(myResponse)); 2904 | 2905 | // Handle response to a request 2906 | if ( 2907 | myResponse.id && 2908 | pendingRequests.has(myResponse.id) && 2909 | myResponse.result 2910 | ) { 2911 | const request = pendingRequests.get(myResponse.id)!; 2912 | clearTimeout(request.timeout); 2913 | 2914 | if (myResponse.error) { 2915 | logger.error(`Error from Figma: ${myResponse.error}`); 2916 | request.reject(new Error(myResponse.error)); 2917 | } else { 2918 | if (myResponse.result) { 2919 | request.resolve(myResponse.result); 2920 | } 2921 | } 2922 | 2923 | pendingRequests.delete(myResponse.id); 2924 | } else { 2925 | // Handle broadcast messages or events 2926 | logger.info(`Received broadcast message: ${JSON.stringify(myResponse)}`); 2927 | } 2928 | } catch (error) { 2929 | logger.error(`Error parsing message: ${error instanceof Error ? error.message : String(error)}`); 2930 | } 2931 | }); 2932 | 2933 | ws.on('error', (error) => { 2934 | logger.error(`Socket error: ${error}`); 2935 | }); 2936 | 2937 | ws.on('close', () => { 2938 | logger.info('Disconnected from Figma socket server'); 2939 | ws = null; 2940 | 2941 | // Reject all pending requests 2942 | for (const [id, request] of pendingRequests.entries()) { 2943 | clearTimeout(request.timeout); 2944 | request.reject(new Error("Connection closed")); 2945 | pendingRequests.delete(id); 2946 | } 2947 | 2948 | // Attempt to reconnect 2949 | logger.info('Attempting to reconnect in 2 seconds...'); 2950 | setTimeout(() => connectToFigma(port), 2000); 2951 | }); 2952 | } 2953 | 2954 | // Function to join a channel 2955 | async function joinChannel(channelName: string): Promise<void> { 2956 | if (!ws || ws.readyState !== WebSocket.OPEN) { 2957 | throw new Error("Not connected to Figma"); 2958 | } 2959 | 2960 | try { 2961 | await sendCommandToFigma("join", { channel: channelName }); 2962 | currentChannel = channelName; 2963 | logger.info(`Joined channel: ${channelName}`); 2964 | } catch (error) { 2965 | logger.error(`Failed to join channel: ${error instanceof Error ? error.message : String(error)}`); 2966 | throw error; 2967 | } 2968 | } 2969 | 2970 | // Function to send commands to Figma 2971 | function sendCommandToFigma( 2972 | command: FigmaCommand, 2973 | params: unknown = {}, 2974 | timeoutMs: number = 30000 2975 | ): Promise<unknown> { 2976 | return new Promise((resolve, reject) => { 2977 | // If not connected, try to connect first 2978 | if (!ws || ws.readyState !== WebSocket.OPEN) { 2979 | connectToFigma(); 2980 | reject(new Error("Not connected to Figma. Attempting to connect...")); 2981 | return; 2982 | } 2983 | 2984 | // Check if we need a channel for this command 2985 | const requiresChannel = command !== "join"; 2986 | if (requiresChannel && !currentChannel) { 2987 | reject(new Error("Must join a channel before sending commands")); 2988 | return; 2989 | } 2990 | 2991 | const id = uuidv4(); 2992 | const request = { 2993 | id, 2994 | type: command === "join" ? "join" : "message", 2995 | ...(command === "join" 2996 | ? { channel: (params as any).channel } 2997 | : { channel: currentChannel }), 2998 | message: { 2999 | id, 3000 | command, 3001 | params: { 3002 | ...(params as any), 3003 | commandId: id, // Include the command ID in params 3004 | }, 3005 | }, 3006 | }; 3007 | 3008 | // Set timeout for request 3009 | const timeout = setTimeout(() => { 3010 | if (pendingRequests.has(id)) { 3011 | pendingRequests.delete(id); 3012 | logger.error(`Request ${id} to Figma timed out after ${timeoutMs / 1000} seconds`); 3013 | reject(new Error('Request to Figma timed out')); 3014 | } 3015 | }, timeoutMs); 3016 | 3017 | // Store the promise callbacks to resolve/reject later 3018 | pendingRequests.set(id, { 3019 | resolve, 3020 | reject, 3021 | timeout, 3022 | lastActivity: Date.now() 3023 | }); 3024 | 3025 | // Send the request 3026 | logger.info(`Sending command to Figma: ${command}`); 3027 | logger.debug(`Request details: ${JSON.stringify(request)}`); 3028 | ws.send(JSON.stringify(request)); 3029 | }); 3030 | } 3031 | 3032 | // Update the join_channel tool 3033 | server.tool( 3034 | "join_channel", 3035 | "Join a specific channel to communicate with Figma", 3036 | { 3037 | channel: z.string().describe("The name of the channel to join").default(""), 3038 | }, 3039 | async ({ channel }: any) => { 3040 | try { 3041 | if (!channel) { 3042 | // If no channel provided, ask the user for input 3043 | return { 3044 | content: [ 3045 | { 3046 | type: "text", 3047 | text: "Please provide a channel name to join:", 3048 | }, 3049 | ], 3050 | followUp: { 3051 | tool: "join_channel", 3052 | description: "Join the specified channel", 3053 | }, 3054 | }; 3055 | } 3056 | 3057 | await joinChannel(channel); 3058 | return { 3059 | content: [ 3060 | { 3061 | type: "text", 3062 | text: `Successfully joined channel: ${channel}`, 3063 | }, 3064 | ], 3065 | }; 3066 | } catch (error) { 3067 | return { 3068 | content: [ 3069 | { 3070 | type: "text", 3071 | text: `Error joining channel: ${error instanceof Error ? error.message : String(error) 3072 | }`, 3073 | }, 3074 | ], 3075 | }; 3076 | } 3077 | } 3078 | ); 3079 | 3080 | // Start the server 3081 | async function main() { 3082 | try { 3083 | // Try to connect to Figma socket server 3084 | connectToFigma(); 3085 | } catch (error) { 3086 | logger.warn(`Could not connect to Figma initially: ${error instanceof Error ? error.message : String(error)}`); 3087 | logger.warn('Will try to connect when the first command is sent'); 3088 | } 3089 | 3090 | // Start the MCP server with stdio transport 3091 | const transport = new StdioServerTransport(); 3092 | await server.connect(transport); 3093 | logger.info('FigmaMCP server running on stdio'); 3094 | } 3095 | 3096 | // Run the server 3097 | main().catch(error => { 3098 | logger.error(`Error starting FigmaMCP server: ${error instanceof Error ? error.message : String(error)}`); 3099 | process.exit(1); 3100 | }); 3101 | 3102 | 3103 | 3104 | ```