@autobest-ui/agent 1.0.1 → 1.0.2

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.
@@ -2,10 +2,10 @@
2
2
  * Tool registry - registers all Figma tools with the MCP server
3
3
  */
4
4
 
5
- import { z } from 'zod';
6
5
  import { readFileSync } from 'fs';
7
6
  import { fileURLToPath } from 'url';
8
7
  import { dirname, join } from 'path';
8
+ import { z } from 'zod';
9
9
  import { handleGetContext } from './context.js';
10
10
  import { handleListPages } from './pages.js';
11
11
  import { handleGetNodes } from './nodes.js';
@@ -42,14 +42,9 @@ import {
42
42
  handleCreateLine,
43
43
  handleSetConstraints,
44
44
  // Phase 4 commands
45
- handleCreatePolygon,
46
- handleBooleanOperation,
47
- handleZoomToNode,
48
- handleSetBlendMode,
49
45
  handleDetachInstance,
50
46
  // Phase 5 commands
51
47
  handleSetLayoutAlign,
52
- handleCreateVector,
53
48
  handleRenameNode,
54
49
  handleReorderNode,
55
50
  // Smart Query commands
@@ -75,7 +70,6 @@ import {
75
70
  handleCreatePage,
76
71
  handleRenamePage,
77
72
  handleDeletePage,
78
- handleReorderPage,
79
73
  // Node Structure commands
80
74
  handleReparentNodes,
81
75
  handleMoveToPage,
@@ -84,7 +78,6 @@ import {
84
78
  // Additional commands
85
79
  handleDuplicatePage,
86
80
  handleSetRotation,
87
- handleSetLayoutGrids,
88
81
  handleCombineAsVariants,
89
82
  // FigJam commands
90
83
  handleCreateSticky,
@@ -150,39 +143,88 @@ const colorSchema = z.union([
150
143
  // ============================================================
151
144
 
152
145
  const SHAPE_TYPES = [
153
- 'SQUARE', 'ELLIPSE', 'ROUNDED_RECTANGLE', 'DIAMOND',
154
- 'TRIANGLE_UP', 'TRIANGLE_DOWN',
155
- 'PARALLELOGRAM_RIGHT', 'PARALLELOGRAM_LEFT',
156
- 'ENG_DATABASE', 'ENG_QUEUE', 'ENG_FILE', 'ENG_FOLDER',
157
- 'TRAPEZOID', 'PREDEFINED_PROCESS', 'SHIELD',
158
- 'DOCUMENT_SINGLE', 'DOCUMENT_MULTIPLE', 'MANUAL_INPUT',
159
- 'HEXAGON', 'CHEVRON', 'PENTAGON', 'OCTAGON', 'STAR', 'PLUS',
160
- 'ARROW_LEFT', 'ARROW_RIGHT',
161
- 'SUMMING_JUNCTION', 'OR',
162
- 'SPEECH_BUBBLE', 'INTERNAL_STORAGE'
146
+ 'SQUARE',
147
+ 'ELLIPSE',
148
+ 'ROUNDED_RECTANGLE',
149
+ 'DIAMOND',
150
+ 'TRIANGLE_UP',
151
+ 'TRIANGLE_DOWN',
152
+ 'PARALLELOGRAM_RIGHT',
153
+ 'PARALLELOGRAM_LEFT',
154
+ 'ENG_DATABASE',
155
+ 'ENG_QUEUE',
156
+ 'ENG_FILE',
157
+ 'ENG_FOLDER',
158
+ 'TRAPEZOID',
159
+ 'PREDEFINED_PROCESS',
160
+ 'SHIELD',
161
+ 'DOCUMENT_SINGLE',
162
+ 'DOCUMENT_MULTIPLE',
163
+ 'MANUAL_INPUT',
164
+ 'HEXAGON',
165
+ 'CHEVRON',
166
+ 'PENTAGON',
167
+ 'OCTAGON',
168
+ 'STAR',
169
+ 'PLUS',
170
+ 'ARROW_LEFT',
171
+ 'ARROW_RIGHT',
172
+ 'SUMMING_JUNCTION',
173
+ 'OR',
174
+ 'SPEECH_BUBBLE',
175
+ 'INTERNAL_STORAGE'
163
176
  ];
164
177
 
165
178
  const CONNECTOR_LINE_TYPES = ['ELBOWED', 'STRAIGHT', 'CURVED'];
166
179
  const CONNECTOR_STROKE_CAPS = [
167
- 'NONE', 'ARROW_EQUILATERAL', 'ARROW_LINES',
168
- 'TRIANGLE_FILLED', 'CIRCLE_FILLED', 'DIAMOND_FILLED'
180
+ 'NONE',
181
+ 'ARROW_EQUILATERAL',
182
+ 'ARROW_LINES',
183
+ 'TRIANGLE_FILLED',
184
+ 'CIRCLE_FILLED',
185
+ 'DIAMOND_FILLED'
169
186
  ];
170
187
  const MAGNETS = ['NONE', 'AUTO', 'TOP', 'LEFT', 'BOTTOM', 'RIGHT', 'CENTER'];
171
188
  const CODE_LANGUAGES = [
172
- 'TYPESCRIPT', 'CPP', 'RUBY', 'CSS', 'JAVASCRIPT', 'HTML',
173
- 'JSON', 'GRAPHQL', 'PYTHON', 'GO', 'SQL', 'SWIFT',
174
- 'KOTLIN', 'RUST', 'BASH', 'PLAINTEXT', 'DART'
189
+ 'TYPESCRIPT',
190
+ 'CPP',
191
+ 'RUBY',
192
+ 'CSS',
193
+ 'JAVASCRIPT',
194
+ 'HTML',
195
+ 'JSON',
196
+ 'GRAPHQL',
197
+ 'PYTHON',
198
+ 'GO',
199
+ 'SQL',
200
+ 'SWIFT',
201
+ 'KOTLIN',
202
+ 'RUST',
203
+ 'BASH',
204
+ 'PLAINTEXT',
205
+ 'DART'
175
206
  ];
176
207
 
177
208
  // Endpoint spec for figma_create_connector / figma_set_connector.
178
209
  // One of: { nodeId, magnet }, { nodeId, position }, { position }.
179
210
  const connectorEndpointSchema = z.object({
180
- nodeId: z.string().optional().describe('ID of the node this endpoint attaches to. Omit for a free-floating endpoint.'),
181
- magnet: z.enum(MAGNETS).optional().describe('Where on the target node the connector attaches. AUTO is recommended; STRAIGHT lines only support CENTER or NONE.'),
182
- position: z.object({
183
- x: z.number(),
184
- y: z.number()
185
- }).optional().describe('Fixed position. Relative to the target node when nodeId is set; absolute canvas coordinates otherwise.')
211
+ nodeId: z
212
+ .string()
213
+ .optional()
214
+ .describe('ID of the node this endpoint attaches to. Omit for a free-floating endpoint.'),
215
+ magnet: z
216
+ .enum(MAGNETS)
217
+ .optional()
218
+ .describe(
219
+ 'Where on the target node the connector attaches. AUTO is recommended; STRAIGHT lines only support CENTER or NONE.'
220
+ ),
221
+ position: z
222
+ .object({
223
+ x: z.number(),
224
+ y: z.number()
225
+ })
226
+ .optional()
227
+ .describe('Fixed position. Relative to the target node when nodeId is set; absolute canvas coordinates otherwise.')
186
228
  });
187
229
 
188
230
  /**
@@ -201,15 +243,21 @@ export function registerTools(server, bridge) {
201
243
  'Get information about the MCP server: package version, WebSocket port, connection state, and connected document info.',
202
244
  {},
203
245
  async () => ({
204
- content: [{
205
- type: 'text',
206
- text: JSON.stringify({
207
- version: pkgVersion,
208
- port: bridge.port,
209
- connected: bridge.isConnected(),
210
- documentInfo: bridge.getDocumentInfo()
211
- }, null, 2)
212
- }]
246
+ content: [
247
+ {
248
+ type: 'text',
249
+ text: JSON.stringify(
250
+ {
251
+ version: pkgVersion,
252
+ port: bridge.port,
253
+ connected: bridge.isConnected(),
254
+ documentInfo: bridge.getDocumentInfo()
255
+ },
256
+ null,
257
+ 2
258
+ )
259
+ }
260
+ ]
213
261
  })
214
262
  );
215
263
 
@@ -239,9 +287,15 @@ export function registerTools(server, bridge) {
239
287
  'Get detailed information about specific Figma nodes by their IDs. Returns node properties including type, position, size, fills, strokes (strokeWeight reads "MIXED" plus the four per-side weights when sides differ), auto-layout (including layoutWrap and counterAxisSpacing), clipsContent, node-level boundVariables (which properties are bound to which variables), explicitVariableModes (variable modes pinned on the node), and more. Composite instance-sublayer IDs (the "I<instanceId>;<childId>" form) resolve reliably — if the direct lookup misses, the instance root is resolved and its subtree searched. IDs that genuinely do not exist come back in notFound with an explanation in notFoundDetails. TIP: Use figma_search_nodes or figma_get_children FIRST to find node IDs efficiently, then use this tool only for nodes you need full details on.',
240
288
  {
241
289
  nodeIds: z.array(z.string()).describe('Array of Figma node IDs (e.g., ["1:23", "4:56"])'),
242
- depth: z.enum(['minimal', 'compact', 'full']).optional().default('full').describe('Detail level: "minimal" (~5 props: id, name, type, childIds), "compact" (~10 props: + x/y/width/height + childIds), "full" (all ~40 props). Use minimal/compact for tree traversal to reduce tokens.')
290
+ depth: z
291
+ .enum(['minimal', 'compact', 'full'])
292
+ .optional()
293
+ .default('full')
294
+ .describe(
295
+ 'Detail level: "minimal" (~5 props: id, name, type, childIds), "compact" (~10 props: + x/y/width/height + childIds), "full" (all ~40 props). Use minimal/compact for tree traversal to reduce tokens.'
296
+ )
243
297
  },
244
- async (args) => handleGetNodes(bridge, args)
298
+ async args => handleGetNodes(bridge, args)
245
299
  );
246
300
 
247
301
  // ============================================================
@@ -256,7 +310,7 @@ export function registerTools(server, bridge) {
256
310
  nodeId: z.string().describe('The node ID to modify'),
257
311
  fills: colorSchema.describe('Fill color - use { color: "#RRGGBB" } for simple colors')
258
312
  },
259
- async (args) => handleSetFills(bridge, args)
313
+ async args => handleSetFills(bridge, args)
260
314
  );
261
315
 
262
316
  // figma_set_strokes - Set stroke colors and weights on a node
@@ -265,14 +319,35 @@ export function registerTools(server, bridge) {
265
319
  'Set stroke color and/or weight. Accepts hex shorthand or strokes array. Supports per-side weights (strokeTopWeight etc.) for border-top-only style dividers — no need to fake them with 1px rectangles. Omit `strokes` to change weights only.',
266
320
  {
267
321
  nodeId: z.string().describe('The node ID to modify'),
268
- strokes: colorSchema.optional().describe('Stroke color - use { color: "#RRGGBB" } for simple colors. Omit to leave existing stroke colors untouched.'),
269
- strokeWeight: z.number().optional().describe('Uniform stroke weight in pixels (applied before any per-side weights)'),
270
- strokeTopWeight: z.number().optional().describe('Top stroke weight in pixels. RECTANGLE / FRAME / COMPONENT / COMPONENT_SET / INSTANCE / SLOT / SLIDE only — errors on other types.'),
271
- strokeRightWeight: z.number().optional().describe('Right stroke weight in pixels (same node-type restriction as strokeTopWeight)'),
272
- strokeBottomWeight: z.number().optional().describe('Bottom stroke weight in pixels (same node-type restriction as strokeTopWeight)'),
273
- strokeLeftWeight: z.number().optional().describe('Left stroke weight in pixels (same node-type restriction as strokeTopWeight)')
274
- },
275
- async (args) => handleSetStrokes(bridge, args)
322
+ strokes: colorSchema
323
+ .optional()
324
+ .describe(
325
+ 'Stroke color - use { color: "#RRGGBB" } for simple colors. Omit to leave existing stroke colors untouched.'
326
+ ),
327
+ strokeWeight: z
328
+ .number()
329
+ .optional()
330
+ .describe('Uniform stroke weight in pixels (applied before any per-side weights)'),
331
+ strokeTopWeight: z
332
+ .number()
333
+ .optional()
334
+ .describe(
335
+ 'Top stroke weight in pixels. RECTANGLE / FRAME / COMPONENT / COMPONENT_SET / INSTANCE / SLOT / SLIDE only — errors on other types.'
336
+ ),
337
+ strokeRightWeight: z
338
+ .number()
339
+ .optional()
340
+ .describe('Right stroke weight in pixels (same node-type restriction as strokeTopWeight)'),
341
+ strokeBottomWeight: z
342
+ .number()
343
+ .optional()
344
+ .describe('Bottom stroke weight in pixels (same node-type restriction as strokeTopWeight)'),
345
+ strokeLeftWeight: z
346
+ .number()
347
+ .optional()
348
+ .describe('Left stroke weight in pixels (same node-type restriction as strokeTopWeight)')
349
+ },
350
+ async args => handleSetStrokes(bridge, args)
276
351
  );
277
352
 
278
353
  // figma_create_rectangle - Create a new rectangle
@@ -288,7 +363,7 @@ export function registerTools(server, bridge) {
288
363
  fills: colorSchema.optional().describe('Fill color'),
289
364
  parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
290
365
  },
291
- async (args) => handleCreateRectangle(bridge, args)
366
+ async args => handleCreateRectangle(bridge, args)
292
367
  );
293
368
 
294
369
  // figma_set_text - Set text content on a text node
@@ -299,7 +374,7 @@ export function registerTools(server, bridge) {
299
374
  nodeId: z.string().describe('The text node ID to modify'),
300
375
  text: z.string().describe('The new text content')
301
376
  },
302
- async (args) => handleSetText(bridge, args)
377
+ async args => handleSetText(bridge, args)
303
378
  );
304
379
 
305
380
  // figma_clone_nodes - Clone/duplicate nodes
@@ -309,12 +384,15 @@ export function registerTools(server, bridge) {
309
384
  {
310
385
  nodeIds: z.array(z.string()).describe('Array of node IDs to clone'),
311
386
  parentId: z.string().optional().describe('Parent node ID for clones (optional)'),
312
- offset: z.object({
313
- x: z.number().optional().default(20).describe('X offset from original'),
314
- y: z.number().optional().default(20).describe('Y offset from original')
315
- }).optional().describe('Position offset for cloned nodes')
387
+ offset: z
388
+ .object({
389
+ x: z.number().optional().default(20).describe('X offset from original'),
390
+ y: z.number().optional().default(20).describe('Y offset from original')
391
+ })
392
+ .optional()
393
+ .describe('Position offset for cloned nodes')
316
394
  },
317
- async (args) => handleCloneNodes(bridge, args)
395
+ async args => handleCloneNodes(bridge, args)
318
396
  );
319
397
 
320
398
  // ============================================================
@@ -328,7 +406,7 @@ export function registerTools(server, bridge) {
328
406
  {
329
407
  nodeIds: z.array(z.string()).describe('Array of node IDs to delete')
330
408
  },
331
- async (args) => handleDeleteNodes(bridge, args)
409
+ async args => handleDeleteNodes(bridge, args)
332
410
  );
333
411
 
334
412
  // figma_move_nodes - Move nodes
@@ -341,47 +419,71 @@ export function registerTools(server, bridge) {
341
419
  y: z.number().optional().describe('Y position (absolute) or offset (if relative=true)'),
342
420
  relative: z.boolean().optional().default(false).describe('If true, x/y are offsets from current position')
343
421
  },
344
- async (args) => handleMoveNodes(bridge, args)
422
+ async args => handleMoveNodes(bridge, args)
345
423
  );
346
424
 
347
425
  // figma_resize_nodes - Resize nodes
348
426
  server.tool(
349
427
  'figma_resize_nodes',
350
428
  'Resize one or more nodes to an explicit pixel size. At least one dimension (width or height) must be provided. ' +
351
- 'PREFER figma_set_layout_align with STRETCH when the goal is "size this child to its parent" — STRETCH works ' +
352
- 'inside instances, survives breakpoint changes, and PRESERVES width/height variable binds; an explicit resize does not. ' +
353
- 'Safety behavior of this tool: (1) resizing an instance sublayer is rejected up front with INSTANCE_SUBLAYER_RESTRICTED ' +
354
- '(Figma silently ignores it); (2) width/height/min/max variable binds are captured before the resize and re-applied ' +
355
- 'afterwards — recovered binds are listed in "rebound", destroyed ones that could not be recovered are named in "warnings"; ' +
356
- '(3) the resulting size is read back and compared to the request — a no-op returns success: false with a RESIZE_NO_OP ' +
357
- 'error, and a clamped result (min/max limits, auto-layout sizing) is reported in "warnings". Each node echoes ' +
358
- '"requested" and "actual" sizes.',
359
- {
360
- nodeIds: z.array(z.string()).describe('Array of node IDs to resize. Instance sublayers are rejected — resize the component master instead.'),
429
+ 'PREFER figma_set_layout_align with STRETCH when the goal is "size this child to its parent" — STRETCH works ' +
430
+ 'inside instances, survives breakpoint changes, and PRESERVES width/height variable binds; an explicit resize does not. ' +
431
+ 'Safety behavior of this tool: (1) resizing an instance sublayer is rejected up front with INSTANCE_SUBLAYER_RESTRICTED ' +
432
+ '(Figma silently ignores it); (2) width/height/min/max variable binds are captured before the resize and re-applied ' +
433
+ 'afterwards — recovered binds are listed in "rebound", destroyed ones that could not be recovered are named in "warnings"; ' +
434
+ '(3) the resulting size is read back and compared to the request — a no-op returns success: false with a RESIZE_NO_OP ' +
435
+ 'error, and a clamped result (min/max limits, auto-layout sizing) is reported in "warnings". Each node echoes ' +
436
+ '"requested" and "actual" sizes.',
437
+ {
438
+ nodeIds: z
439
+ .array(z.string())
440
+ .describe(
441
+ 'Array of node IDs to resize. Instance sublayers are rejected — resize the component master instead.'
442
+ ),
361
443
  width: z.number().optional().describe('New width in pixels'),
362
444
  height: z.number().optional().describe('New height in pixels')
363
445
  },
364
- async (args) => handleResizeNodes(bridge, args)
446
+ async args => handleResizeNodes(bridge, args)
365
447
  );
366
448
 
367
449
  // figma_set_size_limits - Set or clear min/max width/height
368
450
  server.tool(
369
451
  'figma_set_size_limits',
370
452
  'Set or CLEAR the min/max size limits (minWidth, maxWidth, minHeight, maxHeight) on one or more nodes. ' +
371
- 'Pass a positive number to set a limit, or explicit null to remove it — null is the documented way to clear a limit, ' +
372
- 'so max-width is no longer a one-way door. Limits apply to auto-layout frames and their direct children; a node that ' +
373
- 'is neither gets a warning because Figma may ignore the value. Every write is read back and verified: a limit that ' +
374
- 'did not apply returns success: false with LIMIT_NOT_APPLIED, and one that would not clear returns LIMIT_NOT_CLEARED. ' +
375
- 'If the field is variable-bound the bound value wins over the literal — the response warns and points at ' +
376
- 'figma_unbind_variable, which also clears the residual literal. The response echoes all four limits for each node.',
453
+ 'Pass a positive number to set a limit, or explicit null to remove it — null is the documented way to clear a limit, ' +
454
+ 'so max-width is no longer a one-way door. Limits apply to auto-layout frames and their direct children; a node that ' +
455
+ 'is neither gets a warning because Figma may ignore the value. Every write is read back and verified: a limit that ' +
456
+ 'did not apply returns success: false with LIMIT_NOT_APPLIED, and one that would not clear returns LIMIT_NOT_CLEARED. ' +
457
+ 'If the field is variable-bound the bound value wins over the literal — the response warns and points at ' +
458
+ 'figma_unbind_variable, which also clears the residual literal. The response echoes all four limits for each node.',
377
459
  {
378
460
  nodeIds: z.array(z.string()).describe('Array of node IDs to update'),
379
- minWidth: z.number().positive().nullable().optional().describe('Minimum width in pixels. null clears the limit. Omit to leave unchanged.'),
380
- maxWidth: z.number().positive().nullable().optional().describe('Maximum width in pixels. null clears the limit. Omit to leave unchanged.'),
381
- minHeight: z.number().positive().nullable().optional().describe('Minimum height in pixels. null clears the limit. Omit to leave unchanged.'),
382
- maxHeight: z.number().positive().nullable().optional().describe('Maximum height in pixels. null clears the limit. Omit to leave unchanged.')
383
- },
384
- async (args) => handleSetSizeLimits(bridge, args)
461
+ minWidth: z
462
+ .number()
463
+ .positive()
464
+ .nullable()
465
+ .optional()
466
+ .describe('Minimum width in pixels. null clears the limit. Omit to leave unchanged.'),
467
+ maxWidth: z
468
+ .number()
469
+ .positive()
470
+ .nullable()
471
+ .optional()
472
+ .describe('Maximum width in pixels. null clears the limit. Omit to leave unchanged.'),
473
+ minHeight: z
474
+ .number()
475
+ .positive()
476
+ .nullable()
477
+ .optional()
478
+ .describe('Minimum height in pixels. null clears the limit. Omit to leave unchanged.'),
479
+ maxHeight: z
480
+ .number()
481
+ .positive()
482
+ .nullable()
483
+ .optional()
484
+ .describe('Maximum height in pixels. null clears the limit. Omit to leave unchanged.')
485
+ },
486
+ async args => handleSetSizeLimits(bridge, args)
385
487
  );
386
488
 
387
489
  // figma_set_opacity - Set node opacity
@@ -392,18 +494,18 @@ export function registerTools(server, bridge) {
392
494
  nodeId: z.string().describe('The node ID to modify'),
393
495
  opacity: z.number().min(0).max(1).describe('Opacity value from 0 (transparent) to 1 (opaque)')
394
496
  },
395
- async (args) => handleSetOpacity(bridge, args)
497
+ async args => handleSetOpacity(bridge, args)
396
498
  );
397
499
 
398
500
  // figma_set_visible - Show or hide nodes
399
501
  server.tool(
400
502
  'figma_set_visible',
401
- 'Show or hide nodes. Sets node.visible directly — use this instead of binding a BOOLEAN variable or setting opacity to 0 just to hide something. Response echoes each node\'s resulting visibility.',
503
+ "Show or hide nodes. Sets node.visible directly — use this instead of binding a BOOLEAN variable or setting opacity to 0 just to hide something. Response echoes each node's resulting visibility.",
402
504
  {
403
505
  nodeIds: z.array(z.string()).describe('Array of node IDs to show or hide'),
404
506
  visible: z.boolean().describe('true to show, false to hide')
405
507
  },
406
- async (args) => handleSetVisible(bridge, args)
508
+ async args => handleSetVisible(bridge, args)
407
509
  );
408
510
 
409
511
  // figma_set_clips_content - Toggle content clipping on frame-like nodes
@@ -414,7 +516,7 @@ export function registerTools(server, bridge) {
414
516
  nodeIds: z.array(z.string()).describe('Array of node IDs to modify'),
415
517
  clipsContent: z.boolean().describe('true to clip children to the frame bounds, false to let them overflow')
416
518
  },
417
- async (args) => handleSetClipsContent(bridge, args)
519
+ async args => handleSetClipsContent(bridge, args)
418
520
  );
419
521
 
420
522
  // figma_set_corner_radius - Set corner radius
@@ -429,7 +531,7 @@ export function registerTools(server, bridge) {
429
531
  bottomLeft: z.number().optional().describe('Bottom-left corner radius'),
430
532
  bottomRight: z.number().optional().describe('Bottom-right corner radius')
431
533
  },
432
- async (args) => handleSetCornerRadius(bridge, args)
534
+ async args => handleSetCornerRadius(bridge, args)
433
535
  );
434
536
 
435
537
  // figma_group_nodes - Group nodes
@@ -440,7 +542,7 @@ export function registerTools(server, bridge) {
440
542
  nodeIds: z.array(z.string()).describe('Array of node IDs to group together'),
441
543
  name: z.string().optional().default('Group').describe('Name for the new group')
442
544
  },
443
- async (args) => handleGroupNodes(bridge, args)
545
+ async args => handleGroupNodes(bridge, args)
444
546
  );
445
547
 
446
548
  // figma_ungroup_nodes - Ungroup nodes
@@ -450,7 +552,7 @@ export function registerTools(server, bridge) {
450
552
  {
451
553
  nodeIds: z.array(z.string()).describe('Array of group node IDs to ungroup')
452
554
  },
453
- async (args) => handleUngroupNodes(bridge, args)
555
+ async args => handleUngroupNodes(bridge, args)
454
556
  );
455
557
 
456
558
  // ============================================================
@@ -470,7 +572,7 @@ export function registerTools(server, bridge) {
470
572
  fills: colorSchema.optional().describe('Fill color'),
471
573
  parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
472
574
  },
473
- async (args) => handleCreateFrame(bridge, args)
575
+ async args => handleCreateFrame(bridge, args)
474
576
  );
475
577
 
476
578
  // figma_create_text - Create a new text node
@@ -488,7 +590,7 @@ export function registerTools(server, bridge) {
488
590
  name: z.string().optional().default('Text').describe('Node name'),
489
591
  parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
490
592
  },
491
- async (args) => handleCreateText(bridge, args)
593
+ async args => handleCreateText(bridge, args)
492
594
  );
493
595
 
494
596
  // ============================================================
@@ -502,7 +604,7 @@ export function registerTools(server, bridge) {
502
604
  {
503
605
  nodeIds: z.array(z.string()).describe('Array of node IDs to select (empty array to clear)')
504
606
  },
505
- async (args) => handleSetSelection(bridge, args)
607
+ async args => handleSetSelection(bridge, args)
506
608
  );
507
609
 
508
610
  // figma_set_current_page - Switch to a different page
@@ -512,7 +614,7 @@ export function registerTools(server, bridge) {
512
614
  {
513
615
  pageId: z.string().describe('The page ID to switch to')
514
616
  },
515
- async (args) => handleSetCurrentPage(bridge, args)
617
+ async args => handleSetCurrentPage(bridge, args)
516
618
  );
517
619
 
518
620
  // ============================================================
@@ -523,19 +625,36 @@ export function registerTools(server, bridge) {
523
625
  server.tool(
524
626
  'figma_export_node',
525
627
  'Export a node as an image (PNG, SVG, JPG, or PDF). The image is WRITTEN TO DISK and the file path is returned — ' +
526
- 'read that file to actually view the render (inline base64 cannot be viewed, which is why this is file-first). ' +
527
- 'Response is { success, nodeId, path, format, scale, bytes } with no inline image data. ' +
528
- 'Pass outputPath to choose the destination; omit it and the file lands in the OS temp dir under figma-mcp-bridge/. ' +
529
- 'Set returnBase64: true only if you genuinely need the raw data inline instead of a file. ' +
530
- 'Exporting and LOOKING at the render is the only way to catch composition problems that property readback cannot see.',
628
+ 'read that file to actually view the render (inline base64 cannot be viewed, which is why this is file-first). ' +
629
+ 'Response is { success, nodeId, path, format, scale, bytes } with no inline image data. ' +
630
+ 'Pass outputPath to choose the destination; omit it and the file lands in the OS temp dir under figma-mcp-bridge/. ' +
631
+ 'Set returnBase64: true only if you genuinely need the raw data inline instead of a file. ' +
632
+ 'Exporting and LOOKING at the render is the only way to catch composition problems that property readback cannot see.',
531
633
  {
532
634
  nodeId: z.string().describe('The node ID to export'),
533
635
  format: z.enum(['PNG', 'SVG', 'JPG', 'PDF']).optional().default('PNG').describe('Export format'),
534
- scale: z.number().optional().default(1).describe('Export scale (1 = 100%, 2 = 200%, etc.). No need to inflate this to make the image viewable — the file on disk is viewable at any size.'),
535
- outputPath: z.string().optional().describe('Absolute file path to write the image to. Parent directories are created. Omit for an auto-named file in the OS temp dir.'),
536
- returnBase64: z.boolean().optional().default(false).describe('Return base64 image data inline instead of writing a file. Rarely what you want — the inline data cannot be viewed.')
636
+ scale: z
637
+ .number()
638
+ .optional()
639
+ .default(1)
640
+ .describe(
641
+ 'Export scale (1 = 100%, 2 = 200%, etc.). No need to inflate this to make the image viewable — the file on disk is viewable at any size.'
642
+ ),
643
+ outputPath: z
644
+ .string()
645
+ .optional()
646
+ .describe(
647
+ 'Absolute file path to write the image to. Parent directories are created. Omit for an auto-named file in the OS temp dir.'
648
+ ),
649
+ returnBase64: z
650
+ .boolean()
651
+ .optional()
652
+ .default(false)
653
+ .describe(
654
+ 'Return base64 image data inline instead of writing a file. Rarely what you want — the inline data cannot be viewed.'
655
+ )
537
656
  },
538
- async (args) => handleExportNode(bridge, args)
657
+ async args => handleExportNode(bridge, args)
539
658
  );
540
659
 
541
660
  // ============================================================
@@ -554,13 +673,26 @@ export function registerTools(server, bridge) {
554
673
  name: z.string().optional().default('Ellipse').describe('Node name'),
555
674
  fills: colorSchema.optional().describe('Fill color'),
556
675
  parentId: z.string().optional().describe('Parent node ID (defaults to current page)'),
557
- arcData: z.object({
558
- startingAngle: z.number().min(0).max(6.28319).optional().describe('Starting angle in radians (0 = 3 o\'clock)'),
559
- endingAngle: z.number().min(0).max(6.28319).optional().describe('Ending angle in radians (2*PI = full circle)'),
560
- innerRadius: z.number().min(0).max(1).optional().describe('Inner radius ratio (0 = solid, 0.5 = 50% hole)')
561
- }).optional().describe('Arc data for partial ellipses or rings')
676
+ arcData: z
677
+ .object({
678
+ startingAngle: z
679
+ .number()
680
+ .min(0)
681
+ .max(6.28319)
682
+ .optional()
683
+ .describe("Starting angle in radians (0 = 3 o'clock)"),
684
+ endingAngle: z
685
+ .number()
686
+ .min(0)
687
+ .max(6.28319)
688
+ .optional()
689
+ .describe('Ending angle in radians (2*PI = full circle)'),
690
+ innerRadius: z.number().min(0).max(1).optional().describe('Inner radius ratio (0 = solid, 0.5 = 50% hole)')
691
+ })
692
+ .optional()
693
+ .describe('Arc data for partial ellipses or rings')
562
694
  },
563
- async (args) => handleCreateEllipse(bridge, args)
695
+ async args => handleCreateEllipse(bridge, args)
564
696
  );
565
697
 
566
698
  // figma_set_effects - Set effects (shadows, blurs)
@@ -569,43 +701,65 @@ export function registerTools(server, bridge) {
569
701
  'Set effects. Replaces existing.',
570
702
  {
571
703
  nodeId: z.string().describe('The node ID to modify'),
572
- effects: z.array(z.union([
573
- z.object({
574
- type: z.enum(['DROP_SHADOW', 'INNER_SHADOW']).describe('Shadow type'),
575
- color: colorSchema.optional().describe('Shadow color'),
576
- offset: z.object({
577
- x: z.number().describe('Horizontal offset'),
578
- y: z.number().describe('Vertical offset')
579
- }).optional().describe('Shadow offset'),
580
- radius: z.number().min(0).optional().describe('Blur radius'),
581
- spread: z.number().optional().describe('Spread radius'),
582
- visible: z.boolean().optional().describe('Whether effect is visible'),
583
- blendMode: z.string().optional().describe('Blend mode')
584
- }),
585
- z.object({
586
- type: z.enum(['LAYER_BLUR', 'BACKGROUND_BLUR']).describe('Blur type'),
587
- radius: z.number().min(0).describe('Blur radius'),
588
- visible: z.boolean().optional().describe('Whether effect is visible')
589
- })
590
- ])).describe('Array of effects to apply')
591
- },
592
- async (args) => handleSetEffects(bridge, args)
704
+ effects: z
705
+ .array(
706
+ z.union([
707
+ z.object({
708
+ type: z.enum(['DROP_SHADOW', 'INNER_SHADOW']).describe('Shadow type'),
709
+ color: colorSchema.optional().describe('Shadow color'),
710
+ offset: z
711
+ .object({
712
+ x: z.number().describe('Horizontal offset'),
713
+ y: z.number().describe('Vertical offset')
714
+ })
715
+ .optional()
716
+ .describe('Shadow offset'),
717
+ radius: z.number().min(0).optional().describe('Blur radius'),
718
+ spread: z.number().optional().describe('Spread radius'),
719
+ visible: z.boolean().optional().describe('Whether effect is visible'),
720
+ blendMode: z.string().optional().describe('Blend mode')
721
+ }),
722
+ z.object({
723
+ type: z.enum(['LAYER_BLUR', 'BACKGROUND_BLUR']).describe('Blur type'),
724
+ radius: z.number().min(0).describe('Blur radius'),
725
+ visible: z.boolean().optional().describe('Whether effect is visible')
726
+ })
727
+ ])
728
+ )
729
+ .describe('Array of effects to apply')
730
+ },
731
+ async args => handleSetEffects(bridge, args)
593
732
  );
594
733
 
595
734
  // figma_set_auto_layout - Configure auto-layout
596
735
  server.tool(
597
736
  'figma_set_auto_layout',
598
737
  'Configure auto-layout on a frame. Enables responsive layouts with automatic spacing and alignment. ' +
599
- 'Changing layoutMode or primaryAxisSizingMode can clear a width/height variable bind, so the node\'s size binds are ' +
600
- 'captured before the change and re-applied after: recovered binds are listed in "rebound", and any bind that could ' +
601
- 'not be restored is named in "warnings" rather than silently lost.',
738
+ "Changing layoutMode or primaryAxisSizingMode can clear a width/height variable bind, so the node's size binds are " +
739
+ 'captured before the change and re-applied after: recovered binds are listed in "rebound", and any bind that could ' +
740
+ 'not be restored is named in "warnings" rather than silently lost.',
602
741
  {
603
742
  nodeId: z.string().describe('The frame node ID to configure'),
604
- layoutMode: z.enum(['NONE', 'HORIZONTAL', 'VERTICAL']).optional().describe('Layout direction: NONE (disable), HORIZONTAL (row), or VERTICAL (column)'),
605
- primaryAxisSizingMode: z.enum(['FIXED', 'AUTO']).optional().describe('How the frame sizes along the primary axis'),
606
- counterAxisSizingMode: z.enum(['FIXED', 'AUTO']).optional().describe('How the frame sizes along the counter axis'),
607
- primaryAxisAlignItems: z.enum(['MIN', 'CENTER', 'MAX', 'SPACE_BETWEEN']).optional().describe('Alignment of children along primary axis'),
608
- counterAxisAlignItems: z.enum(['MIN', 'CENTER', 'MAX', 'BASELINE']).optional().describe('Alignment of children along counter axis'),
743
+ layoutMode: z
744
+ .enum(['NONE', 'HORIZONTAL', 'VERTICAL'])
745
+ .optional()
746
+ .describe('Layout direction: NONE (disable), HORIZONTAL (row), or VERTICAL (column)'),
747
+ primaryAxisSizingMode: z
748
+ .enum(['FIXED', 'AUTO'])
749
+ .optional()
750
+ .describe('How the frame sizes along the primary axis'),
751
+ counterAxisSizingMode: z
752
+ .enum(['FIXED', 'AUTO'])
753
+ .optional()
754
+ .describe('How the frame sizes along the counter axis'),
755
+ primaryAxisAlignItems: z
756
+ .enum(['MIN', 'CENTER', 'MAX', 'SPACE_BETWEEN'])
757
+ .optional()
758
+ .describe('Alignment of children along primary axis'),
759
+ counterAxisAlignItems: z
760
+ .enum(['MIN', 'CENTER', 'MAX', 'BASELINE'])
761
+ .optional()
762
+ .describe('Alignment of children along counter axis'),
609
763
  paddingTop: z.number().min(0).optional().describe('Top padding in pixels'),
610
764
  paddingRight: z.number().min(0).optional().describe('Right padding in pixels'),
611
765
  paddingBottom: z.number().min(0).optional().describe('Bottom padding in pixels'),
@@ -614,7 +768,7 @@ export function registerTools(server, bridge) {
614
768
  counterAxisSpacing: z.number().min(0).optional().describe('Space between rows when wrapped'),
615
769
  layoutWrap: z.enum(['NO_WRAP', 'WRAP']).optional().describe('Whether to wrap items to new rows/columns')
616
770
  },
617
- async (args) => handleSetAutoLayout(bridge, args)
771
+ async args => handleSetAutoLayout(bridge, args)
618
772
  );
619
773
 
620
774
  // figma_get_local_styles - List local styles
@@ -622,25 +776,29 @@ export function registerTools(server, bridge) {
622
776
  'figma_get_local_styles',
623
777
  'List all local styles defined in the document (colors, text, effects, grids). TIP: Use figma_search_styles instead when looking for specific styles by name - it returns compact results and reduces token usage.',
624
778
  {
625
- type: z.enum(['PAINT', 'TEXT', 'EFFECT', 'GRID', 'ALL']).optional().default('ALL').describe('Filter by style type')
779
+ type: z
780
+ .enum(['PAINT', 'TEXT', 'EFFECT', 'GRID', 'ALL'])
781
+ .optional()
782
+ .default('ALL')
783
+ .describe('Filter by style type')
626
784
  },
627
- async (args) => handleGetLocalStyles(bridge, args)
785
+ async args => handleGetLocalStyles(bridge, args)
628
786
  );
629
787
 
630
788
  // figma_apply_style - Apply a style to a node
631
789
  server.tool(
632
790
  'figma_apply_style',
633
791
  'Apply a local style to a node. Styles provide consistent, reusable design tokens. Works for all five style ' +
634
- 'properties including text — the plugin uses the async setters (setTextStyleIdAsync / setFillStyleIdAsync / ' +
635
- 'setStrokeStyleIdAsync / setEffectStyleIdAsync / setGridStyleIdAsync) that are mandatory under ' +
636
- 'documentAccess: "dynamic-page". The applied style ID is read back off the node and returned as appliedStyleId ' +
637
- 'with verified: true; if the readback does not match, the call fails with STYLE_NOT_APPLIED instead of reporting success.',
792
+ 'properties including text — the plugin uses the async setters (setTextStyleIdAsync / setFillStyleIdAsync / ' +
793
+ 'setStrokeStyleIdAsync / setEffectStyleIdAsync / setGridStyleIdAsync) that are mandatory under ' +
794
+ 'documentAccess: "dynamic-page". The applied style ID is read back off the node and returned as appliedStyleId ' +
795
+ 'with verified: true; if the readback does not match, the call fails with STYLE_NOT_APPLIED instead of reporting success.',
638
796
  {
639
797
  nodeId: z.string().describe('The node ID to apply the style to'),
640
798
  styleId: z.string().describe('The style ID to apply'),
641
799
  property: z.enum(['fills', 'strokes', 'text', 'effects', 'grid']).describe('Which property to apply the style to')
642
800
  },
643
- async (args) => handleApplyStyle(bridge, args)
801
+ async args => handleApplyStyle(bridge, args)
644
802
  );
645
803
 
646
804
  // figma_create_component - Create a component
@@ -658,7 +816,7 @@ export function registerTools(server, bridge) {
658
816
  parentId: z.string().optional().describe('Parent node ID (defaults to current page)'),
659
817
  description: z.string().optional().describe('Component description')
660
818
  },
661
- async (args) => handleCreateComponent(bridge, args)
819
+ async args => handleCreateComponent(bridge, args)
662
820
  );
663
821
 
664
822
  // figma_create_instance - Create an instance of a component
@@ -672,7 +830,7 @@ export function registerTools(server, bridge) {
672
830
  parentId: z.string().optional().describe('Parent node ID (defaults to current page)'),
673
831
  name: z.string().optional().describe('Instance name (defaults to component name)')
674
832
  },
675
- async (args) => handleCreateInstance(bridge, args)
833
+ async args => handleCreateInstance(bridge, args)
676
834
  );
677
835
 
678
836
  // ============================================================
@@ -685,9 +843,13 @@ export function registerTools(server, bridge) {
685
843
  'figma_get_local_variables',
686
844
  'Get all local variables and variable collections from the Figma document. Returns variables with their types (COLOR, FLOAT, STRING, BOOLEAN), modes, and values. WARNING: Can return 25k+ tokens and may be truncated. Use figma_search_variables instead when looking for specific variables.',
687
845
  {
688
- type: z.enum(['COLOR', 'FLOAT', 'STRING', 'BOOLEAN', 'ALL']).optional().default('ALL').describe('Filter by variable type')
846
+ type: z
847
+ .enum(['COLOR', 'FLOAT', 'STRING', 'BOOLEAN', 'ALL'])
848
+ .optional()
849
+ .default('ALL')
850
+ .describe('Filter by variable type')
689
851
  },
690
- async (args) => handleGetLocalVariables(bridge, args)
852
+ async args => handleGetLocalVariables(bridge, args)
691
853
  );
692
854
 
693
855
  // figma_search_variables - Search variables with filtering (optimized for reduced token usage)
@@ -696,14 +858,32 @@ export function registerTools(server, bridge) {
696
858
  'figma_search_variables',
697
859
  'Search for variables by name pattern. More efficient than get_local_variables - use this when looking for specific variables like "tailwind/orange/*" or "*primary*". Returns compact results to reduce token usage. PREFERRED over figma_get_local_variables for efficiency (~500 tokens vs 25k+).',
698
860
  {
699
- namePattern: z.string().optional().describe('Filter by name pattern with wildcards. Examples: "tailwind/orange/*", "*primary*", "spacing/*". Use * for any characters.'),
700
- nameContains: z.string().optional().describe('Simple filter: find variables where name contains this string (case-insensitive). Example: "orange" matches "tailwind/orange/500"'),
701
- type: z.enum(['COLOR', 'FLOAT', 'STRING', 'BOOLEAN', 'ALL']).optional().default('ALL').describe('Filter by variable type'),
861
+ namePattern: z
862
+ .string()
863
+ .optional()
864
+ .describe(
865
+ 'Filter by name pattern with wildcards. Examples: "tailwind/orange/*", "*primary*", "spacing/*". Use * for any characters.'
866
+ ),
867
+ nameContains: z
868
+ .string()
869
+ .optional()
870
+ .describe(
871
+ 'Simple filter: find variables where name contains this string (case-insensitive). Example: "orange" matches "tailwind/orange/500"'
872
+ ),
873
+ type: z
874
+ .enum(['COLOR', 'FLOAT', 'STRING', 'BOOLEAN', 'ALL'])
875
+ .optional()
876
+ .default('ALL')
877
+ .describe('Filter by variable type'),
702
878
  collectionName: z.string().optional().describe('Filter by collection name (exact match or partial)'),
703
- compact: z.boolean().optional().default(true).describe('Return minimal data (id, name, hex/value only). Set false for full metadata.'),
879
+ compact: z
880
+ .boolean()
881
+ .optional()
882
+ .default(true)
883
+ .describe('Return minimal data (id, name, hex/value only). Set false for full metadata.'),
704
884
  limit: z.number().optional().default(50).describe('Maximum number of variables to return')
705
885
  },
706
- async (args) => handleSearchVariables(bridge, args)
886
+ async args => handleSearchVariables(bridge, args)
707
887
  );
708
888
 
709
889
  // ============================================================
@@ -716,14 +896,28 @@ export function registerTools(server, bridge) {
716
896
  'Search for nodes by name within a scope. PREFERRED for finding specific frames, sections, or elements. Requires parentId to scope search. Returns compact results (~50 tokens/node vs ~500 for full).',
717
897
  {
718
898
  parentId: z.string().describe('Scope to search (page/frame/section ID). REQUIRED to prevent runaway queries.'),
719
- nameContains: z.string().optional().describe('Case-insensitive substring match. Example: "color scale" matches "Color Scale Section"'),
899
+ nameContains: z
900
+ .string()
901
+ .optional()
902
+ .describe('Case-insensitive substring match. Example: "color scale" matches "Color Scale Section"'),
720
903
  namePattern: z.string().optional().describe('Glob pattern with wildcards. Examples: "*button*", "Header/*"'),
721
- types: z.array(z.string()).optional().describe('Filter by node types: FRAME, TEXT, SECTION, COMPONENT, INSTANCE, GROUP, etc.'),
722
- maxDepth: z.number().optional().default(-1).describe('How deep to search (-1 = unlimited, 1 = immediate children only)'),
723
- compact: z.boolean().optional().default(true).describe('Return minimal data (id, name, type, parentId, childCount)'),
904
+ types: z
905
+ .array(z.string())
906
+ .optional()
907
+ .describe('Filter by node types: FRAME, TEXT, SECTION, COMPONENT, INSTANCE, GROUP, etc.'),
908
+ maxDepth: z
909
+ .number()
910
+ .optional()
911
+ .default(-1)
912
+ .describe('How deep to search (-1 = unlimited, 1 = immediate children only)'),
913
+ compact: z
914
+ .boolean()
915
+ .optional()
916
+ .default(true)
917
+ .describe('Return minimal data (id, name, type, parentId, childCount)'),
724
918
  limit: z.number().optional().default(50).describe('Maximum number of results')
725
919
  },
726
- async (args) => handleSearchNodes(bridge, args)
920
+ async args => handleSearchNodes(bridge, args)
727
921
  );
728
922
 
729
923
  // figma_search_components - Search local components by name
@@ -733,11 +927,15 @@ export function registerTools(server, bridge) {
733
927
  {
734
928
  nameContains: z.string().optional().describe('Case-insensitive substring match'),
735
929
  namePattern: z.string().optional().describe('Glob pattern with wildcards'),
736
- includeVariants: z.boolean().optional().default(false).describe('Include individual variants from component sets'),
930
+ includeVariants: z
931
+ .boolean()
932
+ .optional()
933
+ .default(false)
934
+ .describe('Include individual variants from component sets'),
737
935
  compact: z.boolean().optional().default(true).describe('Return minimal data'),
738
936
  limit: z.number().optional().default(50).describe('Maximum number of results')
739
937
  },
740
- async (args) => handleSearchComponents(bridge, args)
938
+ async args => handleSearchComponents(bridge, args)
741
939
  );
742
940
 
743
941
  // figma_search_styles - Search local styles by name
@@ -746,11 +944,15 @@ export function registerTools(server, bridge) {
746
944
  'Search local styles by name. More efficient than figma_get_local_styles when looking for specific styles.',
747
945
  {
748
946
  nameContains: z.string().optional().describe('Case-insensitive substring match'),
749
- type: z.enum(['PAINT', 'TEXT', 'EFFECT', 'GRID', 'ALL']).optional().default('ALL').describe('Filter by style type'),
947
+ type: z
948
+ .enum(['PAINT', 'TEXT', 'EFFECT', 'GRID', 'ALL'])
949
+ .optional()
950
+ .default('ALL')
951
+ .describe('Filter by style type'),
750
952
  compact: z.boolean().optional().default(true).describe('Return minimal data'),
751
953
  limit: z.number().optional().default(50).describe('Maximum number of results')
752
954
  },
753
- async (args) => handleSearchStyles(bridge, args)
955
+ async args => handleSearchStyles(bridge, args)
754
956
  );
755
957
 
756
958
  // figma_get_children - Get immediate children of a node
@@ -759,40 +961,68 @@ export function registerTools(server, bridge) {
759
961
  'Get immediate children of a node. Use for browsing hierarchy one level at a time. More efficient than figma_get_nodes for exploring structure. Compact results include x/y, so they can be used to measure layout (e.g. which children share a row after wrapping). Composite instance-sublayer parent IDs ("I<instanceId>;<childId>") resolve here too.',
760
962
  {
761
963
  parentId: z.string().describe('Node ID to get children of. REQUIRED.'),
762
- compact: z.boolean().optional().default(true).describe('Return minimal data (id, name, type, x, y, parentId, childCount). Set false for the full ~40-property serialization.')
964
+ compact: z
965
+ .boolean()
966
+ .optional()
967
+ .default(true)
968
+ .describe(
969
+ 'Return minimal data (id, name, type, x, y, parentId, childCount). Set false for the full ~40-property serialization.'
970
+ )
763
971
  },
764
- async (args) => handleGetChildren(bridge, args)
972
+ async args => handleGetChildren(bridge, args)
765
973
  );
766
974
 
767
975
  // figma_set_variable - Set variable value or bind to node
768
976
  server.tool(
769
977
  'figma_set_variable',
770
978
  'Set the value of an existing variable for a specific mode, or bind a variable to a node property OR to a local style. Styles are supported: pass styleId (or a style ID as nodeId) with field. TEXT styles bind fontFamily, fontSize, fontStyle, fontWeight, letterSpacing, lineHeight, paragraphSpacing, paragraphIndent; PAINT styles bind their color via field "paints". Style binds echo the style\'s boundVariables so the bind is verifiable in the same call. ' +
771
- 'Node binds are VERIFIED: after binding, node.boundVariables is re-read and the response returns it with verified: true. ' +
772
- 'If the bind did not land, the call fails with BIND_NOT_APPLIED rather than reporting success — this catches Figma\'s ' +
773
- 'silent no-ops. Binding "width" or "height" on an instance sublayer is rejected up front with ' +
774
- 'INSTANCE_SUBLAYER_RESTRICTED: Figma does not allow that override, so bind it on the component master instead (or size ' +
775
- 'the sublayer with figma_set_layout_align: STRETCH).',
979
+ 'Node binds are VERIFIED: after binding, node.boundVariables is re-read and the response returns it with verified: true. ' +
980
+ "If the bind did not land, the call fails with BIND_NOT_APPLIED rather than reporting success — this catches Figma's " +
981
+ 'silent no-ops. Binding "width" or "height" on an instance sublayer is rejected up front with ' +
982
+ 'INSTANCE_SUBLAYER_RESTRICTED: Figma does not allow that override, so bind it on the component master instead (or size ' +
983
+ 'the sublayer with figma_set_layout_align: STRETCH).',
776
984
  {
777
985
  variableId: z.string().describe('The variable ID to set or bind'),
778
986
  modeId: z.string().optional().describe('Mode ID to set value for (required when setting value)'),
779
- value: z.union([
780
- z.number(),
781
- z.string(),
782
- z.boolean(),
783
- z.object({
784
- r: z.number().min(0).max(1).describe('Red (0-1)'),
785
- g: z.number().min(0).max(1).describe('Green (0-1)'),
786
- b: z.number().min(0).max(1).describe('Blue (0-1)'),
787
- a: z.number().min(0).max(1).optional().describe('Alpha (0-1)')
788
- })
789
- ]).optional().describe('The value to set (number, string, boolean, or color object)'),
790
- nodeId: z.string().optional().describe('Node ID to bind variable to (for binding operation). A style ID passed here is routed to the style path.'),
791
- styleId: z.string().optional().describe('Local style ID to bind variable to (e.g., "S:abc123..."). Use instead of nodeId to bind a TEXT or PAINT style.'),
792
- field: z.string().optional().describe('Field to bind. Nodes: "opacity", "cornerRadius", "fills", "strokes", etc. Text styles: "fontSize", "lineHeight", "letterSpacing", "paragraphSpacing", "paragraphIndent", "fontFamily", "fontStyle", "fontWeight". Paint styles: "paints".'),
793
- paintIndex: z.number().optional().default(0).describe('Paint array index when binding to fills, strokes, or a paint style')
987
+ value: z
988
+ .union([
989
+ z.number(),
990
+ z.string(),
991
+ z.boolean(),
992
+ z.object({
993
+ r: z.number().min(0).max(1).describe('Red (0-1)'),
994
+ g: z.number().min(0).max(1).describe('Green (0-1)'),
995
+ b: z.number().min(0).max(1).describe('Blue (0-1)'),
996
+ a: z.number().min(0).max(1).optional().describe('Alpha (0-1)')
997
+ })
998
+ ])
999
+ .optional()
1000
+ .describe('The value to set (number, string, boolean, or color object)'),
1001
+ nodeId: z
1002
+ .string()
1003
+ .optional()
1004
+ .describe(
1005
+ 'Node ID to bind variable to (for binding operation). A style ID passed here is routed to the style path.'
1006
+ ),
1007
+ styleId: z
1008
+ .string()
1009
+ .optional()
1010
+ .describe(
1011
+ 'Local style ID to bind variable to (e.g., "S:abc123..."). Use instead of nodeId to bind a TEXT or PAINT style.'
1012
+ ),
1013
+ field: z
1014
+ .string()
1015
+ .optional()
1016
+ .describe(
1017
+ 'Field to bind. Nodes: "opacity", "cornerRadius", "fills", "strokes", etc. Text styles: "fontSize", "lineHeight", "letterSpacing", "paragraphSpacing", "paragraphIndent", "fontFamily", "fontStyle", "fontWeight". Paint styles: "paints".'
1018
+ ),
1019
+ paintIndex: z
1020
+ .number()
1021
+ .optional()
1022
+ .default(0)
1023
+ .describe('Paint array index when binding to fills, strokes, or a paint style')
794
1024
  },
795
- async (args) => handleSetVariable(bridge, args)
1025
+ async args => handleSetVariable(bridge, args)
796
1026
  );
797
1027
 
798
1028
  // figma_create_line - Create a line
@@ -807,10 +1037,14 @@ export function registerTools(server, bridge) {
807
1037
  name: z.string().optional().default('Line').describe('Node name'),
808
1038
  strokeWeight: z.number().optional().default(1).describe('Stroke weight in pixels'),
809
1039
  strokes: colorSchema.optional().describe('Stroke color'),
810
- strokeCap: z.enum(['NONE', 'ROUND', 'SQUARE', 'ARROW_LINES', 'ARROW_EQUILATERAL']).optional().default('NONE').describe('Stroke cap style (ARROW_LINES/ARROW_EQUILATERAL for arrows)'),
1040
+ strokeCap: z
1041
+ .enum(['NONE', 'ROUND', 'SQUARE', 'ARROW_LINES', 'ARROW_EQUILATERAL'])
1042
+ .optional()
1043
+ .default('NONE')
1044
+ .describe('Stroke cap style (ARROW_LINES/ARROW_EQUILATERAL for arrows)'),
811
1045
  parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
812
1046
  },
813
- async (args) => handleCreateLine(bridge, args)
1047
+ async args => handleCreateLine(bridge, args)
814
1048
  );
815
1049
 
816
1050
  // figma_set_constraints - Set resize constraints
@@ -819,10 +1053,16 @@ export function registerTools(server, bridge) {
819
1053
  'Set resize constraints on a node. Constraints control how a node resizes when its parent frame resizes. Only works on nodes inside frames (not auto-layout frames).',
820
1054
  {
821
1055
  nodeId: z.string().describe('The node ID to set constraints on'),
822
- horizontal: z.enum(['MIN', 'CENTER', 'MAX', 'STRETCH', 'SCALE']).optional().describe('Horizontal constraint: MIN (left), CENTER, MAX (right), STRETCH (left+right), SCALE (proportional)'),
823
- vertical: z.enum(['MIN', 'CENTER', 'MAX', 'STRETCH', 'SCALE']).optional().describe('Vertical constraint: MIN (top), CENTER, MAX (bottom), STRETCH (top+bottom), SCALE (proportional)')
1056
+ horizontal: z
1057
+ .enum(['MIN', 'CENTER', 'MAX', 'STRETCH', 'SCALE'])
1058
+ .optional()
1059
+ .describe('Horizontal constraint: MIN (left), CENTER, MAX (right), STRETCH (left+right), SCALE (proportional)'),
1060
+ vertical: z
1061
+ .enum(['MIN', 'CENTER', 'MAX', 'STRETCH', 'SCALE'])
1062
+ .optional()
1063
+ .describe('Vertical constraint: MIN (top), CENTER, MAX (bottom), STRETCH (top+bottom), SCALE (proportional)')
824
1064
  },
825
- async (args) => handleSetConstraints(bridge, args)
1065
+ async args => handleSetConstraints(bridge, args)
826
1066
  );
827
1067
 
828
1068
  // ============================================================
@@ -895,7 +1135,7 @@ export function registerTools(server, bridge) {
895
1135
  {
896
1136
  nodeId: z.string().describe('The instance node ID to detach')
897
1137
  },
898
- async (args) => handleDetachInstance(bridge, args)
1138
+ async args => handleDetachInstance(bridge, args)
899
1139
  );
900
1140
 
901
1141
  // ============================================================
@@ -908,11 +1148,22 @@ export function registerTools(server, bridge) {
908
1148
  'Set how a child behaves within an auto-layout frame. Controls individual alignment (STRETCH), growth (fill container), and absolute positioning.',
909
1149
  {
910
1150
  nodeId: z.string().describe('The child node ID to modify'),
911
- layoutAlign: z.enum(['MIN', 'CENTER', 'MAX', 'STRETCH', 'INHERIT']).optional().describe('Counter-axis alignment: STRETCH to fill width/height'),
912
- layoutGrow: z.number().min(0).max(1).optional().describe('Primary-axis growth: 0 = fixed size, 1 = fill available space'),
913
- layoutPositioning: z.enum(['AUTO', 'ABSOLUTE']).optional().describe('AUTO = follow auto-layout, ABSOLUTE = manually positioned')
914
- },
915
- async (args) => handleSetLayoutAlign(bridge, args)
1151
+ layoutAlign: z
1152
+ .enum(['MIN', 'CENTER', 'MAX', 'STRETCH', 'INHERIT'])
1153
+ .optional()
1154
+ .describe('Counter-axis alignment: STRETCH to fill width/height'),
1155
+ layoutGrow: z
1156
+ .number()
1157
+ .min(0)
1158
+ .max(1)
1159
+ .optional()
1160
+ .describe('Primary-axis growth: 0 = fixed size, 1 = fill available space'),
1161
+ layoutPositioning: z
1162
+ .enum(['AUTO', 'ABSOLUTE'])
1163
+ .optional()
1164
+ .describe('AUTO = follow auto-layout, ABSOLUTE = manually positioned')
1165
+ },
1166
+ async args => handleSetLayoutAlign(bridge, args)
916
1167
  );
917
1168
 
918
1169
  // DISABLED - Uncomment to enable custom vector paths
@@ -943,27 +1194,27 @@ export function registerTools(server, bridge) {
943
1194
  nodeIds: z.array(z.string()).optional().describe('Array of node IDs to rename (batch)'),
944
1195
  name: z.string().describe('The new name for the node(s)')
945
1196
  },
946
- async (args) => handleRenameNode(bridge, args)
1197
+ async args => handleRenameNode(bridge, args)
947
1198
  );
948
1199
 
949
1200
  // figma_reorder_node - Change z-order of a node
950
1201
  server.tool(
951
1202
  'figma_reorder_node',
952
1203
  'Change the z-order (layer order) of a node among its siblings. A numeric position is the FINAL index the node ' +
953
- 'ends up at — index 2 means the node is at index 2 when the call returns, not one off from it. Figma sorts children ' +
954
- 'back-to-front, so 0 is the BOTTOM of the layer stack and childCount - 1 is the top; "back" is 0 and "front" is the ' +
955
- 'last index. Out-of-range indices are clamped into range and the response reports clamped: true with a message. ' +
956
- 'The final index is verified by reading it back — a mismatch fails with REORDER_FAILED rather than reporting success. ' +
957
- 'Reordering children of an INSTANCE is blocked by Figma and returns INSTANCE_SUBLAYER_RESTRICTED; reorder on the master instead.',
1204
+ 'ends up at — index 2 means the node is at index 2 when the call returns, not one off from it. Figma sorts children ' +
1205
+ 'back-to-front, so 0 is the BOTTOM of the layer stack and childCount - 1 is the top; "back" is 0 and "front" is the ' +
1206
+ 'last index. Out-of-range indices are clamped into range and the response reports clamped: true with a message. ' +
1207
+ 'The final index is verified by reading it back — a mismatch fails with REORDER_FAILED rather than reporting success. ' +
1208
+ 'Reordering children of an INSTANCE is blocked by Figma and returns INSTANCE_SUBLAYER_RESTRICTED; reorder on the master instead.',
958
1209
  {
959
1210
  nodeId: z.string().describe('The node ID to reorder'),
960
- position: z.union([
961
- z.literal('front'),
962
- z.literal('back'),
963
- z.number()
964
- ]).describe('Final position: "front" (top of stack), "back" (bottom), or the final zero-based index among siblings (0 = bottom)')
1211
+ position: z
1212
+ .union([z.literal('front'), z.literal('back'), z.number()])
1213
+ .describe(
1214
+ 'Final position: "front" (top of stack), "back" (bottom), or the final zero-based index among siblings (0 = bottom)'
1215
+ )
965
1216
  },
966
- async (args) => handleReorderNode(bridge, args)
1217
+ async args => handleReorderNode(bridge, args)
967
1218
  );
968
1219
 
969
1220
  // ============================================================
@@ -981,19 +1232,28 @@ export function registerTools(server, bridge) {
981
1232
  fontStyle: z.string().optional().describe('Font style (e.g., "Bold", "Regular")'),
982
1233
  textCase: z.enum(['ORIGINAL', 'UPPER', 'LOWER', 'TITLE']).optional().describe('Text case transformation'),
983
1234
  textDecoration: z.enum(['NONE', 'UNDERLINE', 'STRIKETHROUGH']).optional().describe('Text decoration'),
984
- lineHeight: z.union([
985
- z.object({ unit: z.literal('AUTO') }),
986
- z.object({ unit: z.literal('PIXELS'), value: z.number() }),
987
- z.object({ unit: z.literal('PERCENT'), value: z.number() })
988
- ]).optional().describe('Line height (AUTO, or PIXELS/PERCENT with value)'),
989
- letterSpacing: z.union([
990
- z.object({ unit: z.literal('PIXELS'), value: z.number() }),
991
- z.object({ unit: z.literal('PERCENT'), value: z.number() })
992
- ]).optional().describe('Letter spacing (PIXELS or PERCENT with value)'),
993
- textAlignHorizontal: z.enum(['LEFT', 'CENTER', 'RIGHT', 'JUSTIFIED']).optional().describe('Horizontal text alignment'),
1235
+ lineHeight: z
1236
+ .union([
1237
+ z.object({ unit: z.literal('AUTO') }),
1238
+ z.object({ unit: z.literal('PIXELS'), value: z.number() }),
1239
+ z.object({ unit: z.literal('PERCENT'), value: z.number() })
1240
+ ])
1241
+ .optional()
1242
+ .describe('Line height (AUTO, or PIXELS/PERCENT with value)'),
1243
+ letterSpacing: z
1244
+ .union([
1245
+ z.object({ unit: z.literal('PIXELS'), value: z.number() }),
1246
+ z.object({ unit: z.literal('PERCENT'), value: z.number() })
1247
+ ])
1248
+ .optional()
1249
+ .describe('Letter spacing (PIXELS or PERCENT with value)'),
1250
+ textAlignHorizontal: z
1251
+ .enum(['LEFT', 'CENTER', 'RIGHT', 'JUSTIFIED'])
1252
+ .optional()
1253
+ .describe('Horizontal text alignment'),
994
1254
  textAlignVertical: z.enum(['TOP', 'CENTER', 'BOTTOM']).optional().describe('Vertical text alignment')
995
1255
  },
996
- async (args) => handleSetTextStyle(bridge, args)
1256
+ async args => handleSetTextStyle(bridge, args)
997
1257
  );
998
1258
 
999
1259
  // figma_create_paint_style - Create a local paint style
@@ -1005,7 +1265,7 @@ export function registerTools(server, bridge) {
1005
1265
  fills: colorSchema.describe('Fill color - use { color: "#RRGGBB" } for simple colors'),
1006
1266
  description: z.string().optional().describe('Style description')
1007
1267
  },
1008
- async (args) => handleCreatePaintStyle(bridge, args)
1268
+ async args => handleCreatePaintStyle(bridge, args)
1009
1269
  );
1010
1270
 
1011
1271
  // figma_create_text_style - Create a local text style
@@ -1017,20 +1277,26 @@ export function registerTools(server, bridge) {
1017
1277
  fontFamily: z.string().optional().default('Inter').describe('Font family'),
1018
1278
  fontStyle: z.string().optional().default('Regular').describe('Font style (Regular, Bold, etc.)'),
1019
1279
  fontSize: z.number().optional().default(16).describe('Font size in pixels'),
1020
- lineHeight: z.union([
1021
- z.object({ unit: z.literal('AUTO') }),
1022
- z.object({ unit: z.literal('PIXELS'), value: z.number() }),
1023
- z.object({ unit: z.literal('PERCENT'), value: z.number() })
1024
- ]).optional().describe('Line height'),
1025
- letterSpacing: z.union([
1026
- z.object({ unit: z.literal('PIXELS'), value: z.number() }),
1027
- z.object({ unit: z.literal('PERCENT'), value: z.number() })
1028
- ]).optional().describe('Letter spacing'),
1280
+ lineHeight: z
1281
+ .union([
1282
+ z.object({ unit: z.literal('AUTO') }),
1283
+ z.object({ unit: z.literal('PIXELS'), value: z.number() }),
1284
+ z.object({ unit: z.literal('PERCENT'), value: z.number() })
1285
+ ])
1286
+ .optional()
1287
+ .describe('Line height'),
1288
+ letterSpacing: z
1289
+ .union([
1290
+ z.object({ unit: z.literal('PIXELS'), value: z.number() }),
1291
+ z.object({ unit: z.literal('PERCENT'), value: z.number() })
1292
+ ])
1293
+ .optional()
1294
+ .describe('Letter spacing'),
1029
1295
  textCase: z.enum(['ORIGINAL', 'UPPER', 'LOWER', 'TITLE']).optional().describe('Text case'),
1030
1296
  textDecoration: z.enum(['NONE', 'UNDERLINE', 'STRIKETHROUGH']).optional().describe('Text decoration'),
1031
1297
  description: z.string().optional().describe('Style description')
1032
1298
  },
1033
- async (args) => handleCreateTextStyle(bridge, args)
1299
+ async args => handleCreateTextStyle(bridge, args)
1034
1300
  );
1035
1301
 
1036
1302
  // figma_delete_style - Delete a local style
@@ -1040,7 +1306,7 @@ export function registerTools(server, bridge) {
1040
1306
  {
1041
1307
  styleId: z.string().describe('The style ID to delete (e.g., "S:abc123...")')
1042
1308
  },
1043
- async (args) => handleDeleteStyle(bridge, args)
1309
+ async args => handleDeleteStyle(bridge, args)
1044
1310
  );
1045
1311
 
1046
1312
  // figma_create_variable_collection - Create a variable collection
@@ -1051,7 +1317,7 @@ export function registerTools(server, bridge) {
1051
1317
  name: z.string().describe('Collection name'),
1052
1318
  modes: z.array(z.string()).optional().describe('Mode names (defaults to ["Mode 1"])')
1053
1319
  },
1054
- async (args) => handleCreateVariableCollection(bridge, args)
1320
+ async args => handleCreateVariableCollection(bridge, args)
1055
1321
  );
1056
1322
 
1057
1323
  // figma_create_variable - Create a variable
@@ -1062,24 +1328,49 @@ export function registerTools(server, bridge) {
1062
1328
  collectionId: z.string().describe('Variable collection ID'),
1063
1329
  name: z.string().describe('Variable name (use "/" for groups, e.g., "colors/primary")'),
1064
1330
  type: z.enum(['COLOR', 'FLOAT', 'STRING', 'BOOLEAN']).describe('Variable type'),
1065
- value: z.union([
1066
- z.string(),
1067
- z.number(),
1068
- z.boolean(),
1069
- z.object({ r: z.number(), g: z.number(), b: z.number(), a: z.number().optional() }),
1070
- z.object({ color: z.string() })
1071
- ]).optional().describe('Initial value for default mode'),
1331
+ value: z
1332
+ .union([
1333
+ z.string(),
1334
+ z.number(),
1335
+ z.boolean(),
1336
+ z.object({ r: z.number(), g: z.number(), b: z.number(), a: z.number().optional() }),
1337
+ z.object({ color: z.string() })
1338
+ ])
1339
+ .optional()
1340
+ .describe('Initial value for default mode'),
1072
1341
  aliasOf: z.string().optional().describe('Variable ID to alias (instead of direct value)'),
1073
1342
  description: z.string().optional().describe('Variable description'),
1074
- scopes: z.array(z.enum([
1075
- 'ALL_SCOPES', 'TEXT_CONTENT', 'CORNER_RADIUS', 'WIDTH_HEIGHT',
1076
- 'GAP', 'ALL_FILLS', 'FRAME_FILL', 'SHAPE_FILL', 'TEXT_FILL', 'STROKE_COLOR',
1077
- 'STROKE_FLOAT', 'EFFECT_FLOAT', 'EFFECT_COLOR', 'OPACITY', 'FONT_FAMILY',
1078
- 'FONT_STYLE', 'FONT_WEIGHT', 'FONT_SIZE', 'LINE_HEIGHT', 'LETTER_SPACING',
1079
- 'PARAGRAPH_SPACING', 'PARAGRAPH_INDENT'
1080
- ])).optional().describe('Where this variable can be used')
1081
- },
1082
- async (args) => handleCreateVariable(bridge, args)
1343
+ scopes: z
1344
+ .array(
1345
+ z.enum([
1346
+ 'ALL_SCOPES',
1347
+ 'TEXT_CONTENT',
1348
+ 'CORNER_RADIUS',
1349
+ 'WIDTH_HEIGHT',
1350
+ 'GAP',
1351
+ 'ALL_FILLS',
1352
+ 'FRAME_FILL',
1353
+ 'SHAPE_FILL',
1354
+ 'TEXT_FILL',
1355
+ 'STROKE_COLOR',
1356
+ 'STROKE_FLOAT',
1357
+ 'EFFECT_FLOAT',
1358
+ 'EFFECT_COLOR',
1359
+ 'OPACITY',
1360
+ 'FONT_FAMILY',
1361
+ 'FONT_STYLE',
1362
+ 'FONT_WEIGHT',
1363
+ 'FONT_SIZE',
1364
+ 'LINE_HEIGHT',
1365
+ 'LETTER_SPACING',
1366
+ 'PARAGRAPH_SPACING',
1367
+ 'PARAGRAPH_INDENT'
1368
+ ])
1369
+ )
1370
+ .optional()
1371
+ .describe('Where this variable can be used')
1372
+ },
1373
+ async args => handleCreateVariable(bridge, args)
1083
1374
  );
1084
1375
 
1085
1376
  // figma_rename_variable - Rename an existing variable
@@ -1090,7 +1381,7 @@ export function registerTools(server, bridge) {
1090
1381
  variableId: z.string().describe('The variable ID to rename'),
1091
1382
  name: z.string().describe('The new name for the variable (use "/" for groups)')
1092
1383
  },
1093
- async (args) => handleRenameVariable(bridge, args)
1384
+ async args => handleRenameVariable(bridge, args)
1094
1385
  );
1095
1386
 
1096
1387
  // figma_delete_variables - Delete one or more variables
@@ -1100,7 +1391,7 @@ export function registerTools(server, bridge) {
1100
1391
  {
1101
1392
  variableIds: z.array(z.string()).describe('Array of variable IDs to delete')
1102
1393
  },
1103
- async (args) => handleDeleteVariables(bridge, args)
1394
+ async args => handleDeleteVariables(bridge, args)
1104
1395
  );
1105
1396
 
1106
1397
  // figma_delete_variable_collection - Delete a variable collection
@@ -1110,7 +1401,7 @@ export function registerTools(server, bridge) {
1110
1401
  {
1111
1402
  collectionId: z.string().describe('The collection ID to delete')
1112
1403
  },
1113
- async (args) => handleDeleteVariableCollection(bridge, args)
1404
+ async args => handleDeleteVariableCollection(bridge, args)
1114
1405
  );
1115
1406
 
1116
1407
  // figma_rename_variable_collection - Rename a variable collection
@@ -1121,7 +1412,7 @@ export function registerTools(server, bridge) {
1121
1412
  collectionId: z.string().describe('The collection ID to rename'),
1122
1413
  name: z.string().describe('The new name for the collection')
1123
1414
  },
1124
- async (args) => handleRenameVariableCollection(bridge, args)
1415
+ async args => handleRenameVariableCollection(bridge, args)
1125
1416
  );
1126
1417
 
1127
1418
  // figma_rename_mode - Rename a mode in a collection
@@ -1133,7 +1424,7 @@ export function registerTools(server, bridge) {
1133
1424
  modeId: z.string().describe('The mode ID to rename'),
1134
1425
  name: z.string().describe('The new name for the mode')
1135
1426
  },
1136
- async (args) => handleRenameMode(bridge, args)
1427
+ async args => handleRenameMode(bridge, args)
1137
1428
  );
1138
1429
 
1139
1430
  // figma_add_mode - Add a mode to a collection
@@ -1144,7 +1435,7 @@ export function registerTools(server, bridge) {
1144
1435
  collectionId: z.string().describe('The collection ID to add mode to'),
1145
1436
  name: z.string().describe('Name for the new mode')
1146
1437
  },
1147
- async (args) => handleAddMode(bridge, args)
1438
+ async args => handleAddMode(bridge, args)
1148
1439
  );
1149
1440
 
1150
1441
  // figma_delete_mode - Delete a mode from a collection
@@ -1155,7 +1446,7 @@ export function registerTools(server, bridge) {
1155
1446
  collectionId: z.string().describe('The collection ID containing the mode'),
1156
1447
  modeId: z.string().describe('The mode ID to delete')
1157
1448
  },
1158
- async (args) => handleDeleteMode(bridge, args)
1449
+ async args => handleDeleteMode(bridge, args)
1159
1450
  );
1160
1451
 
1161
1452
  // figma_set_variable_mode - Pin or unpin an explicit variable mode on nodes/pages
@@ -1165,26 +1456,37 @@ export function registerTools(server, bridge) {
1165
1456
  {
1166
1457
  nodeIds: z.array(z.string()).describe('Array of node IDs or page IDs to pin/unpin'),
1167
1458
  collectionId: z.string().describe('Variable collection ID the pin applies to (pins are per-collection)'),
1168
- modeId: z.string().optional().describe('Mode ID to pin. Required unless clear is true. Must belong to collectionId — the error lists the valid modes if it does not.'),
1169
- clear: z.boolean().optional().default(false).describe('true to remove this collection\'s pin from the nodes instead of setting one')
1459
+ modeId: z
1460
+ .string()
1461
+ .optional()
1462
+ .describe(
1463
+ 'Mode ID to pin. Required unless clear is true. Must belong to collectionId — the error lists the valid modes if it does not.'
1464
+ ),
1465
+ clear: z
1466
+ .boolean()
1467
+ .optional()
1468
+ .default(false)
1469
+ .describe("true to remove this collection's pin from the nodes instead of setting one")
1170
1470
  },
1171
- async (args) => handleSetVariableMode(bridge, args)
1471
+ async args => handleSetVariableMode(bridge, args)
1172
1472
  );
1173
1473
 
1174
1474
  // figma_unbind_variable - Remove variable binding from a node
1175
1475
  server.tool(
1176
1476
  'figma_unbind_variable',
1177
1477
  'Remove a variable binding from a node property. The unbind is verified by readback (UNBIND_FAILED if the field is ' +
1178
- 'still bound) and the response echoes the node\'s remaining boundVariables. ' +
1179
- 'Special handling for minWidth / maxWidth / minHeight / maxHeight: unbinding one of these leaves the last resolved ' +
1180
- 'number behind as a hard literal clamp, so the literal is cleared to null too — the response reports previousLiteral ' +
1181
- 'and clearedLiteral. Set a new limit with figma_set_size_limits.',
1478
+ "still bound) and the response echoes the node's remaining boundVariables. " +
1479
+ 'Special handling for minWidth / maxWidth / minHeight / maxHeight: unbinding one of these leaves the last resolved ' +
1480
+ 'number behind as a hard literal clamp, so the literal is cleared to null too — the response reports previousLiteral ' +
1481
+ 'and clearedLiteral. Set a new limit with figma_set_size_limits.',
1182
1482
  {
1183
1483
  nodeId: z.string().describe('The node ID to unbind from'),
1184
- field: z.string().describe('The field to unbind (fills, strokes, opacity, cornerRadius, minWidth, maxWidth, etc.)'),
1484
+ field: z
1485
+ .string()
1486
+ .describe('The field to unbind (fills, strokes, opacity, cornerRadius, minWidth, maxWidth, etc.)'),
1185
1487
  paintIndex: z.number().optional().default(0).describe('Paint array index for fills/strokes')
1186
1488
  },
1187
- async (args) => handleUnbindVariable(bridge, args)
1489
+ async args => handleUnbindVariable(bridge, args)
1188
1490
  );
1189
1491
 
1190
1492
  // ============================================================
@@ -1199,7 +1501,7 @@ export function registerTools(server, bridge) {
1199
1501
  name: z.string().describe('Name for the new page'),
1200
1502
  index: z.number().optional().describe('Position in the page list (0 = first). Defaults to end.')
1201
1503
  },
1202
- async (args) => handleCreatePage(bridge, args)
1504
+ async args => handleCreatePage(bridge, args)
1203
1505
  );
1204
1506
 
1205
1507
  // figma_rename_page - Rename a page
@@ -1210,7 +1512,7 @@ export function registerTools(server, bridge) {
1210
1512
  pageId: z.string().describe('The page ID to rename'),
1211
1513
  name: z.string().describe('The new name for the page')
1212
1514
  },
1213
- async (args) => handleRenamePage(bridge, args)
1515
+ async args => handleRenamePage(bridge, args)
1214
1516
  );
1215
1517
 
1216
1518
  // figma_delete_page - Delete a page
@@ -1220,7 +1522,7 @@ export function registerTools(server, bridge) {
1220
1522
  {
1221
1523
  pageId: z.string().describe('The page ID to delete')
1222
1524
  },
1223
- async (args) => handleDeletePage(bridge, args)
1525
+ async args => handleDeletePage(bridge, args)
1224
1526
  );
1225
1527
 
1226
1528
  // DISABLED - Uncomment to enable page reordering
@@ -1248,7 +1550,7 @@ export function registerTools(server, bridge) {
1248
1550
  newParentId: z.string().describe('The new parent node ID (must be a frame, group, or page)'),
1249
1551
  index: z.number().optional().describe('Position within the new parent (0 = bottom/back). Defaults to top/front.')
1250
1552
  },
1251
- async (args) => handleReparentNodes(bridge, args)
1553
+ async args => handleReparentNodes(bridge, args)
1252
1554
  );
1253
1555
 
1254
1556
  // figma_move_to_page - Move nodes to a different page
@@ -1261,7 +1563,7 @@ export function registerTools(server, bridge) {
1261
1563
  x: z.number().optional().describe('X position on the target page'),
1262
1564
  y: z.number().optional().describe('Y position on the target page')
1263
1565
  },
1264
- async (args) => handleMoveToPage(bridge, args)
1566
+ async args => handleMoveToPage(bridge, args)
1265
1567
  );
1266
1568
 
1267
1569
  // ============================================================
@@ -1276,7 +1578,7 @@ export function registerTools(server, bridge) {
1276
1578
  instanceId: z.string().describe('The instance node ID to swap'),
1277
1579
  newComponentId: z.string().describe('The component ID to swap to')
1278
1580
  },
1279
- async (args) => handleSwapInstance(bridge, args)
1581
+ async args => handleSwapInstance(bridge, args)
1280
1582
  );
1281
1583
 
1282
1584
  // ============================================================
@@ -1291,26 +1593,32 @@ export function registerTools(server, bridge) {
1291
1593
  pageId: z.string().describe('The page ID to duplicate'),
1292
1594
  name: z.string().optional().describe('Name for the new page (defaults to "original name + copy")')
1293
1595
  },
1294
- async (args) => handleDuplicatePage(bridge, args)
1596
+ async args => handleDuplicatePage(bridge, args)
1295
1597
  );
1296
1598
 
1297
1599
  // figma_set_rotation - Set rotation on nodes
1298
1600
  server.tool(
1299
1601
  'figma_set_rotation',
1300
1602
  'Set the rotation (in degrees) of one or more nodes. pivot defaults to "center", which keeps the node\'s visual ' +
1301
- 'centre in place by writing relativeTransform. pivot: "top-left" is Figma\'s raw node.rotation behavior, which ' +
1302
- 'rotates about the top-left corner and therefore MOVES the visual centre. ' +
1303
- 'Limitation: an auto-layout parent computes its children\'s positions and ignores the translation part of ' +
1304
- 'relativeTransform, so a centre pivot is impossible on a non-ABSOLUTE auto-layout child — those nodes get a ' +
1305
- 'top-left rotation plus an explicit warning naming the parent (set layoutPositioning: ABSOLUTE, or wrap the node ' +
1306
- 'in a plain frame, to get a true centre pivot). Each node echoes appliedPivot, its resulting rotation, and its ' +
1307
- 'absoluteBoundingBox so the pivot can be verified.',
1603
+ 'centre in place by writing relativeTransform. pivot: "top-left" is Figma\'s raw node.rotation behavior, which ' +
1604
+ 'rotates about the top-left corner and therefore MOVES the visual centre. ' +
1605
+ "Limitation: an auto-layout parent computes its children's positions and ignores the translation part of " +
1606
+ 'relativeTransform, so a centre pivot is impossible on a non-ABSOLUTE auto-layout child — those nodes get a ' +
1607
+ 'top-left rotation plus an explicit warning naming the parent (set layoutPositioning: ABSOLUTE, or wrap the node ' +
1608
+ 'in a plain frame, to get a true centre pivot). Each node echoes appliedPivot, its resulting rotation, and its ' +
1609
+ 'absoluteBoundingBox so the pivot can be verified.',
1308
1610
  {
1309
1611
  nodeIds: z.array(z.string()).describe('Array of node IDs to rotate'),
1310
1612
  rotation: z.number().min(-180).max(180).describe('Rotation in degrees (-180 to 180)'),
1311
- pivot: z.enum(['center', 'top-left']).optional().default('center').describe('Point to rotate about. "center" (default) preserves the node\'s visual centre; "top-left" is Figma\'s raw node.rotation behavior.')
1613
+ pivot: z
1614
+ .enum(['center', 'top-left'])
1615
+ .optional()
1616
+ .default('center')
1617
+ .describe(
1618
+ 'Point to rotate about. "center" (default) preserves the node\'s visual centre; "top-left" is Figma\'s raw node.rotation behavior.'
1619
+ )
1312
1620
  },
1313
- async (args) => handleSetRotation(bridge, args)
1621
+ async args => handleSetRotation(bridge, args)
1314
1622
  );
1315
1623
 
1316
1624
  // figma_combine_as_variants - Combine components into a component set
@@ -1320,7 +1628,7 @@ export function registerTools(server, bridge) {
1320
1628
  {
1321
1629
  componentIds: z.array(z.string()).min(2).describe('Array of component IDs to combine (minimum 2)')
1322
1630
  },
1323
- async (args) => handleCombineAsVariants(bridge, args)
1631
+ async args => handleCombineAsVariants(bridge, args)
1324
1632
  );
1325
1633
 
1326
1634
  // DISABLED - Uncomment to enable layout grids
@@ -1368,7 +1676,7 @@ export function registerTools(server, bridge) {
1368
1676
  isWideWidth: z.boolean().optional().describe('Use the wide rectangular sticky variant'),
1369
1677
  parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
1370
1678
  },
1371
- async (args) => handleCreateSticky(bridge, args)
1679
+ async args => handleCreateSticky(bridge, args)
1372
1680
  );
1373
1681
 
1374
1682
  // figma_set_sticky - Update a sticky's metadata
@@ -1379,7 +1687,7 @@ export function registerTools(server, bridge) {
1379
1687
  nodeId: z.string().describe('The STICKY node ID'),
1380
1688
  isWideWidth: z.boolean().optional().describe('Wide vs square sticky')
1381
1689
  },
1382
- async (args) => handleSetSticky(bridge, args)
1690
+ async args => handleSetSticky(bridge, args)
1383
1691
  );
1384
1692
 
1385
1693
  // figma_create_shape_with_text - Create a flowchart shape with embedded text
@@ -1391,14 +1699,18 @@ export function registerTools(server, bridge) {
1391
1699
  y: z.number().optional().default(0).describe('Y position'),
1392
1700
  width: z.number().optional().default(208).describe('Width in pixels'),
1393
1701
  height: z.number().optional().default(208).describe('Height in pixels'),
1394
- shapeType: z.enum(SHAPE_TYPES).describe('Shape variety: SQUARE, ELLIPSE, ROUNDED_RECTANGLE, DIAMOND, TRIANGLE_UP/DOWN, PARALLELOGRAM_RIGHT/LEFT, ENG_DATABASE, ENG_QUEUE, ENG_FILE, ENG_FOLDER, TRAPEZOID, PREDEFINED_PROCESS, SHIELD, DOCUMENT_SINGLE/MULTIPLE, MANUAL_INPUT, HEXAGON, CHEVRON, PENTAGON, OCTAGON, STAR, PLUS, ARROW_LEFT/RIGHT, SUMMING_JUNCTION, OR, SPEECH_BUBBLE, INTERNAL_STORAGE'),
1702
+ shapeType: z
1703
+ .enum(SHAPE_TYPES)
1704
+ .describe(
1705
+ 'Shape variety: SQUARE, ELLIPSE, ROUNDED_RECTANGLE, DIAMOND, TRIANGLE_UP/DOWN, PARALLELOGRAM_RIGHT/LEFT, ENG_DATABASE, ENG_QUEUE, ENG_FILE, ENG_FOLDER, TRAPEZOID, PREDEFINED_PROCESS, SHIELD, DOCUMENT_SINGLE/MULTIPLE, MANUAL_INPUT, HEXAGON, CHEVRON, PENTAGON, OCTAGON, STAR, PLUS, ARROW_LEFT/RIGHT, SUMMING_JUNCTION, OR, SPEECH_BUBBLE, INTERNAL_STORAGE'
1706
+ ),
1395
1707
  text: z.string().optional().describe('Embedded text content (font auto-loaded)'),
1396
1708
  fills: colorSchema.optional().describe('Shape fill color'),
1397
1709
  strokes: colorSchema.optional().describe('Shape stroke color'),
1398
1710
  strokeWeight: z.number().optional().describe('Stroke weight in pixels'),
1399
1711
  parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
1400
1712
  },
1401
- async (args) => handleCreateShapeWithText(bridge, args)
1713
+ async args => handleCreateShapeWithText(bridge, args)
1402
1714
  );
1403
1715
 
1404
1716
  // figma_set_shape_type - Change the shape variant
@@ -1409,7 +1721,7 @@ export function registerTools(server, bridge) {
1409
1721
  nodeId: z.string().describe('The SHAPE_WITH_TEXT node ID'),
1410
1722
  shapeType: z.enum(SHAPE_TYPES).describe('New shape type')
1411
1723
  },
1412
- async (args) => handleSetShapeType(bridge, args)
1724
+ async args => handleSetShapeType(bridge, args)
1413
1725
  );
1414
1726
 
1415
1727
  // figma_create_connector - Create an arrow/connector between nodes
@@ -1417,23 +1729,35 @@ export function registerTools(server, bridge) {
1417
1729
  'figma_create_connector',
1418
1730
  'FigJam only: create a connector (arrow line) between two nodes for flowcharts and diagrams. Endpoints can attach to nodes via magnets (AUTO recommended), to fixed positions on nodes, or be free-floating on the canvas. Default end cap is ARROW_EQUILATERAL so it looks like an arrow without configuration. ELBOWED is best for orthogonal flowcharts; STRAIGHT only supports CENTER/NONE magnets.',
1419
1731
  {
1420
- start: connectorEndpointSchema.optional().describe('Start endpoint: { nodeId, magnet } | { nodeId, position } | { position }'),
1421
- end: connectorEndpointSchema.optional().describe('End endpoint: { nodeId, magnet } | { nodeId, position } | { position }'),
1422
- lineType: z.enum(CONNECTOR_LINE_TYPES).optional().default('ELBOWED').describe('Line routing: ELBOWED (right angles), STRAIGHT, or CURVED'),
1732
+ start: connectorEndpointSchema
1733
+ .optional()
1734
+ .describe('Start endpoint: { nodeId, magnet } | { nodeId, position } | { position }'),
1735
+ end: connectorEndpointSchema
1736
+ .optional()
1737
+ .describe('End endpoint: { nodeId, magnet } | { nodeId, position } | { position }'),
1738
+ lineType: z
1739
+ .enum(CONNECTOR_LINE_TYPES)
1740
+ .optional()
1741
+ .default('ELBOWED')
1742
+ .describe('Line routing: ELBOWED (right angles), STRAIGHT, or CURVED'),
1423
1743
  startCap: z.enum(CONNECTOR_STROKE_CAPS).optional().default('NONE').describe('Decoration at start endpoint'),
1424
- endCap: z.enum(CONNECTOR_STROKE_CAPS).optional().default('ARROW_EQUILATERAL').describe('Decoration at end endpoint (default arrow)'),
1744
+ endCap: z
1745
+ .enum(CONNECTOR_STROKE_CAPS)
1746
+ .optional()
1747
+ .default('ARROW_EQUILATERAL')
1748
+ .describe('Decoration at end endpoint (default arrow)'),
1425
1749
  text: z.string().optional().describe('Center label text on the connector'),
1426
1750
  strokes: colorSchema.optional().describe('Line color'),
1427
1751
  strokeWeight: z.number().optional().describe('Line thickness in pixels'),
1428
1752
  parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
1429
1753
  },
1430
- async (args) => handleCreateConnector(bridge, args)
1754
+ async args => handleCreateConnector(bridge, args)
1431
1755
  );
1432
1756
 
1433
1757
  // figma_set_connector - Update an existing connector
1434
1758
  server.tool(
1435
1759
  'figma_set_connector',
1436
- 'FigJam only: modify an existing connector\'s endpoints, line type, end caps, or label.',
1760
+ "FigJam only: modify an existing connector's endpoints, line type, end caps, or label.",
1437
1761
  {
1438
1762
  nodeId: z.string().describe('The CONNECTOR node ID'),
1439
1763
  start: connectorEndpointSchema.optional().describe('Replacement start endpoint'),
@@ -1443,7 +1767,7 @@ export function registerTools(server, bridge) {
1443
1767
  endCap: z.enum(CONNECTOR_STROKE_CAPS).optional().describe('New end decoration'),
1444
1768
  text: z.string().optional().describe('Replacement label text')
1445
1769
  },
1446
- async (args) => handleSetConnector(bridge, args)
1770
+ async args => handleSetConnector(bridge, args)
1447
1771
  );
1448
1772
 
1449
1773
  // figma_create_section - Create a labeled section (works in Figma and FigJam)
@@ -1457,26 +1781,33 @@ export function registerTools(server, bridge) {
1457
1781
  height: z.number().optional().default(400).describe('Height in pixels'),
1458
1782
  name: z.string().optional().describe('Section label'),
1459
1783
  fills: colorSchema.optional().describe('Section background fill'),
1460
- sectionContentsHidden: z.boolean().optional().describe('Visually collapse the section\'s contents'),
1461
- devStatus: z.enum(['READY_FOR_DEV', 'COMPLETED']).optional().describe('Dev Mode handoff status (only valid on sections directly under a page or another section)'),
1784
+ sectionContentsHidden: z.boolean().optional().describe("Visually collapse the section's contents"),
1785
+ devStatus: z
1786
+ .enum(['READY_FOR_DEV', 'COMPLETED'])
1787
+ .optional()
1788
+ .describe('Dev Mode handoff status (only valid on sections directly under a page or another section)'),
1462
1789
  devStatusDescription: z.string().optional().describe('Optional description shown with the dev status'),
1463
1790
  parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
1464
1791
  },
1465
- async (args) => handleCreateSection(bridge, args)
1792
+ async args => handleCreateSection(bridge, args)
1466
1793
  );
1467
1794
 
1468
1795
  // figma_set_section - Update a section
1469
1796
  server.tool(
1470
1797
  'figma_set_section',
1471
- 'Update a section\'s name, dev status, or content visibility. Pass devStatus: null to clear.',
1798
+ "Update a section's name, dev status, or content visibility. Pass devStatus: null to clear.",
1472
1799
  {
1473
1800
  nodeId: z.string().describe('The SECTION node ID'),
1474
1801
  name: z.string().optional().describe('New section label'),
1475
1802
  sectionContentsHidden: z.boolean().optional().describe('Show or hide section contents'),
1476
- devStatus: z.enum(['READY_FOR_DEV', 'COMPLETED']).nullable().optional().describe('Set dev status, or null to clear'),
1803
+ devStatus: z
1804
+ .enum(['READY_FOR_DEV', 'COMPLETED'])
1805
+ .nullable()
1806
+ .optional()
1807
+ .describe('Set dev status, or null to clear'),
1477
1808
  devStatusDescription: z.string().optional().describe('Description shown with dev status')
1478
1809
  },
1479
- async (args) => handleSetSection(bridge, args)
1810
+ async args => handleSetSection(bridge, args)
1480
1811
  );
1481
1812
 
1482
1813
  // figma_create_table - Create a table
@@ -1488,16 +1819,21 @@ export function registerTools(server, bridge) {
1488
1819
  y: z.number().optional().default(0).describe('Y position'),
1489
1820
  numRows: z.number().int().min(1).optional().default(2).describe('Number of rows'),
1490
1821
  numColumns: z.number().int().min(1).optional().default(2).describe('Number of columns'),
1491
- cells: z.array(z.object({
1492
- row: z.number().int().min(0).describe('Row index (0-based)'),
1493
- column: z.number().int().min(0).describe('Column index (0-based)'),
1494
- text: z.string().optional().describe('Cell text content'),
1495
- fills: colorSchema.optional().describe('Cell background fill')
1496
- })).optional().describe('Initial cell content. Cells outside the table bounds are silently ignored.'),
1822
+ cells: z
1823
+ .array(
1824
+ z.object({
1825
+ row: z.number().int().min(0).describe('Row index (0-based)'),
1826
+ column: z.number().int().min(0).describe('Column index (0-based)'),
1827
+ text: z.string().optional().describe('Cell text content'),
1828
+ fills: colorSchema.optional().describe('Cell background fill')
1829
+ })
1830
+ )
1831
+ .optional()
1832
+ .describe('Initial cell content. Cells outside the table bounds are silently ignored.'),
1497
1833
  fills: colorSchema.optional().describe('Table background fill'),
1498
1834
  parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
1499
1835
  },
1500
- async (args) => handleCreateTable(bridge, args)
1836
+ async args => handleCreateTable(bridge, args)
1501
1837
  );
1502
1838
 
1503
1839
  // figma_set_table_cell - Set the text/fill of a table cell
@@ -1511,7 +1847,7 @@ export function registerTools(server, bridge) {
1511
1847
  text: z.string().optional().describe('New cell text'),
1512
1848
  fills: colorSchema.optional().describe('New cell background fill')
1513
1849
  },
1514
- async (args) => handleSetTableCell(bridge, args)
1850
+ async args => handleSetTableCell(bridge, args)
1515
1851
  );
1516
1852
 
1517
1853
  // figma_insert_table_row - Insert a row before the given index
@@ -1522,7 +1858,7 @@ export function registerTools(server, bridge) {
1522
1858
  nodeId: z.string().describe('The TABLE node ID'),
1523
1859
  rowIndex: z.number().int().min(0).describe('Insert position (0 = top)')
1524
1860
  },
1525
- async (args) => handleInsertTableRow(bridge, args)
1861
+ async args => handleInsertTableRow(bridge, args)
1526
1862
  );
1527
1863
 
1528
1864
  // figma_insert_table_column - Insert a column before the given index
@@ -1533,7 +1869,7 @@ export function registerTools(server, bridge) {
1533
1869
  nodeId: z.string().describe('The TABLE node ID'),
1534
1870
  columnIndex: z.number().int().min(0).describe('Insert position (0 = leftmost)')
1535
1871
  },
1536
- async (args) => handleInsertTableColumn(bridge, args)
1872
+ async args => handleInsertTableColumn(bridge, args)
1537
1873
  );
1538
1874
 
1539
1875
  // figma_remove_table_row - Remove a row
@@ -1544,7 +1880,7 @@ export function registerTools(server, bridge) {
1544
1880
  nodeId: z.string().describe('The TABLE node ID'),
1545
1881
  rowIndex: z.number().int().min(0).describe('Row to remove')
1546
1882
  },
1547
- async (args) => handleRemoveTableRow(bridge, args)
1883
+ async args => handleRemoveTableRow(bridge, args)
1548
1884
  );
1549
1885
 
1550
1886
  // figma_remove_table_column - Remove a column
@@ -1555,7 +1891,7 @@ export function registerTools(server, bridge) {
1555
1891
  nodeId: z.string().describe('The TABLE node ID'),
1556
1892
  columnIndex: z.number().int().min(0).describe('Column to remove')
1557
1893
  },
1558
- async (args) => handleRemoveTableColumn(bridge, args)
1894
+ async args => handleRemoveTableColumn(bridge, args)
1559
1895
  );
1560
1896
 
1561
1897
  // figma_resize_table_row - Set row height
@@ -1567,7 +1903,7 @@ export function registerTools(server, bridge) {
1567
1903
  rowIndex: z.number().int().min(0).describe('Row index'),
1568
1904
  height: z.number().min(1).describe('New height in pixels')
1569
1905
  },
1570
- async (args) => handleResizeTableRow(bridge, args)
1906
+ async args => handleResizeTableRow(bridge, args)
1571
1907
  );
1572
1908
 
1573
1909
  // figma_resize_table_column - Set column width
@@ -1579,7 +1915,7 @@ export function registerTools(server, bridge) {
1579
1915
  columnIndex: z.number().int().min(0).describe('Column index'),
1580
1916
  width: z.number().min(1).describe('New width in pixels')
1581
1917
  },
1582
- async (args) => handleResizeTableColumn(bridge, args)
1918
+ async args => handleResizeTableColumn(bridge, args)
1583
1919
  );
1584
1920
 
1585
1921
  // figma_move_table_row - Reorder rows
@@ -1591,7 +1927,7 @@ export function registerTools(server, bridge) {
1591
1927
  fromIndex: z.number().int().min(0).describe('Source row index'),
1592
1928
  toIndex: z.number().int().min(0).describe('Destination row index')
1593
1929
  },
1594
- async (args) => handleMoveTableRow(bridge, args)
1930
+ async args => handleMoveTableRow(bridge, args)
1595
1931
  );
1596
1932
 
1597
1933
  // figma_move_table_column - Reorder columns
@@ -1603,7 +1939,7 @@ export function registerTools(server, bridge) {
1603
1939
  fromIndex: z.number().int().min(0).describe('Source column index'),
1604
1940
  toIndex: z.number().int().min(0).describe('Destination column index')
1605
1941
  },
1606
- async (args) => handleMoveTableColumn(bridge, args)
1942
+ async args => handleMoveTableColumn(bridge, args)
1607
1943
  );
1608
1944
 
1609
1945
  // figma_create_code_block - Create a syntax-highlighted code block
@@ -1617,19 +1953,19 @@ export function registerTools(server, bridge) {
1617
1953
  codeLanguage: z.enum(CODE_LANGUAGES).optional().default('PLAINTEXT').describe('Syntax highlighting language'),
1618
1954
  parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
1619
1955
  },
1620
- async (args) => handleCreateCodeBlock(bridge, args)
1956
+ async args => handleCreateCodeBlock(bridge, args)
1621
1957
  );
1622
1958
 
1623
1959
  // figma_set_code_block - Update a code block
1624
1960
  server.tool(
1625
1961
  'figma_set_code_block',
1626
- 'FigJam only: update an existing code block\'s code text or language.',
1962
+ "FigJam only: update an existing code block's code text or language.",
1627
1963
  {
1628
1964
  nodeId: z.string().describe('The CODE_BLOCK node ID'),
1629
1965
  code: z.string().optional().describe('New code text'),
1630
1966
  codeLanguage: z.enum(CODE_LANGUAGES).optional().describe('New syntax-highlighting language')
1631
1967
  },
1632
- async (args) => handleSetCodeBlock(bridge, args)
1968
+ async args => handleSetCodeBlock(bridge, args)
1633
1969
  );
1634
1970
 
1635
1971
  // figma_create_link_preview - Embed a URL (auto-detects iframe vs. card)
@@ -1642,7 +1978,7 @@ export function registerTools(server, bridge) {
1642
1978
  url: z.string().describe('The URL to preview'),
1643
1979
  parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
1644
1980
  },
1645
- async (args) => handleCreateLinkPreview(bridge, args)
1981
+ async args => handleCreateLinkPreview(bridge, args)
1646
1982
  );
1647
1983
 
1648
1984
  // ---- Prototype tools ----
@@ -1654,7 +1990,7 @@ export function registerTools(server, bridge) {
1654
1990
  {
1655
1991
  nodeId: z.string().describe('Node ID to read reactions from')
1656
1992
  },
1657
- async (args) => handleGetReactions(bridge, args)
1993
+ async args => handleGetReactions(bridge, args)
1658
1994
  );
1659
1995
 
1660
1996
  // figma_add_reaction - Add a prototype interaction to a node
@@ -1663,43 +1999,121 @@ export function registerTools(server, bridge) {
1663
1999
  'Prototype (Figma Design only): add a reaction (interaction) to a node. A reaction pairs a trigger with an action. Existing reactions are preserved.\n\nTrigger types: ON_CLICK, ON_HOVER, ON_PRESS, ON_DRAG, ON_MEDIA_END, AFTER_TIMEOUT, MOUSE_UP, MOUSE_DOWN, MOUSE_ENTER, MOUSE_LEAVE, ON_KEY_DOWN, ON_MEDIA_HIT.\n\nAction types: NODE (navigate/overlay/scroll — set navigation field), BACK, CLOSE, URL.\n\nFor NODE actions, navigation values: NAVIGATE (go to frame), SWAP (replace current frame), OVERLAY (open as overlay), SCROLL_TO (scroll to frame), CHANGE_TO (change component variant).',
1664
2000
  {
1665
2001
  nodeId: z.string().describe('Node ID to add the reaction to'),
1666
- trigger: z.object({
1667
- type: z.enum([
1668
- 'ON_CLICK', 'ON_HOVER', 'ON_PRESS', 'ON_DRAG', 'ON_MEDIA_END',
1669
- 'AFTER_TIMEOUT', 'MOUSE_UP', 'MOUSE_DOWN', 'MOUSE_ENTER', 'MOUSE_LEAVE',
1670
- 'ON_KEY_DOWN', 'ON_MEDIA_HIT'
1671
- ]).describe('Trigger type'),
1672
- timeout: z.number().optional().describe('Delay in ms — required for AFTER_TIMEOUT'),
1673
- delay: z.number().optional().describe('Delay in ms — for MOUSE_UP, MOUSE_DOWN, MOUSE_ENTER, MOUSE_LEAVE'),
1674
- device: z.enum(['KEYBOARD', 'XBOX_ONE', 'PS4', 'SWITCH_PRO', 'UNKNOWN_CONTROLLER']).optional().describe('Input device — for ON_KEY_DOWN (default: KEYBOARD)'),
1675
- keyCodes: z.array(z.number()).optional().describe('Key codes — for ON_KEY_DOWN'),
1676
- mediaHitTime: z.number().optional().describe('Time in seconds — for ON_MEDIA_HIT')
1677
- }).describe('What triggers the reaction'),
1678
- action: z.object({
1679
- type: z.enum(['NODE', 'BACK', 'CLOSE', 'URL']).describe(
1680
- 'Action type. NODE covers all navigation (use navigation field to specify NAVIGATE/OVERLAY/SCROLL_TO/SWAP/CHANGE_TO). BACK goes to previous frame. CLOSE closes overlay. URL opens a URL.'
1681
- ),
1682
- destinationId: z.string().optional().describe('Target frame/node ID — for NODE action'),
1683
- url: z.string().optional().describe('URL string — required for URL action'),
1684
- openInNewTab: z.boolean().optional().describe('Open URL in a new tab (default false) — for URL action'),
1685
- navigation: z.enum(['NAVIGATE', 'SWAP', 'OVERLAY', 'SCROLL_TO', 'CHANGE_TO']).optional().describe('Navigation type for NODE action (default: NAVIGATE)'),
1686
- transition: z.object({
1687
- type: z.enum(['DISSOLVE', 'SMART_ANIMATE', 'SCROLL_ANIMATE', 'MOVE_IN', 'MOVE_OUT', 'PUSH', 'SLIDE_IN', 'SLIDE_OUT']).describe('Transition type. DISSOLVE/SMART_ANIMATE/SCROLL_ANIMATE take no direction; MOVE_IN/MOVE_OUT/PUSH/SLIDE_IN/SLIDE_OUT require direction.'),
1688
- direction: z.enum(['LEFT', 'RIGHT', 'TOP', 'BOTTOM']).optional().describe('Direction — required for MOVE_IN, MOVE_OUT, PUSH, SLIDE_IN, SLIDE_OUT'),
1689
- matchLayers: z.boolean().optional().describe('Smart-match shared layers across frames during a directional transition (default false). Only used by directional types.'),
1690
- duration: z.number().optional().describe('Duration in seconds (default 0.3)'),
1691
- easing: z.object({
1692
- type: z.enum(['LINEAR', 'EASE_IN', 'EASE_OUT', 'EASE_IN_AND_OUT', 'EASE_IN_BACK', 'EASE_OUT_BACK', 'EASE_IN_AND_OUT_BACK', 'CUSTOM_CUBIC_BEZIER', 'GENTLE', 'QUICK', 'BOUNCY', 'SLOW', 'CUSTOM_SPRING']).describe('Easing type. GENTLE/QUICK/BOUNCY/SLOW are spring presets.'),
1693
- easingFunctionCubicBezier: z.object({
1694
- x1: z.number(), y1: z.number(), x2: z.number(), y2: z.number()
1695
- }).optional().describe('Cubic bezier control points — required for CUSTOM_CUBIC_BEZIER')
1696
- }).optional().describe('Easing curve (default: LINEAR)')
1697
- }).optional().describe('Transition animation omit for no animation'),
1698
- preserveScrollPosition: z.boolean().optional().describe('Preserve scroll position on navigate (default false)'),
1699
- overlayRelativePosition: z.object({ x: z.number(), y: z.number() }).optional().describe('Overlay position offset for OVERLAY navigation')
1700
- }).describe('What happens when the trigger fires')
1701
- },
1702
- async (args) => handleAddReaction(bridge, args)
2002
+ trigger: z
2003
+ .object({
2004
+ type: z
2005
+ .enum([
2006
+ 'ON_CLICK',
2007
+ 'ON_HOVER',
2008
+ 'ON_PRESS',
2009
+ 'ON_DRAG',
2010
+ 'ON_MEDIA_END',
2011
+ 'AFTER_TIMEOUT',
2012
+ 'MOUSE_UP',
2013
+ 'MOUSE_DOWN',
2014
+ 'MOUSE_ENTER',
2015
+ 'MOUSE_LEAVE',
2016
+ 'ON_KEY_DOWN',
2017
+ 'ON_MEDIA_HIT'
2018
+ ])
2019
+ .describe('Trigger type'),
2020
+ timeout: z.number().optional().describe('Delay in ms required for AFTER_TIMEOUT'),
2021
+ delay: z.number().optional().describe('Delay in ms — for MOUSE_UP, MOUSE_DOWN, MOUSE_ENTER, MOUSE_LEAVE'),
2022
+ device: z
2023
+ .enum(['KEYBOARD', 'XBOX_ONE', 'PS4', 'SWITCH_PRO', 'UNKNOWN_CONTROLLER'])
2024
+ .optional()
2025
+ .describe('Input device for ON_KEY_DOWN (default: KEYBOARD)'),
2026
+ keyCodes: z.array(z.number()).optional().describe('Key codes for ON_KEY_DOWN'),
2027
+ mediaHitTime: z.number().optional().describe('Time in seconds — for ON_MEDIA_HIT')
2028
+ })
2029
+ .describe('What triggers the reaction'),
2030
+ action: z
2031
+ .object({
2032
+ type: z
2033
+ .enum(['NODE', 'BACK', 'CLOSE', 'URL'])
2034
+ .describe(
2035
+ 'Action type. NODE covers all navigation (use navigation field to specify NAVIGATE/OVERLAY/SCROLL_TO/SWAP/CHANGE_TO). BACK goes to previous frame. CLOSE closes overlay. URL opens a URL.'
2036
+ ),
2037
+ destinationId: z.string().optional().describe('Target frame/node ID — for NODE action'),
2038
+ url: z.string().optional().describe('URL string required for URL action'),
2039
+ openInNewTab: z.boolean().optional().describe('Open URL in a new tab (default false) — for URL action'),
2040
+ navigation: z
2041
+ .enum(['NAVIGATE', 'SWAP', 'OVERLAY', 'SCROLL_TO', 'CHANGE_TO'])
2042
+ .optional()
2043
+ .describe('Navigation type for NODE action (default: NAVIGATE)'),
2044
+ transition: z
2045
+ .object({
2046
+ type: z
2047
+ .enum([
2048
+ 'DISSOLVE',
2049
+ 'SMART_ANIMATE',
2050
+ 'SCROLL_ANIMATE',
2051
+ 'MOVE_IN',
2052
+ 'MOVE_OUT',
2053
+ 'PUSH',
2054
+ 'SLIDE_IN',
2055
+ 'SLIDE_OUT'
2056
+ ])
2057
+ .describe(
2058
+ 'Transition type. DISSOLVE/SMART_ANIMATE/SCROLL_ANIMATE take no direction; MOVE_IN/MOVE_OUT/PUSH/SLIDE_IN/SLIDE_OUT require direction.'
2059
+ ),
2060
+ direction: z
2061
+ .enum(['LEFT', 'RIGHT', 'TOP', 'BOTTOM'])
2062
+ .optional()
2063
+ .describe('Direction — required for MOVE_IN, MOVE_OUT, PUSH, SLIDE_IN, SLIDE_OUT'),
2064
+ matchLayers: z
2065
+ .boolean()
2066
+ .optional()
2067
+ .describe(
2068
+ 'Smart-match shared layers across frames during a directional transition (default false). Only used by directional types.'
2069
+ ),
2070
+ duration: z.number().optional().describe('Duration in seconds (default 0.3)'),
2071
+ easing: z
2072
+ .object({
2073
+ type: z
2074
+ .enum([
2075
+ 'LINEAR',
2076
+ 'EASE_IN',
2077
+ 'EASE_OUT',
2078
+ 'EASE_IN_AND_OUT',
2079
+ 'EASE_IN_BACK',
2080
+ 'EASE_OUT_BACK',
2081
+ 'EASE_IN_AND_OUT_BACK',
2082
+ 'CUSTOM_CUBIC_BEZIER',
2083
+ 'GENTLE',
2084
+ 'QUICK',
2085
+ 'BOUNCY',
2086
+ 'SLOW',
2087
+ 'CUSTOM_SPRING'
2088
+ ])
2089
+ .describe('Easing type. GENTLE/QUICK/BOUNCY/SLOW are spring presets.'),
2090
+ easingFunctionCubicBezier: z
2091
+ .object({
2092
+ x1: z.number(),
2093
+ y1: z.number(),
2094
+ x2: z.number(),
2095
+ y2: z.number()
2096
+ })
2097
+ .optional()
2098
+ .describe('Cubic bezier control points — required for CUSTOM_CUBIC_BEZIER')
2099
+ })
2100
+ .optional()
2101
+ .describe('Easing curve (default: LINEAR)')
2102
+ })
2103
+ .optional()
2104
+ .describe('Transition animation — omit for no animation'),
2105
+ preserveScrollPosition: z
2106
+ .boolean()
2107
+ .optional()
2108
+ .describe('Preserve scroll position on navigate (default false)'),
2109
+ overlayRelativePosition: z
2110
+ .object({ x: z.number(), y: z.number() })
2111
+ .optional()
2112
+ .describe('Overlay position offset — for OVERLAY navigation')
2113
+ })
2114
+ .describe('What happens when the trigger fires')
2115
+ },
2116
+ async args => handleAddReaction(bridge, args)
1703
2117
  );
1704
2118
 
1705
2119
  // figma_remove_reaction - Remove a reaction by index
@@ -1710,7 +2124,7 @@ export function registerTools(server, bridge) {
1710
2124
  nodeId: z.string().describe('Node ID to remove the reaction from'),
1711
2125
  index: z.number().int().min(0).describe('Zero-based index of the reaction to remove')
1712
2126
  },
1713
- async (args) => handleRemoveReaction(bridge, args)
2127
+ async args => handleRemoveReaction(bridge, args)
1714
2128
  );
1715
2129
 
1716
2130
  // figma_set_flow_starting_point - Set or clear a prototype flow starting point
@@ -1718,10 +2132,17 @@ export function registerTools(server, bridge) {
1718
2132
  'figma_set_flow_starting_point',
1719
2133
  'Prototype (Figma Design only): set a top-level frame as a prototype flow starting point on the current page, or clear it. Flow starting points are page-level — Figma stores them as { nodeId, name } entries on the page.',
1720
2134
  {
1721
- nodeId: z.string().describe('Frame node ID to set as flow starting point. Must be FRAME, COMPONENT, or COMPONENT_SET.'),
1722
- flowName: z.string().optional().describe('Name for the flow (defaults to "Flow 1" if omitted). If the frame is already a flow starting point, its name is updated.'),
2135
+ nodeId: z
2136
+ .string()
2137
+ .describe('Frame node ID to set as flow starting point. Must be FRAME, COMPONENT, or COMPONENT_SET.'),
2138
+ flowName: z
2139
+ .string()
2140
+ .optional()
2141
+ .describe(
2142
+ 'Name for the flow (defaults to "Flow 1" if omitted). If the frame is already a flow starting point, its name is updated.'
2143
+ ),
1723
2144
  clear: z.boolean().optional().describe('If true, remove the flow starting point for this frame from the page')
1724
2145
  },
1725
- async (args) => handleSetFlowStartingPoint(bridge, args)
2146
+ async args => handleSetFlowStartingPoint(bridge, args)
1726
2147
  );
1727
2148
  }