@seed-design/mcp 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/tools.ts CHANGED
@@ -1,8 +1,8 @@
1
- import type { GetFileNodesResponse } from "@figma/rest-api-spec";
2
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
2
  import { createRestNormalizer, figma, getFigmaColorVariableNames, react } from "@seed-design/figma";
4
3
  import { z } from "zod";
5
4
  import type { McpConfig } from "./config";
5
+ import { parseFigmaUrl } from "./figma-rest-client";
6
6
  import { formatError } from "./logger";
7
7
  import {
8
8
  formatErrorResponse,
@@ -10,128 +10,146 @@ import {
10
10
  formatObjectResponse,
11
11
  formatTextResponse,
12
12
  } from "./responses";
13
+ import type { FigmaRestClient } from "./figma-rest-client";
14
+ import {
15
+ createToolContext,
16
+ fetchMultipleNodesData,
17
+ fetchNodeData,
18
+ requireWebSocket,
19
+ type ToolMode,
20
+ } from "./tools-helpers";
13
21
  import type { FigmaWebSocketClient } from "./websocket";
14
22
 
15
- export function registerTools(
16
- server: McpServer,
17
- figmaClient: FigmaWebSocketClient,
18
- config: McpConfig | null = {},
19
- ): void {
20
- const { joinChannel, sendCommandToFigma } = figmaClient;
21
- const { extend } = config ?? {};
23
+ const singleNodeBaseSchema = z.object({
24
+ figmaUrl: z
25
+ .url()
26
+ .optional()
27
+ .describe("Figma node URL. Extracts fileKey and nodeId automatically when provided."),
28
+ fileKey: z
29
+ .string()
30
+ .optional()
31
+ .describe("Figma file key. Use with nodeId when not using figmaUrl."),
32
+ nodeId: z.string().optional().describe("Node ID (e.g., '0:1')."),
33
+ personalAccessToken: z
34
+ .string()
35
+ .optional()
36
+ .describe("Figma PAT. Falls back to FIGMA_PERSONAL_ACCESS_TOKEN env when not provided."),
37
+ });
22
38
 
23
- // join_channel tool
24
- server.tool(
25
- "join_channel",
26
- "Join a specific channel to communicate with Figma",
27
- {
28
- channel: z.string().describe("The name of the channel to join").default(""),
29
- },
30
- async ({ channel }) => {
31
- try {
32
- if (!channel) {
33
- // If no channel provided, ask the user for input
34
- return {
35
- ...formatTextResponse("Please provide a channel name to join:"),
36
- followUp: {
37
- tool: "join_channel",
38
- description: "Join the specified channel",
39
- },
40
- };
41
- }
39
+ const multiNodeBaseSchema = z.object({
40
+ fileKey: z
41
+ .string()
42
+ .optional()
43
+ .describe("Figma file key. Required when WebSocket connection is not available."),
44
+ nodeIds: z.array(z.string()).describe("Array of node IDs (e.g., ['0:1', '0:2'])"),
45
+ personalAccessToken: z
46
+ .string()
47
+ .optional()
48
+ .describe(
49
+ "Figma PAT. Falls back to FIGMA_PERSONAL_ACCESS_TOKEN env when not provided to be used when WebSocket connection is not available.",
50
+ ),
51
+ });
42
52
 
43
- await joinChannel(channel);
44
- return formatTextResponse(`Successfully joined channel: ${channel}`);
45
- } catch (error) {
46
- return formatErrorResponse("join_channel", error);
47
- }
48
- },
49
- );
53
+ function getSingleNodeParamsSchema(mode: ToolMode) {
54
+ switch (mode) {
55
+ case "websocket":
56
+ return singleNodeBaseSchema.pick({ nodeId: true }).required();
57
+ default:
58
+ return singleNodeBaseSchema;
59
+ }
60
+ }
50
61
 
51
- // Document Info Tool
52
- server.tool(
53
- "get_document_info",
54
- "Get detailed information about the current Figma document",
55
- {},
56
- async () => {
57
- try {
58
- const result = await sendCommandToFigma("get_document_info");
59
- return formatObjectResponse(result);
60
- } catch (error) {
61
- return formatErrorResponse("get_document_info", error);
62
- }
63
- },
64
- );
62
+ function getMultiNodeParamsSchema(mode: ToolMode) {
63
+ switch (mode) {
64
+ case "websocket":
65
+ return multiNodeBaseSchema.pick({ nodeIds: true });
66
+ case "rest":
67
+ return multiNodeBaseSchema.required({ fileKey: true });
68
+ default:
69
+ return multiNodeBaseSchema;
70
+ }
71
+ }
65
72
 
66
- // Selection Tool
67
- server.tool(
68
- "get_selection",
69
- "Get information about the current selection in Figma",
70
- {},
71
- async () => {
72
- try {
73
- const result = await sendCommandToFigma("get_selection");
74
- return formatObjectResponse(result);
75
- } catch (error) {
76
- return formatErrorResponse("get_selection", error);
77
- }
78
- },
79
- );
73
+ function resolveSingleNodeParams(params: z.infer<typeof singleNodeBaseSchema>): {
74
+ fileKey: string | undefined;
75
+ nodeId: string;
76
+ personalAccessToken: string | undefined;
77
+ } {
78
+ if (params.figmaUrl) {
79
+ const parsed = parseFigmaUrl(params.figmaUrl);
80
80
 
81
- // Annotation Tool
82
- server.tool(
83
- "add_annotations",
84
- "Add annotations to multiple nodes in Figma",
85
- {
86
- annotations: z.array(
87
- z.object({
88
- nodeId: z.string().describe("The ID of the node to add an annotation to"),
89
- labelMarkdown: z
90
- .string()
91
- .describe("The markdown label for the annotation, do not escape newlines"),
92
- }),
93
- ),
94
- },
95
- async ({ annotations }) => {
96
- try {
97
- await sendCommandToFigma("add_annotations", { annotations });
81
+ return {
82
+ fileKey: parsed.fileKey,
83
+ nodeId: parsed.nodeId,
84
+ personalAccessToken: params.personalAccessToken,
85
+ };
86
+ }
98
87
 
99
- return formatTextResponse(
100
- `Annotations added to nodes ${annotations.map((annotation) => annotation.nodeId).join(", ")}`,
101
- );
102
- } catch (error) {
103
- return formatErrorResponse("add_annotations", error);
104
- }
105
- },
106
- );
88
+ if (!params.nodeId) {
89
+ throw new Error(
90
+ "Either figmaUrl or nodeId must be provided. Use figmaUrl for automatic parsing, or provide fileKey + nodeId directly.",
91
+ );
92
+ }
107
93
 
108
- // Get Annotations Tool
109
- server.tool(
110
- "get_annotations",
111
- "Get annotations for a specific node in Figma",
112
- { nodeId: z.string().describe("The ID of the node to get annotations for") },
113
- async ({ nodeId }) => {
114
- try {
115
- const result = await sendCommandToFigma("get_annotations", { nodeId });
116
- return formatObjectResponse(result);
117
- } catch (error) {
118
- return formatErrorResponse("get_annotations", error);
119
- }
120
- },
121
- );
94
+ return {
95
+ fileKey: params.fileKey,
96
+ nodeId: params.nodeId,
97
+ personalAccessToken: params.personalAccessToken,
98
+ };
99
+ }
100
+
101
+ function getSingleNodeDescription(baseDescription: string, mode: ToolMode): string {
102
+ switch (mode) {
103
+ case "rest":
104
+ return `${baseDescription} Provide either: (1) figmaUrl (e.g., https://www.figma.com/design/ABC/Name?node-id=0-1), or (2) fileKey + nodeId.`;
105
+ case "websocket":
106
+ return `${baseDescription} Provide nodeId. Requires WebSocket connection with Figma Plugin.`;
107
+ case "all":
108
+ return `${baseDescription} Provide either: (1) figmaUrl (e.g., https://www.figma.com/design/ABC/Name?node-id=0-1), (2) fileKey + nodeId, or (3) nodeId only for WebSocket mode.`;
109
+ }
110
+ }
122
111
 
123
- // Component Info Tool
124
- server.tool(
112
+ function getMultiNodeDescription(baseDescription: string, mode: ToolMode): string {
113
+ switch (mode) {
114
+ case "rest":
115
+ return `${baseDescription} Provide fileKey + nodeIds.`;
116
+ case "websocket":
117
+ return `${baseDescription} Provide nodeIds. Requires WebSocket connection with Figma Plugin.`;
118
+ case "all":
119
+ return `${baseDescription} Provide either: (1) fileKey + nodeIds for REST API, or (2) nodeIds only for WebSocket mode. If you have multiple URLs, call get_node_info for each URL instead.`;
120
+ }
121
+ }
122
+
123
+ export function registerTools(
124
+ server: McpServer,
125
+ figmaClient: FigmaWebSocketClient | null,
126
+ restClient: FigmaRestClient | null,
127
+ config: McpConfig | null,
128
+ mode: ToolMode,
129
+ ): void {
130
+ const context = createToolContext(figmaClient, restClient, config, mode);
131
+ const singleNodeParamsSchema = getSingleNodeParamsSchema(mode);
132
+ const multiNodeParamsSchema = getMultiNodeParamsSchema(mode);
133
+
134
+ const shouldRegisterWebSocketOnlyTools = mode === "websocket" || mode === "all";
135
+
136
+ // REST API + WebSocket Tools (hybrid)
137
+ // These tools support both REST API and WebSocket modes
138
+
139
+ // Component Info Tool (REST API + WebSocket)
140
+ server.registerTool(
125
141
  "get_component_info",
126
- "Get detailed information about a specific component node in Figma",
127
142
  {
128
- nodeId: z.string().describe("The ID of the component node to get information about"),
143
+ description: getSingleNodeDescription(
144
+ "Get detailed information about a specific component node in Figma.",
145
+ mode,
146
+ ),
147
+ inputSchema: singleNodeParamsSchema,
129
148
  },
130
- async ({ nodeId }) => {
149
+ async (params: z.infer<typeof singleNodeBaseSchema>) => {
131
150
  try {
132
- const result = (await sendCommandToFigma("get_node_info", {
133
- nodeId,
134
- })) as GetFileNodesResponse["nodes"][string];
151
+ const { fileKey, nodeId, personalAccessToken } = resolveSingleNodeParams(params);
152
+ const result = await fetchNodeData({ fileKey, nodeId, personalAccessToken }, context);
135
153
 
136
154
  const node = result.document;
137
155
  if (node.type !== "COMPONENT" && node.type !== "COMPONENT_SET") {
@@ -155,21 +173,26 @@ export function registerTools(
155
173
  componentPropertyDefinitions: node.componentPropertyDefinitions,
156
174
  });
157
175
  } catch (error) {
158
- return formatErrorResponse("get_node_info", error);
176
+ return formatErrorResponse("get_component_info", error);
159
177
  }
160
178
  },
161
179
  );
162
180
 
163
- // Node Info Tool
164
- server.tool(
181
+ // Node Info Tool (REST API + WebSocket)
182
+ server.registerTool(
165
183
  "get_node_info",
166
- "Get detailed information about a specific node in Figma",
167
184
  {
168
- nodeId: z.string().describe("The ID of the node to get information about"),
185
+ description: getSingleNodeDescription(
186
+ "Get detailed information about a specific node in Figma.",
187
+ mode,
188
+ ),
189
+ inputSchema: singleNodeParamsSchema,
169
190
  },
170
- async ({ nodeId }) => {
191
+ async (params: z.infer<typeof singleNodeBaseSchema>) => {
171
192
  try {
172
- const result: any = await sendCommandToFigma("get_node_info", { nodeId });
193
+ const { fileKey, nodeId, personalAccessToken } = resolveSingleNodeParams(params);
194
+ const result = await fetchNodeData({ fileKey, nodeId, personalAccessToken }, context);
195
+
173
196
  const normalizer = createRestNormalizer(result);
174
197
  const node = normalizer(result.document);
175
198
 
@@ -182,99 +205,105 @@ export function registerTools(
182
205
  shouldInferVariableName: true,
183
206
  });
184
207
  const original =
185
- noInferPipeline.generateCode(node, {
186
- shouldPrintSource: true,
187
- })?.jsx ?? "Failed to generate summarized node info";
208
+ noInferPipeline.generateCode(node, { shouldPrintSource: true })?.jsx ??
209
+ "Failed to generate summarized node info";
188
210
  const inferred =
189
- inferPipeline.generateCode(node, {
190
- shouldPrintSource: true,
191
- })?.jsx ?? "Failed to generate summarized node info";
211
+ inferPipeline.generateCode(node, { shouldPrintSource: true })?.jsx ??
212
+ "Failed to generate summarized node info";
192
213
 
193
214
  return formatObjectResponse({
194
- original: {
195
- data: original,
196
- description: "Original Figma node info",
197
- },
198
- inferred: {
199
- data: inferred,
200
- description: "AutoLayout Inferred Figma node info (fix suggestions)",
201
- },
215
+ original: { data: original, description: "Original Figma node info" },
216
+ inferred: { data: inferred, description: "AutoLayout Inferred Figma node info" },
202
217
  });
203
218
  } catch (error) {
204
219
  return formatTextResponse(
205
- `Error in get_node_info: ${formatError(error)}\n\n⚠️ Figma 라이브러리가 최신 버전인지 확인해주세요.`,
220
+ `Error in get_node_info: ${formatError(error)}\n\n⚠️ Please make sure you have the latest version of the Figma library.`,
206
221
  );
207
222
  }
208
223
  },
209
224
  );
210
225
 
211
- // Nodes Info Tool
212
- server.tool(
226
+ // Nodes Info Tool (REST API + WebSocket)
227
+ server.registerTool(
213
228
  "get_nodes_info",
214
- "Get detailed information about multiple nodes in Figma",
215
229
  {
216
- nodeIds: z.array(z.string()).describe("Array of node IDs to get information about"),
230
+ description: getMultiNodeDescription(
231
+ "Get detailed information about multiple nodes in Figma.",
232
+ mode,
233
+ ),
234
+ inputSchema: multiNodeParamsSchema,
217
235
  },
218
- async ({ nodeIds }) => {
236
+ async ({ fileKey, nodeIds, personalAccessToken }: z.infer<typeof multiNodeBaseSchema>) => {
219
237
  try {
220
- const results = await Promise.all(
221
- nodeIds.map(async (nodeId) => {
222
- const result: any = await sendCommandToFigma("get_node_info", { nodeId });
223
- const normalizer = createRestNormalizer(result);
224
- const node = normalizer(result.document);
225
-
226
- const noInferPipeline = figma.createPipeline({
227
- shouldInferAutoLayout: false,
228
- shouldInferVariableName: false,
229
- });
230
- const inferPipeline = figma.createPipeline({
231
- shouldInferAutoLayout: true,
232
- shouldInferVariableName: true,
233
- });
234
- const original =
235
- noInferPipeline.generateCode(node, {
236
- shouldPrintSource: true,
237
- })?.jsx ?? "Failed to generate summarized node info";
238
- const inferred =
239
- inferPipeline.generateCode(node, {
240
- shouldPrintSource: true,
241
- })?.jsx ?? "Failed to generate summarized node info";
238
+ if (nodeIds.length === 0) {
239
+ return formatErrorResponse("get_nodes_info", new Error("No node IDs provided"));
240
+ }
242
241
 
243
- return {
244
- nodeId,
245
- original: {
246
- data: original,
247
- description: "Original Figma node info",
248
- },
249
- inferred: {
250
- data: inferred,
251
- description: "AutoLayout Inferred Figma node info (fix suggestions)",
252
- },
253
- };
254
- }),
242
+ const nodesData = await fetchMultipleNodesData(
243
+ { fileKey, nodeIds, personalAccessToken },
244
+ context,
255
245
  );
246
+
247
+ const results = nodeIds.map((nodeId) => {
248
+ const nodeData = nodesData[nodeId];
249
+ if (!nodeData) {
250
+ return { nodeId, error: `Node ${nodeId} not found` };
251
+ }
252
+
253
+ const normalizer = createRestNormalizer(nodeData);
254
+ const node = normalizer(nodeData.document);
255
+
256
+ const noInferPipeline = figma.createPipeline({
257
+ shouldInferAutoLayout: false,
258
+ shouldInferVariableName: false,
259
+ });
260
+ const inferPipeline = figma.createPipeline({
261
+ shouldInferAutoLayout: true,
262
+ shouldInferVariableName: true,
263
+ });
264
+ const original =
265
+ noInferPipeline.generateCode(node, { shouldPrintSource: true })?.jsx ??
266
+ "Failed to generate summarized node info";
267
+ const inferred =
268
+ inferPipeline.generateCode(node, { shouldPrintSource: true })?.jsx ??
269
+ "Failed to generate summarized node info";
270
+
271
+ return {
272
+ nodeId,
273
+ original: { data: original, description: "Original Figma node info" },
274
+ inferred: { data: inferred, description: "AutoLayout Inferred Figma node info" },
275
+ };
276
+ });
277
+
256
278
  return formatObjectResponse(results);
257
279
  } catch (error) {
258
280
  return formatTextResponse(
259
- `Error in get_nodes_info: ${formatError(error)}\n\n⚠️ Figma 라이브러리가 최신 버전인지 확인해주세요.`,
281
+ `Error in get_nodes_info: ${formatError(error)}\n\n⚠️ Please make sure you have the latest version of the Figma library.`,
260
282
  );
261
283
  }
262
284
  },
263
285
  );
264
286
 
265
- server.tool(
287
+ // Get Node React Code Tool (REST API + WebSocket)
288
+ server.registerTool(
266
289
  "get_node_react_code",
267
- "Get the React code for a specific node in Figma",
268
- { nodeId: z.string().describe("The ID of the node to get code for") },
269
- async ({ nodeId }) => {
290
+ {
291
+ description: getSingleNodeDescription(
292
+ "Get the React code for a specific node in Figma.",
293
+ mode,
294
+ ),
295
+ inputSchema: singleNodeParamsSchema,
296
+ },
297
+ async (params: z.infer<typeof singleNodeBaseSchema>) => {
270
298
  try {
271
- const result: any = await sendCommandToFigma("get_node_info", { nodeId });
272
- const normalizer = createRestNormalizer(result);
299
+ const { fileKey, nodeId, personalAccessToken } = resolveSingleNodeParams(params);
300
+ const result = await fetchNodeData({ fileKey, nodeId, personalAccessToken }, context);
273
301
 
302
+ const normalizer = createRestNormalizer(result);
274
303
  const pipeline = react.createPipeline({
275
304
  shouldInferAutoLayout: true,
276
305
  shouldInferVariableName: true,
277
- extend,
306
+ extend: context.extend,
278
307
  });
279
308
  const generated = pipeline.generateCode(normalizer(result.document), {
280
309
  shouldPrintSource: false,
@@ -282,75 +311,216 @@ export function registerTools(
282
311
 
283
312
  if (!generated) {
284
313
  return formatTextResponse(
285
- "Failed to generate code\n\n⚠️ Figma 라이브러리가 최신 버전인지 확인해주세요.",
314
+ "Failed to generate code\n\n⚠️ Please make sure you have the latest version of the Figma library.",
286
315
  );
287
316
  }
288
317
 
289
318
  return formatTextResponse(`${generated.imports}\n\n${generated.jsx}`);
290
319
  } catch (error) {
291
320
  return formatTextResponse(
292
- `Error in get_node_react_code: ${formatError(error)}\n\n⚠️ Figma 라이브러리가 최신 버전인지 확인해주세요.`,
321
+ `Error in get_node_react_code: ${formatError(error)}\n\n⚠️ Please make sure you have the latest version of the Figma library.`,
293
322
  );
294
323
  }
295
324
  },
296
325
  );
297
326
 
298
- // Export Node as Image Tool
299
- server.tool(
300
- "export_node_as_image",
301
- "Export a node as an image from Figma",
302
- {
303
- nodeId: z.string().describe("The ID of the node to export"),
304
- format: z.enum(["PNG", "JPG", "SVG", "PDF"]).optional().describe("Export format"),
305
- scale: z.number().positive().optional().describe("Export scale"),
306
- },
307
- async ({ nodeId, format, scale }) => {
308
- try {
309
- const result = await sendCommandToFigma("export_node_as_image", {
310
- nodeId,
311
- format: format || "PNG",
312
- scale: scale || 1,
313
- });
314
- const typedResult = result as { base64: string; mimeType: string };
315
- return formatImageResponse(typedResult.base64, typedResult.mimeType || "image/png");
316
- } catch (error) {
317
- return formatErrorResponse("export_node_as_image", error);
318
- }
319
- },
320
- );
327
+ // Utility Tools (No Figma connection required)
321
328
 
322
329
  // Retrieve Color Variable Names Tool
323
- server.tool(
330
+ server.registerTool(
324
331
  "retrieve_color_variable_names",
325
- "Retrieve available color variable names in scope",
326
332
  {
327
- scope: z
328
- .enum(["fg", "bg", "stroke", "palette"])
329
- .array()
330
- .describe("The scope of the color variable names to retrieve"),
333
+ description:
334
+ "Retrieve available SEED Design color variable names by scope. No Figma connection required.",
335
+ inputSchema: z.object({
336
+ scope: z
337
+ .enum(["fg", "bg", "stroke", "palette"])
338
+ .array()
339
+ .describe("The scope of the color variable names to retrieve"),
340
+ }),
331
341
  },
332
342
  async ({ scope }) => {
333
343
  try {
334
344
  const result = getFigmaColorVariableNames(scope);
345
+
335
346
  return formatObjectResponse(result);
336
347
  } catch (error) {
337
348
  return formatErrorResponse("retrieve_color_variable_names", error);
338
349
  }
339
350
  },
340
351
  );
352
+
353
+ if (shouldRegisterWebSocketOnlyTools) {
354
+ // WebSocket Only Tools
355
+
356
+ server.registerTool(
357
+ "join_channel",
358
+ {
359
+ description: "Join a specific channel to communicate with Figma (WebSocket mode only)",
360
+ inputSchema: z.object({
361
+ channel: z.string().describe("The name of the channel to join").default(""),
362
+ }),
363
+ },
364
+ async ({ channel }) => {
365
+ try {
366
+ if (!figmaClient)
367
+ return formatErrorResponse(
368
+ "join_channel",
369
+ new Error("WebSocket not available. This tool requires Figma Plugin connection."),
370
+ );
371
+
372
+ if (!channel)
373
+ // If no channel provided, ask the user for input
374
+ return {
375
+ ...formatTextResponse("Please provide a channel name to join:"),
376
+ followUp: {
377
+ tool: "join_channel",
378
+ description: "Join the specified channel",
379
+ },
380
+ };
381
+
382
+ await figmaClient.joinChannel(channel);
383
+
384
+ return formatTextResponse(`Successfully joined channel: ${channel}`);
385
+ } catch (error) {
386
+ return formatErrorResponse("join_channel", error);
387
+ }
388
+ },
389
+ );
390
+
391
+ // Document Info Tool
392
+ server.registerTool(
393
+ "get_document_info",
394
+ {
395
+ description:
396
+ "Get detailed information about the current Figma document (WebSocket mode only)",
397
+ },
398
+ async () => {
399
+ try {
400
+ requireWebSocket(context);
401
+ const result = await context.sendCommandToFigma("get_document_info");
402
+
403
+ return formatObjectResponse(result);
404
+ } catch (error) {
405
+ return formatErrorResponse("get_document_info", error);
406
+ }
407
+ },
408
+ );
409
+
410
+ // Selection Tool
411
+ server.registerTool(
412
+ "get_selection",
413
+ {
414
+ description: "Get information about the current selection in Figma (WebSocket mode only)",
415
+ },
416
+ async () => {
417
+ try {
418
+ requireWebSocket(context);
419
+ const result = await context.sendCommandToFigma("get_selection");
420
+
421
+ return formatObjectResponse(result);
422
+ } catch (error) {
423
+ return formatErrorResponse("get_selection", error);
424
+ }
425
+ },
426
+ );
427
+
428
+ // Annotation Tool
429
+ server.registerTool(
430
+ "add_annotations",
431
+ {
432
+ description: "Add annotations to multiple nodes in Figma (WebSocket mode only)",
433
+ inputSchema: z.object({
434
+ annotations: z.array(
435
+ z.object({
436
+ nodeId: z.string().describe("The ID of the node to add an annotation to"),
437
+ labelMarkdown: z
438
+ .string()
439
+ .describe("The markdown label for the annotation, do not escape newlines"),
440
+ }),
441
+ ),
442
+ }),
443
+ },
444
+ async ({ annotations }) => {
445
+ try {
446
+ requireWebSocket(context);
447
+ await context.sendCommandToFigma("add_annotations", { annotations });
448
+
449
+ return formatTextResponse(
450
+ `Annotations added to nodes ${annotations.map((annotation) => annotation.nodeId).join(", ")}`,
451
+ );
452
+ } catch (error) {
453
+ return formatErrorResponse("add_annotations", error);
454
+ }
455
+ },
456
+ );
457
+
458
+ // Get Annotations Tool
459
+ server.registerTool(
460
+ "get_annotations",
461
+ {
462
+ description: "Get annotations for a specific node in Figma (WebSocket mode only)",
463
+ inputSchema: z.object({
464
+ nodeId: z.string().describe("The ID of the node to get annotations for"),
465
+ }),
466
+ },
467
+ async ({ nodeId }) => {
468
+ try {
469
+ requireWebSocket(context);
470
+ const result = await context.sendCommandToFigma("get_annotations", { nodeId });
471
+
472
+ return formatObjectResponse(result);
473
+ } catch (error) {
474
+ return formatErrorResponse("get_annotations", error);
475
+ }
476
+ },
477
+ );
478
+
479
+ // Export Node as Image Tool
480
+ server.registerTool(
481
+ "export_node_as_image",
482
+ {
483
+ description: "Export a node as an image from Figma (WebSocket mode only)",
484
+ inputSchema: z.object({
485
+ nodeId: z.string().describe("The ID of the node to export"),
486
+ format: z.enum(["PNG", "JPG", "SVG", "PDF"]).optional().describe("Export format"),
487
+ scale: z.number().positive().optional().describe("Export scale"),
488
+ }),
489
+ },
490
+ async ({ nodeId, format, scale }) => {
491
+ try {
492
+ requireWebSocket(context);
493
+ const result = await context.sendCommandToFigma("export_node_as_image", {
494
+ nodeId,
495
+ format: format || "PNG",
496
+ scale: scale || 1,
497
+ });
498
+
499
+ const typedResult = result as { base64: string; mimeType: string };
500
+ return formatImageResponse(typedResult.base64, typedResult.mimeType || "image/png");
501
+ } catch (error) {
502
+ return formatErrorResponse("export_node_as_image", error);
503
+ }
504
+ },
505
+ );
506
+ }
341
507
  }
342
508
 
509
+ // editing tools require WebSocket client
510
+
343
511
  export function registerEditingTools(server: McpServer, figmaClient: FigmaWebSocketClient): void {
344
512
  const { sendCommandToFigma } = figmaClient;
345
513
 
346
514
  // Clone Node Tool
347
- server.tool(
515
+ server.registerTool(
348
516
  "clone_node",
349
- "Clone an existing node in Figma",
350
517
  {
351
- nodeId: z.string().describe("The ID of the node to clone"),
352
- x: z.number().optional().describe("New X position for the clone"),
353
- y: z.number().optional().describe("New Y position for the clone"),
518
+ description: "Clone an existing node in Figma (WebSocket mode only)",
519
+ inputSchema: z.object({
520
+ nodeId: z.string().describe("The ID of the node to clone"),
521
+ x: z.number().optional().describe("New X position for the clone"),
522
+ y: z.number().optional().describe("New Y position for the clone"),
523
+ }),
354
524
  },
355
525
  async ({ nodeId, x, y }) => {
356
526
  try {
@@ -362,6 +532,7 @@ export function registerEditingTools(server: McpServer, figmaClient: FigmaWebSoc
362
532
  y?: number;
363
533
  success: boolean;
364
534
  };
535
+
365
536
  return formatTextResponse(
366
537
  `Cloned node with new ID: ${typedResult.id}${x !== undefined && y !== undefined ? ` at position (${x}, ${y})` : ""}`,
367
538
  );
@@ -371,20 +542,23 @@ export function registerEditingTools(server: McpServer, figmaClient: FigmaWebSoc
371
542
  },
372
543
  );
373
544
 
374
- server.tool(
545
+ server.registerTool(
375
546
  "set_fill_color",
376
- "Set the fill color of a node",
377
547
  {
378
- nodeId: z.string().describe("The ID of the node to set the fill color of"),
379
- colorToken: z
380
- .string()
381
- .describe(
382
- "The color token to set the fill color to. Format: `{category}/{name}`. Example: `bg/brand`",
383
- ),
548
+ description: "Set the fill color of a node (WebSocket mode only)",
549
+ inputSchema: z.object({
550
+ nodeId: z.string().describe("The ID of the node to set the fill color of"),
551
+ colorToken: z
552
+ .string()
553
+ .describe(
554
+ "The color token to set the fill color to. Format: `{category}/{name}`. Example: `bg/brand`",
555
+ ),
556
+ }),
384
557
  },
385
558
  async ({ nodeId, colorToken }) => {
386
559
  try {
387
560
  await sendCommandToFigma("set_fill_color", { nodeId, colorToken });
561
+
388
562
  return formatTextResponse(`Fill color set to ${colorToken}`);
389
563
  } catch (error) {
390
564
  return formatErrorResponse("set_fill_color", error);
@@ -392,20 +566,23 @@ export function registerEditingTools(server: McpServer, figmaClient: FigmaWebSoc
392
566
  },
393
567
  );
394
568
 
395
- server.tool(
569
+ server.registerTool(
396
570
  "set_stroke_color",
397
- "Set the stroke color of a node",
398
571
  {
399
- nodeId: z.string().describe("The ID of the node to set the stroke color of"),
400
- colorToken: z
401
- .string()
402
- .describe(
403
- "The color token to set the stroke color to. Format: `{category}/{name}`. Example: `stroke/neutral`",
404
- ),
572
+ description: "Set the stroke color of a node (WebSocket mode only)",
573
+ inputSchema: z.object({
574
+ nodeId: z.string().describe("The ID of the node to set the stroke color of"),
575
+ colorToken: z
576
+ .string()
577
+ .describe(
578
+ "The color token to set the stroke color to. Format: `{category}/{name}`. Example: `stroke/neutral`",
579
+ ),
580
+ }),
405
581
  },
406
582
  async ({ nodeId, colorToken }) => {
407
583
  try {
408
584
  await sendCommandToFigma("set_stroke_color", { nodeId, colorToken });
585
+
409
586
  return formatTextResponse(`Stroke color set to ${colorToken}`);
410
587
  } catch (error) {
411
588
  return formatErrorResponse("set_stroke_color", error);
@@ -413,31 +590,39 @@ export function registerEditingTools(server: McpServer, figmaClient: FigmaWebSoc
413
590
  },
414
591
  );
415
592
 
416
- server.tool(
593
+ server.registerTool(
417
594
  "set_auto_layout",
418
- "Set the auto layout of a node",
419
595
  {
420
- nodeId: z.string().describe("The ID of the node to set the auto layout of"),
421
- layoutMode: z.enum(["HORIZONTAL", "VERTICAL"]).optional().describe("The layout mode to set"),
422
- layoutWrap: z.enum(["NO_WRAP", "WRAP"]).optional().describe("The layout wrap to set"),
423
- primaryAxisAlignItems: z
424
- .enum(["MIN", "MAX", "CENTER", "SPACE_BETWEEN"])
425
- .optional()
426
- .describe("The primary axis align items to set"),
427
- counterAxisAlignItems: z
428
- .enum(["MIN", "MAX", "CENTER", "BASELINE"])
429
- .optional()
430
- .describe("The counter axis align items to set"),
431
- itemSpacing: z.number().optional().describe("The item spacing to set"),
432
- horizontalPadding: z.number().optional().describe("The horizontal padding to set"),
433
- verticalPadding: z.number().optional().describe("The vertical padding to set"),
434
- paddingLeft: z.number().optional().describe("The padding left to set (when left != right)"),
435
- paddingRight: z.number().optional().describe("The padding right to set (when left != right)"),
436
- paddingTop: z.number().optional().describe("The padding top to set (when top != bottom)"),
437
- paddingBottom: z
438
- .number()
439
- .optional()
440
- .describe("The padding bottom to set (when top != bottom)"),
596
+ description: "Set the auto layout of a node (WebSocket mode only)",
597
+ inputSchema: z.object({
598
+ nodeId: z.string().describe("The ID of the node to set the auto layout of"),
599
+ layoutMode: z
600
+ .enum(["HORIZONTAL", "VERTICAL"])
601
+ .optional()
602
+ .describe("The layout mode to set"),
603
+ layoutWrap: z.enum(["NO_WRAP", "WRAP"]).optional().describe("The layout wrap to set"),
604
+ primaryAxisAlignItems: z
605
+ .enum(["MIN", "MAX", "CENTER", "SPACE_BETWEEN"])
606
+ .optional()
607
+ .describe("The primary axis align items to set"),
608
+ counterAxisAlignItems: z
609
+ .enum(["MIN", "MAX", "CENTER", "BASELINE"])
610
+ .optional()
611
+ .describe("The counter axis align items to set"),
612
+ itemSpacing: z.number().optional().describe("The item spacing to set"),
613
+ horizontalPadding: z.number().optional().describe("The horizontal padding to set"),
614
+ verticalPadding: z.number().optional().describe("The vertical padding to set"),
615
+ paddingLeft: z.number().optional().describe("The padding left to set (when left != right)"),
616
+ paddingRight: z
617
+ .number()
618
+ .optional()
619
+ .describe("The padding right to set (when left != right)"),
620
+ paddingTop: z.number().optional().describe("The padding top to set (when top != bottom)"),
621
+ paddingBottom: z
622
+ .number()
623
+ .optional()
624
+ .describe("The padding bottom to set (when top != bottom)"),
625
+ }),
441
626
  },
442
627
  async ({
443
628
  nodeId,
@@ -468,6 +653,7 @@ export function registerEditingTools(server: McpServer, figmaClient: FigmaWebSoc
468
653
  paddingTop,
469
654
  paddingBottom,
470
655
  });
656
+
471
657
  return formatTextResponse(`Layout set to ${layoutMode}`);
472
658
  } catch (error) {
473
659
  return formatErrorResponse("set_auto_layout", error);
@@ -475,21 +661,23 @@ export function registerEditingTools(server: McpServer, figmaClient: FigmaWebSoc
475
661
  },
476
662
  );
477
663
 
478
- server.tool(
664
+ server.registerTool(
479
665
  "set_size",
480
- "Set the size of a node",
481
666
  {
482
- nodeId: z.string().describe("The ID of the node to set the size of"),
483
- layoutSizingHorizontal: z
484
- .enum(["HUG", "FILL"])
485
- .optional()
486
- .describe("The horizontal layout sizing to set (exclusive with width)"),
487
- layoutSizingVertical: z
488
- .enum(["HUG", "FILL"])
489
- .optional()
490
- .describe("The vertical layout sizing to set (exclusive with height)"),
491
- width: z.number().optional().describe("The width to set (raw value)"),
492
- height: z.number().optional().describe("The height to set (raw value)"),
667
+ description: "Set the size of a node (WebSocket mode only)",
668
+ inputSchema: z.object({
669
+ nodeId: z.string().describe("The ID of the node to set the size of"),
670
+ layoutSizingHorizontal: z
671
+ .enum(["HUG", "FILL"])
672
+ .optional()
673
+ .describe("The horizontal layout sizing to set (exclusive with width)"),
674
+ layoutSizingVertical: z
675
+ .enum(["HUG", "FILL"])
676
+ .optional()
677
+ .describe("The vertical layout sizing to set (exclusive with height)"),
678
+ width: z.number().optional().describe("The width to set (raw value)"),
679
+ height: z.number().optional().describe("The height to set (raw value)"),
680
+ }),
493
681
  },
494
682
  async ({ nodeId, layoutSizingHorizontal, layoutSizingVertical, width, height }) => {
495
683
  try {
@@ -500,6 +688,7 @@ export function registerEditingTools(server: McpServer, figmaClient: FigmaWebSoc
500
688
  width,
501
689
  height,
502
690
  });
691
+
503
692
  return formatTextResponse(
504
693
  `Size set to ${width ?? layoutSizingHorizontal}x${height ?? layoutSizingVertical}`,
505
694
  );