#
tokens: 30839/50000 1/17 files (page 2/3)
lines: on (toggle) GitHub
raw markdown copy reset
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 | 
```
Page 2/3FirstPrevNextLast