@seed-design/mcp 0.0.0-alpha-20260324091316

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