@autobest-ui/agent 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -10
- package/mcp/figma-mcp-bridge/LICENSE +21 -0
- package/mcp/figma-mcp-bridge/README.md +25 -0
- package/mcp/figma-mcp-bridge/config.toml.example +10 -0
- package/mcp/figma-mcp-bridge/index.test.js +47 -0
- package/mcp/figma-mcp-bridge/package.json +16 -0
- package/mcp/figma-mcp-bridge/skills/figma-bridge/SKILL.md +98 -0
- package/mcp/figma-mcp-bridge/src/index.js +68 -0
- package/mcp/figma-mcp-bridge/src/server.js +159 -0
- package/mcp/figma-mcp-bridge/src/tools/context.js +75 -0
- package/mcp/figma-mcp-bridge/src/tools/index.js +1727 -0
- package/mcp/figma-mcp-bridge/src/tools/mutations.js +4423 -0
- package/mcp/figma-mcp-bridge/src/tools/nodes.js +78 -0
- package/mcp/figma-mcp-bridge/src/tools/pages.js +55 -0
- package/mcp/figma-mcp-bridge/src/websocket.js +255 -0
- package/mcp/rag-mcp-bridge/README.md +24 -12
- package/package.json +9 -4
- package/plugins/figma-plugin/LICENSE +21 -0
- package/plugins/figma-plugin/README.md +25 -0
- package/plugins/figma-plugin/code.js +6608 -0
- package/plugins/figma-plugin/manifest.json +32 -0
- package/plugins/figma-plugin/scripts/setup.mjs +72 -0
- package/plugins/figma-plugin/scripts/setup.test.mjs +45 -0
- package/plugins/figma-plugin/ui.html +235 -0
|
@@ -0,0 +1,1727 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registry - registers all Figma tools with the MCP server
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
import { handleGetContext } from './context.js';
|
|
10
|
+
import { handleListPages } from './pages.js';
|
|
11
|
+
import { handleGetNodes } from './nodes.js';
|
|
12
|
+
import {
|
|
13
|
+
handleSetFills,
|
|
14
|
+
handleSetStrokes,
|
|
15
|
+
handleCreateRectangle,
|
|
16
|
+
handleSetText,
|
|
17
|
+
handleCloneNodes,
|
|
18
|
+
handleDeleteNodes,
|
|
19
|
+
handleMoveNodes,
|
|
20
|
+
handleResizeNodes,
|
|
21
|
+
handleSetOpacity,
|
|
22
|
+
handleSetCornerRadius,
|
|
23
|
+
handleGroupNodes,
|
|
24
|
+
handleUngroupNodes,
|
|
25
|
+
handleCreateFrame,
|
|
26
|
+
handleCreateText,
|
|
27
|
+
handleSetSelection,
|
|
28
|
+
handleSetCurrentPage,
|
|
29
|
+
handleExportNode,
|
|
30
|
+
// Phase 2 commands
|
|
31
|
+
handleCreateEllipse,
|
|
32
|
+
handleSetEffects,
|
|
33
|
+
handleSetAutoLayout,
|
|
34
|
+
handleGetLocalStyles,
|
|
35
|
+
handleApplyStyle,
|
|
36
|
+
handleCreateComponent,
|
|
37
|
+
handleCreateInstance,
|
|
38
|
+
// Phase 3 commands
|
|
39
|
+
handleGetLocalVariables,
|
|
40
|
+
handleSearchVariables,
|
|
41
|
+
handleSetVariable,
|
|
42
|
+
handleCreateLine,
|
|
43
|
+
handleSetConstraints,
|
|
44
|
+
// Phase 4 commands
|
|
45
|
+
handleCreatePolygon,
|
|
46
|
+
handleBooleanOperation,
|
|
47
|
+
handleZoomToNode,
|
|
48
|
+
handleSetBlendMode,
|
|
49
|
+
handleDetachInstance,
|
|
50
|
+
// Phase 5 commands
|
|
51
|
+
handleSetLayoutAlign,
|
|
52
|
+
handleCreateVector,
|
|
53
|
+
handleRenameNode,
|
|
54
|
+
handleReorderNode,
|
|
55
|
+
// Smart Query commands
|
|
56
|
+
handleSearchNodes,
|
|
57
|
+
handleSearchComponents,
|
|
58
|
+
handleSearchStyles,
|
|
59
|
+
handleGetChildren,
|
|
60
|
+
// Design System Creation commands
|
|
61
|
+
handleSetTextStyle,
|
|
62
|
+
handleCreatePaintStyle,
|
|
63
|
+
handleCreateTextStyle,
|
|
64
|
+
handleCreateVariableCollection,
|
|
65
|
+
handleCreateVariable,
|
|
66
|
+
handleRenameVariable,
|
|
67
|
+
handleDeleteVariables,
|
|
68
|
+
handleDeleteVariableCollection,
|
|
69
|
+
handleRenameVariableCollection,
|
|
70
|
+
handleRenameMode,
|
|
71
|
+
handleAddMode,
|
|
72
|
+
handleDeleteMode,
|
|
73
|
+
handleUnbindVariable,
|
|
74
|
+
// Page Management commands
|
|
75
|
+
handleCreatePage,
|
|
76
|
+
handleRenamePage,
|
|
77
|
+
handleDeletePage,
|
|
78
|
+
handleReorderPage,
|
|
79
|
+
// Node Structure commands
|
|
80
|
+
handleReparentNodes,
|
|
81
|
+
handleMoveToPage,
|
|
82
|
+
// Instance commands
|
|
83
|
+
handleSwapInstance,
|
|
84
|
+
// Additional commands
|
|
85
|
+
handleDuplicatePage,
|
|
86
|
+
handleSetRotation,
|
|
87
|
+
handleSetLayoutGrids,
|
|
88
|
+
handleCombineAsVariants,
|
|
89
|
+
// FigJam commands
|
|
90
|
+
handleCreateSticky,
|
|
91
|
+
handleSetSticky,
|
|
92
|
+
handleCreateShapeWithText,
|
|
93
|
+
handleSetShapeType,
|
|
94
|
+
handleCreateConnector,
|
|
95
|
+
handleSetConnector,
|
|
96
|
+
handleCreateSection,
|
|
97
|
+
handleSetSection,
|
|
98
|
+
handleCreateTable,
|
|
99
|
+
handleSetTableCell,
|
|
100
|
+
handleInsertTableRow,
|
|
101
|
+
handleInsertTableColumn,
|
|
102
|
+
handleRemoveTableRow,
|
|
103
|
+
handleRemoveTableColumn,
|
|
104
|
+
handleResizeTableRow,
|
|
105
|
+
handleResizeTableColumn,
|
|
106
|
+
handleMoveTableRow,
|
|
107
|
+
handleMoveTableColumn,
|
|
108
|
+
handleCreateCodeBlock,
|
|
109
|
+
handleSetCodeBlock,
|
|
110
|
+
handleCreateLinkPreview,
|
|
111
|
+
// Prototype commands
|
|
112
|
+
handleGetReactions,
|
|
113
|
+
handleAddReaction,
|
|
114
|
+
handleRemoveReaction,
|
|
115
|
+
handleSetFlowStartingPoint,
|
|
116
|
+
// Visibility / clipping / style deletion / variable modes / size limits
|
|
117
|
+
handleSetVisible,
|
|
118
|
+
handleSetClipsContent,
|
|
119
|
+
handleDeleteStyle,
|
|
120
|
+
handleSetVariableMode,
|
|
121
|
+
handleSetSizeLimits
|
|
122
|
+
} from './mutations.js';
|
|
123
|
+
|
|
124
|
+
// Read package.json once at module load — used by figma_server_info to surface the running version
|
|
125
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
126
|
+
let pkgVersion = 'unknown';
|
|
127
|
+
try {
|
|
128
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8'));
|
|
129
|
+
pkgVersion = pkg.version;
|
|
130
|
+
} catch (_) {
|
|
131
|
+
// package.json not found — fall back to 'unknown'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Color schema for fill/stroke shorthand
|
|
135
|
+
const colorSchema = z.union([
|
|
136
|
+
z.object({
|
|
137
|
+
color: z.string().describe('Hex color (e.g., "#FF0000" or "#FF0000FF" with alpha)')
|
|
138
|
+
}),
|
|
139
|
+
z.object({
|
|
140
|
+
r: z.number().min(0).max(1).describe('Red (0-1)'),
|
|
141
|
+
g: z.number().min(0).max(1).describe('Green (0-1)'),
|
|
142
|
+
b: z.number().min(0).max(1).describe('Blue (0-1)'),
|
|
143
|
+
a: z.number().min(0).max(1).optional().describe('Alpha (0-1, optional)')
|
|
144
|
+
}),
|
|
145
|
+
z.array(z.any()).describe('Full Figma fills array')
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
// ============================================================
|
|
149
|
+
// FigJam shared enums
|
|
150
|
+
// ============================================================
|
|
151
|
+
|
|
152
|
+
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'
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const CONNECTOR_LINE_TYPES = ['ELBOWED', 'STRAIGHT', 'CURVED'];
|
|
166
|
+
const CONNECTOR_STROKE_CAPS = [
|
|
167
|
+
'NONE', 'ARROW_EQUILATERAL', 'ARROW_LINES',
|
|
168
|
+
'TRIANGLE_FILLED', 'CIRCLE_FILLED', 'DIAMOND_FILLED'
|
|
169
|
+
];
|
|
170
|
+
const MAGNETS = ['NONE', 'AUTO', 'TOP', 'LEFT', 'BOTTOM', 'RIGHT', 'CENTER'];
|
|
171
|
+
const CODE_LANGUAGES = [
|
|
172
|
+
'TYPESCRIPT', 'CPP', 'RUBY', 'CSS', 'JAVASCRIPT', 'HTML',
|
|
173
|
+
'JSON', 'GRAPHQL', 'PYTHON', 'GO', 'SQL', 'SWIFT',
|
|
174
|
+
'KOTLIN', 'RUST', 'BASH', 'PLAINTEXT', 'DART'
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
// Endpoint spec for figma_create_connector / figma_set_connector.
|
|
178
|
+
// One of: { nodeId, magnet }, { nodeId, position }, { position }.
|
|
179
|
+
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.')
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Register all tools with the MCP server
|
|
190
|
+
* @param {McpServer} server - MCP Server instance
|
|
191
|
+
* @param {FigmaBridge} bridge - Figma bridge instance
|
|
192
|
+
*/
|
|
193
|
+
export function registerTools(server, bridge) {
|
|
194
|
+
// ============================================================
|
|
195
|
+
// Server Info
|
|
196
|
+
// ============================================================
|
|
197
|
+
|
|
198
|
+
// figma_server_info - Get MCP server info including port and version
|
|
199
|
+
server.tool(
|
|
200
|
+
'figma_server_info',
|
|
201
|
+
'Get information about the MCP server: package version, WebSocket port, connection state, and connected document info.',
|
|
202
|
+
{},
|
|
203
|
+
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
|
+
}]
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// ============================================================
|
|
217
|
+
// Query Tools
|
|
218
|
+
// ============================================================
|
|
219
|
+
|
|
220
|
+
// figma_get_context - Get current document context
|
|
221
|
+
server.tool(
|
|
222
|
+
'figma_get_context',
|
|
223
|
+
'Get the current Figma document context including file info, current page, and selection. Use this to understand what document is open and what the user has selected.',
|
|
224
|
+
{},
|
|
225
|
+
async () => handleGetContext(bridge)
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// figma_list_pages - List all pages
|
|
229
|
+
server.tool(
|
|
230
|
+
'figma_list_pages',
|
|
231
|
+
'List all pages in the current Figma document. Returns page IDs, names, and indicates which page is currently active.',
|
|
232
|
+
{},
|
|
233
|
+
async () => handleListPages(bridge)
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// figma_get_nodes - Get node details by ID
|
|
237
|
+
server.tool(
|
|
238
|
+
'figma_get_nodes',
|
|
239
|
+
'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
|
+
{
|
|
241
|
+
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.')
|
|
243
|
+
},
|
|
244
|
+
async (args) => handleGetNodes(bridge, args)
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// ============================================================
|
|
248
|
+
// Mutation Tools
|
|
249
|
+
// ============================================================
|
|
250
|
+
|
|
251
|
+
// figma_set_fills - Set fill colors on a node
|
|
252
|
+
server.tool(
|
|
253
|
+
'figma_set_fills',
|
|
254
|
+
'Set fill color. Accepts hex shorthand or fills array.',
|
|
255
|
+
{
|
|
256
|
+
nodeId: z.string().describe('The node ID to modify'),
|
|
257
|
+
fills: colorSchema.describe('Fill color - use { color: "#RRGGBB" } for simple colors')
|
|
258
|
+
},
|
|
259
|
+
async (args) => handleSetFills(bridge, args)
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// figma_set_strokes - Set stroke colors and weights on a node
|
|
263
|
+
server.tool(
|
|
264
|
+
'figma_set_strokes',
|
|
265
|
+
'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
|
+
{
|
|
267
|
+
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)
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
// figma_create_rectangle - Create a new rectangle
|
|
279
|
+
server.tool(
|
|
280
|
+
'figma_create_rectangle',
|
|
281
|
+
'Create a rectangle.',
|
|
282
|
+
{
|
|
283
|
+
x: z.number().optional().default(0).describe('X position'),
|
|
284
|
+
y: z.number().optional().default(0).describe('Y position'),
|
|
285
|
+
width: z.number().optional().default(100).describe('Width in pixels'),
|
|
286
|
+
height: z.number().optional().default(100).describe('Height in pixels'),
|
|
287
|
+
name: z.string().optional().default('Rectangle').describe('Node name'),
|
|
288
|
+
fills: colorSchema.optional().describe('Fill color'),
|
|
289
|
+
parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
|
|
290
|
+
},
|
|
291
|
+
async (args) => handleCreateRectangle(bridge, args)
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// figma_set_text - Set text content on a text node
|
|
295
|
+
server.tool(
|
|
296
|
+
'figma_set_text',
|
|
297
|
+
'Set text content. Auto-loads fonts.',
|
|
298
|
+
{
|
|
299
|
+
nodeId: z.string().describe('The text node ID to modify'),
|
|
300
|
+
text: z.string().describe('The new text content')
|
|
301
|
+
},
|
|
302
|
+
async (args) => handleSetText(bridge, args)
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// figma_clone_nodes - Clone/duplicate nodes
|
|
306
|
+
server.tool(
|
|
307
|
+
'figma_clone_nodes',
|
|
308
|
+
'Duplicate nodes.',
|
|
309
|
+
{
|
|
310
|
+
nodeIds: z.array(z.string()).describe('Array of node IDs to clone'),
|
|
311
|
+
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')
|
|
316
|
+
},
|
|
317
|
+
async (args) => handleCloneNodes(bridge, args)
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
// ============================================================
|
|
321
|
+
// Node Manipulation Tools
|
|
322
|
+
// ============================================================
|
|
323
|
+
|
|
324
|
+
// figma_delete_nodes - Delete nodes
|
|
325
|
+
server.tool(
|
|
326
|
+
'figma_delete_nodes',
|
|
327
|
+
'Delete nodes.',
|
|
328
|
+
{
|
|
329
|
+
nodeIds: z.array(z.string()).describe('Array of node IDs to delete')
|
|
330
|
+
},
|
|
331
|
+
async (args) => handleDeleteNodes(bridge, args)
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// figma_move_nodes - Move nodes
|
|
335
|
+
server.tool(
|
|
336
|
+
'figma_move_nodes',
|
|
337
|
+
'Move nodes. Use relative=true for offset.',
|
|
338
|
+
{
|
|
339
|
+
nodeIds: z.array(z.string()).describe('Array of node IDs to move'),
|
|
340
|
+
x: z.number().optional().describe('X position (absolute) or offset (if relative=true)'),
|
|
341
|
+
y: z.number().optional().describe('Y position (absolute) or offset (if relative=true)'),
|
|
342
|
+
relative: z.boolean().optional().default(false).describe('If true, x/y are offsets from current position')
|
|
343
|
+
},
|
|
344
|
+
async (args) => handleMoveNodes(bridge, args)
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// figma_resize_nodes - Resize nodes
|
|
348
|
+
server.tool(
|
|
349
|
+
'figma_resize_nodes',
|
|
350
|
+
'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.'),
|
|
361
|
+
width: z.number().optional().describe('New width in pixels'),
|
|
362
|
+
height: z.number().optional().describe('New height in pixels')
|
|
363
|
+
},
|
|
364
|
+
async (args) => handleResizeNodes(bridge, args)
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// figma_set_size_limits - Set or clear min/max width/height
|
|
368
|
+
server.tool(
|
|
369
|
+
'figma_set_size_limits',
|
|
370
|
+
'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.',
|
|
377
|
+
{
|
|
378
|
+
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)
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
// figma_set_opacity - Set node opacity
|
|
388
|
+
server.tool(
|
|
389
|
+
'figma_set_opacity',
|
|
390
|
+
'Set opacity (0-1).',
|
|
391
|
+
{
|
|
392
|
+
nodeId: z.string().describe('The node ID to modify'),
|
|
393
|
+
opacity: z.number().min(0).max(1).describe('Opacity value from 0 (transparent) to 1 (opaque)')
|
|
394
|
+
},
|
|
395
|
+
async (args) => handleSetOpacity(bridge, args)
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
// figma_set_visible - Show or hide nodes
|
|
399
|
+
server.tool(
|
|
400
|
+
'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.',
|
|
402
|
+
{
|
|
403
|
+
nodeIds: z.array(z.string()).describe('Array of node IDs to show or hide'),
|
|
404
|
+
visible: z.boolean().describe('true to show, false to hide')
|
|
405
|
+
},
|
|
406
|
+
async (args) => handleSetVisible(bridge, args)
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
// figma_set_clips_content - Toggle content clipping on frame-like nodes
|
|
410
|
+
server.tool(
|
|
411
|
+
'figma_set_clips_content',
|
|
412
|
+
'Set whether frame-like nodes clip their children to the frame bounds. Works on FRAME, COMPONENT, COMPONENT_SET, INSTANCE, SLOT and SLIDE — other node types return an error. Response echoes the resulting clipsContent. Read it back with figma_get_nodes.',
|
|
413
|
+
{
|
|
414
|
+
nodeIds: z.array(z.string()).describe('Array of node IDs to modify'),
|
|
415
|
+
clipsContent: z.boolean().describe('true to clip children to the frame bounds, false to let them overflow')
|
|
416
|
+
},
|
|
417
|
+
async (args) => handleSetClipsContent(bridge, args)
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
// figma_set_corner_radius - Set corner radius
|
|
421
|
+
server.tool(
|
|
422
|
+
'figma_set_corner_radius',
|
|
423
|
+
'Set corner radius. Use individual values for asymmetric.',
|
|
424
|
+
{
|
|
425
|
+
nodeId: z.string().describe('The node ID to modify'),
|
|
426
|
+
radius: z.number().optional().describe('Uniform corner radius for all corners'),
|
|
427
|
+
topLeft: z.number().optional().describe('Top-left corner radius'),
|
|
428
|
+
topRight: z.number().optional().describe('Top-right corner radius'),
|
|
429
|
+
bottomLeft: z.number().optional().describe('Bottom-left corner radius'),
|
|
430
|
+
bottomRight: z.number().optional().describe('Bottom-right corner radius')
|
|
431
|
+
},
|
|
432
|
+
async (args) => handleSetCornerRadius(bridge, args)
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// figma_group_nodes - Group nodes
|
|
436
|
+
server.tool(
|
|
437
|
+
'figma_group_nodes',
|
|
438
|
+
'Group nodes.',
|
|
439
|
+
{
|
|
440
|
+
nodeIds: z.array(z.string()).describe('Array of node IDs to group together'),
|
|
441
|
+
name: z.string().optional().default('Group').describe('Name for the new group')
|
|
442
|
+
},
|
|
443
|
+
async (args) => handleGroupNodes(bridge, args)
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
// figma_ungroup_nodes - Ungroup nodes
|
|
447
|
+
server.tool(
|
|
448
|
+
'figma_ungroup_nodes',
|
|
449
|
+
'Ungroup nodes.',
|
|
450
|
+
{
|
|
451
|
+
nodeIds: z.array(z.string()).describe('Array of group node IDs to ungroup')
|
|
452
|
+
},
|
|
453
|
+
async (args) => handleUngroupNodes(bridge, args)
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
// ============================================================
|
|
457
|
+
// Creation Tools
|
|
458
|
+
// ============================================================
|
|
459
|
+
|
|
460
|
+
// figma_create_frame - Create a new frame
|
|
461
|
+
server.tool(
|
|
462
|
+
'figma_create_frame',
|
|
463
|
+
'Create a frame.',
|
|
464
|
+
{
|
|
465
|
+
x: z.number().optional().default(0).describe('X position'),
|
|
466
|
+
y: z.number().optional().default(0).describe('Y position'),
|
|
467
|
+
width: z.number().optional().default(100).describe('Width in pixels'),
|
|
468
|
+
height: z.number().optional().default(100).describe('Height in pixels'),
|
|
469
|
+
name: z.string().optional().default('Frame').describe('Frame name'),
|
|
470
|
+
fills: colorSchema.optional().describe('Fill color'),
|
|
471
|
+
parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
|
|
472
|
+
},
|
|
473
|
+
async (args) => handleCreateFrame(bridge, args)
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
// figma_create_text - Create a new text node
|
|
477
|
+
server.tool(
|
|
478
|
+
'figma_create_text',
|
|
479
|
+
'Create a text node.',
|
|
480
|
+
{
|
|
481
|
+
x: z.number().optional().default(0).describe('X position'),
|
|
482
|
+
y: z.number().optional().default(0).describe('Y position'),
|
|
483
|
+
text: z.string().optional().default('Text').describe('The text content'),
|
|
484
|
+
fontSize: z.number().optional().default(16).describe('Font size in pixels'),
|
|
485
|
+
fontFamily: z.string().optional().default('Inter').describe('Font family name'),
|
|
486
|
+
fontStyle: z.string().optional().default('Regular').describe('Font style (Regular, Bold, etc.)'),
|
|
487
|
+
fills: colorSchema.optional().describe('Text color'),
|
|
488
|
+
name: z.string().optional().default('Text').describe('Node name'),
|
|
489
|
+
parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
|
|
490
|
+
},
|
|
491
|
+
async (args) => handleCreateText(bridge, args)
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
// ============================================================
|
|
495
|
+
// Navigation Tools
|
|
496
|
+
// ============================================================
|
|
497
|
+
|
|
498
|
+
// figma_set_selection - Set the current selection
|
|
499
|
+
server.tool(
|
|
500
|
+
'figma_set_selection',
|
|
501
|
+
'Set selection. Empty array clears.',
|
|
502
|
+
{
|
|
503
|
+
nodeIds: z.array(z.string()).describe('Array of node IDs to select (empty array to clear)')
|
|
504
|
+
},
|
|
505
|
+
async (args) => handleSetSelection(bridge, args)
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
// figma_set_current_page - Switch to a different page
|
|
509
|
+
server.tool(
|
|
510
|
+
'figma_set_current_page',
|
|
511
|
+
'Switch to a different page in the Figma document.',
|
|
512
|
+
{
|
|
513
|
+
pageId: z.string().describe('The page ID to switch to')
|
|
514
|
+
},
|
|
515
|
+
async (args) => handleSetCurrentPage(bridge, args)
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
// ============================================================
|
|
519
|
+
// Export Tools
|
|
520
|
+
// ============================================================
|
|
521
|
+
|
|
522
|
+
// figma_export_node - Export a node as an image
|
|
523
|
+
server.tool(
|
|
524
|
+
'figma_export_node',
|
|
525
|
+
'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.',
|
|
531
|
+
{
|
|
532
|
+
nodeId: z.string().describe('The node ID to export'),
|
|
533
|
+
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.')
|
|
537
|
+
},
|
|
538
|
+
async (args) => handleExportNode(bridge, args)
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
// ============================================================
|
|
542
|
+
// Phase 2 Tools
|
|
543
|
+
// ============================================================
|
|
544
|
+
|
|
545
|
+
// figma_create_ellipse - Create an ellipse/circle
|
|
546
|
+
server.tool(
|
|
547
|
+
'figma_create_ellipse',
|
|
548
|
+
'Create ellipse. Use arcData for arcs/rings.',
|
|
549
|
+
{
|
|
550
|
+
x: z.number().optional().default(0).describe('X position'),
|
|
551
|
+
y: z.number().optional().default(0).describe('Y position'),
|
|
552
|
+
width: z.number().optional().default(100).describe('Width in pixels (diameter for circle)'),
|
|
553
|
+
height: z.number().optional().default(100).describe('Height in pixels (same as width for circle)'),
|
|
554
|
+
name: z.string().optional().default('Ellipse').describe('Node name'),
|
|
555
|
+
fills: colorSchema.optional().describe('Fill color'),
|
|
556
|
+
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')
|
|
562
|
+
},
|
|
563
|
+
async (args) => handleCreateEllipse(bridge, args)
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
// figma_set_effects - Set effects (shadows, blurs)
|
|
567
|
+
server.tool(
|
|
568
|
+
'figma_set_effects',
|
|
569
|
+
'Set effects. Replaces existing.',
|
|
570
|
+
{
|
|
571
|
+
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)
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
// figma_set_auto_layout - Configure auto-layout
|
|
596
|
+
server.tool(
|
|
597
|
+
'figma_set_auto_layout',
|
|
598
|
+
'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.',
|
|
602
|
+
{
|
|
603
|
+
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'),
|
|
609
|
+
paddingTop: z.number().min(0).optional().describe('Top padding in pixels'),
|
|
610
|
+
paddingRight: z.number().min(0).optional().describe('Right padding in pixels'),
|
|
611
|
+
paddingBottom: z.number().min(0).optional().describe('Bottom padding in pixels'),
|
|
612
|
+
paddingLeft: z.number().min(0).optional().describe('Left padding in pixels'),
|
|
613
|
+
itemSpacing: z.number().min(0).optional().describe('Space between items in pixels'),
|
|
614
|
+
counterAxisSpacing: z.number().min(0).optional().describe('Space between rows when wrapped'),
|
|
615
|
+
layoutWrap: z.enum(['NO_WRAP', 'WRAP']).optional().describe('Whether to wrap items to new rows/columns')
|
|
616
|
+
},
|
|
617
|
+
async (args) => handleSetAutoLayout(bridge, args)
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
// figma_get_local_styles - List local styles
|
|
621
|
+
server.tool(
|
|
622
|
+
'figma_get_local_styles',
|
|
623
|
+
'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
|
+
{
|
|
625
|
+
type: z.enum(['PAINT', 'TEXT', 'EFFECT', 'GRID', 'ALL']).optional().default('ALL').describe('Filter by style type')
|
|
626
|
+
},
|
|
627
|
+
async (args) => handleGetLocalStyles(bridge, args)
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
// figma_apply_style - Apply a style to a node
|
|
631
|
+
server.tool(
|
|
632
|
+
'figma_apply_style',
|
|
633
|
+
'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.',
|
|
638
|
+
{
|
|
639
|
+
nodeId: z.string().describe('The node ID to apply the style to'),
|
|
640
|
+
styleId: z.string().describe('The style ID to apply'),
|
|
641
|
+
property: z.enum(['fills', 'strokes', 'text', 'effects', 'grid']).describe('Which property to apply the style to')
|
|
642
|
+
},
|
|
643
|
+
async (args) => handleApplyStyle(bridge, args)
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
// figma_create_component - Create a component
|
|
647
|
+
server.tool(
|
|
648
|
+
'figma_create_component',
|
|
649
|
+
'Create a component.',
|
|
650
|
+
{
|
|
651
|
+
fromNodeId: z.string().optional().describe('Convert an existing node to a component'),
|
|
652
|
+
x: z.number().optional().default(0).describe('X position'),
|
|
653
|
+
y: z.number().optional().default(0).describe('Y position'),
|
|
654
|
+
width: z.number().optional().default(100).describe('Width in pixels'),
|
|
655
|
+
height: z.number().optional().default(100).describe('Height in pixels'),
|
|
656
|
+
name: z.string().optional().default('Component').describe('Component name'),
|
|
657
|
+
fills: colorSchema.optional().describe('Fill color'),
|
|
658
|
+
parentId: z.string().optional().describe('Parent node ID (defaults to current page)'),
|
|
659
|
+
description: z.string().optional().describe('Component description')
|
|
660
|
+
},
|
|
661
|
+
async (args) => handleCreateComponent(bridge, args)
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
// figma_create_instance - Create an instance of a component
|
|
665
|
+
server.tool(
|
|
666
|
+
'figma_create_instance',
|
|
667
|
+
'Create a component instance.',
|
|
668
|
+
{
|
|
669
|
+
componentId: z.string().describe('The component ID to create an instance of'),
|
|
670
|
+
x: z.number().optional().default(0).describe('X position'),
|
|
671
|
+
y: z.number().optional().default(0).describe('Y position'),
|
|
672
|
+
parentId: z.string().optional().describe('Parent node ID (defaults to current page)'),
|
|
673
|
+
name: z.string().optional().describe('Instance name (defaults to component name)')
|
|
674
|
+
},
|
|
675
|
+
async (args) => handleCreateInstance(bridge, args)
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
// ============================================================
|
|
679
|
+
// Phase 3 Tools: Variables, Lines, Constraints
|
|
680
|
+
// ============================================================
|
|
681
|
+
|
|
682
|
+
// figma_get_local_variables - Get local variables
|
|
683
|
+
// WARNING: Can return 25k+ tokens for large design systems. Prefer figma_search_variables when possible.
|
|
684
|
+
server.tool(
|
|
685
|
+
'figma_get_local_variables',
|
|
686
|
+
'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
|
+
{
|
|
688
|
+
type: z.enum(['COLOR', 'FLOAT', 'STRING', 'BOOLEAN', 'ALL']).optional().default('ALL').describe('Filter by variable type')
|
|
689
|
+
},
|
|
690
|
+
async (args) => handleGetLocalVariables(bridge, args)
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
// figma_search_variables - Search variables with filtering (optimized for reduced token usage)
|
|
694
|
+
// PREFERRED: Use this instead of figma_get_local_variables for ~50x token reduction
|
|
695
|
+
server.tool(
|
|
696
|
+
'figma_search_variables',
|
|
697
|
+
'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
|
+
{
|
|
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'),
|
|
702
|
+
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.'),
|
|
704
|
+
limit: z.number().optional().default(50).describe('Maximum number of variables to return')
|
|
705
|
+
},
|
|
706
|
+
async (args) => handleSearchVariables(bridge, args)
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
// ============================================================
|
|
710
|
+
// Smart Query Tools (token-efficient search)
|
|
711
|
+
// ============================================================
|
|
712
|
+
|
|
713
|
+
// figma_search_nodes - Search nodes by name within a scope
|
|
714
|
+
server.tool(
|
|
715
|
+
'figma_search_nodes',
|
|
716
|
+
'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
|
+
{
|
|
718
|
+
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"'),
|
|
720
|
+
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)'),
|
|
724
|
+
limit: z.number().optional().default(50).describe('Maximum number of results')
|
|
725
|
+
},
|
|
726
|
+
async (args) => handleSearchNodes(bridge, args)
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
// figma_search_components - Search local components by name
|
|
730
|
+
server.tool(
|
|
731
|
+
'figma_search_components',
|
|
732
|
+
'Search local components by name. Use when looking for specific components like "Button", "Header", etc. Returns compact results with component metadata.',
|
|
733
|
+
{
|
|
734
|
+
nameContains: z.string().optional().describe('Case-insensitive substring match'),
|
|
735
|
+
namePattern: z.string().optional().describe('Glob pattern with wildcards'),
|
|
736
|
+
includeVariants: z.boolean().optional().default(false).describe('Include individual variants from component sets'),
|
|
737
|
+
compact: z.boolean().optional().default(true).describe('Return minimal data'),
|
|
738
|
+
limit: z.number().optional().default(50).describe('Maximum number of results')
|
|
739
|
+
},
|
|
740
|
+
async (args) => handleSearchComponents(bridge, args)
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
// figma_search_styles - Search local styles by name
|
|
744
|
+
server.tool(
|
|
745
|
+
'figma_search_styles',
|
|
746
|
+
'Search local styles by name. More efficient than figma_get_local_styles when looking for specific styles.',
|
|
747
|
+
{
|
|
748
|
+
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'),
|
|
750
|
+
compact: z.boolean().optional().default(true).describe('Return minimal data'),
|
|
751
|
+
limit: z.number().optional().default(50).describe('Maximum number of results')
|
|
752
|
+
},
|
|
753
|
+
async (args) => handleSearchStyles(bridge, args)
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
// figma_get_children - Get immediate children of a node
|
|
757
|
+
server.tool(
|
|
758
|
+
'figma_get_children',
|
|
759
|
+
'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
|
+
{
|
|
761
|
+
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.')
|
|
763
|
+
},
|
|
764
|
+
async (args) => handleGetChildren(bridge, args)
|
|
765
|
+
);
|
|
766
|
+
|
|
767
|
+
// figma_set_variable - Set variable value or bind to node
|
|
768
|
+
server.tool(
|
|
769
|
+
'figma_set_variable',
|
|
770
|
+
'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).',
|
|
776
|
+
{
|
|
777
|
+
variableId: z.string().describe('The variable ID to set or bind'),
|
|
778
|
+
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')
|
|
794
|
+
},
|
|
795
|
+
async (args) => handleSetVariable(bridge, args)
|
|
796
|
+
);
|
|
797
|
+
|
|
798
|
+
// figma_create_line - Create a line
|
|
799
|
+
server.tool(
|
|
800
|
+
'figma_create_line',
|
|
801
|
+
'Create a line.',
|
|
802
|
+
{
|
|
803
|
+
x: z.number().optional().default(0).describe('X position'),
|
|
804
|
+
y: z.number().optional().default(0).describe('Y position'),
|
|
805
|
+
length: z.number().optional().default(100).describe('Line length in pixels'),
|
|
806
|
+
rotation: z.number().optional().default(0).describe('Line rotation in degrees (0 = horizontal)'),
|
|
807
|
+
name: z.string().optional().default('Line').describe('Node name'),
|
|
808
|
+
strokeWeight: z.number().optional().default(1).describe('Stroke weight in pixels'),
|
|
809
|
+
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)'),
|
|
811
|
+
parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
|
|
812
|
+
},
|
|
813
|
+
async (args) => handleCreateLine(bridge, args)
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
// figma_set_constraints - Set resize constraints
|
|
817
|
+
server.tool(
|
|
818
|
+
'figma_set_constraints',
|
|
819
|
+
'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
|
+
{
|
|
821
|
+
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)')
|
|
824
|
+
},
|
|
825
|
+
async (args) => handleSetConstraints(bridge, args)
|
|
826
|
+
);
|
|
827
|
+
|
|
828
|
+
// ============================================================
|
|
829
|
+
// Phase 4 Tools: Polygons, Boolean Operations, Viewport, Blend Mode, Detach
|
|
830
|
+
// ============================================================
|
|
831
|
+
|
|
832
|
+
// DISABLED - Uncomment to enable advanced shape tools
|
|
833
|
+
// // figma_create_polygon - Create a polygon or star
|
|
834
|
+
// server.tool(
|
|
835
|
+
// 'figma_create_polygon',
|
|
836
|
+
// 'Create a polygon (triangle, pentagon, hexagon, etc.) or star shape. Set innerRadius (0-1) to create a star with spiky points.',
|
|
837
|
+
// {
|
|
838
|
+
// x: z.number().optional().default(0).describe('X position'),
|
|
839
|
+
// y: z.number().optional().default(0).describe('Y position'),
|
|
840
|
+
// width: z.number().optional().default(100).describe('Width in pixels'),
|
|
841
|
+
// height: z.number().optional().default(100).describe('Height in pixels'),
|
|
842
|
+
// pointCount: z.number().min(3).optional().default(5).describe('Number of sides (polygon) or points (star). Minimum 3.'),
|
|
843
|
+
// innerRadius: z.number().min(0).max(1).optional().describe('Inner radius ratio for stars (0-1). 0 = very spiky, 1 = polygon. Omit for regular polygon.'),
|
|
844
|
+
// name: z.string().optional().describe('Node name'),
|
|
845
|
+
// fills: colorSchema.optional().describe('Fill color'),
|
|
846
|
+
// strokes: colorSchema.optional().describe('Stroke color'),
|
|
847
|
+
// strokeWeight: z.number().optional().describe('Stroke weight in pixels'),
|
|
848
|
+
// cornerRadius: z.number().optional().describe('Corner radius for vertices'),
|
|
849
|
+
// parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
|
|
850
|
+
// },
|
|
851
|
+
// async (args) => handleCreatePolygon(bridge, args)
|
|
852
|
+
// );
|
|
853
|
+
|
|
854
|
+
// // figma_boolean_operation - Perform boolean operations on shapes
|
|
855
|
+
// server.tool(
|
|
856
|
+
// 'figma_boolean_operation',
|
|
857
|
+
// 'Combine multiple shapes using boolean operations (union, subtract, intersect, exclude) or flatten them into a single vector.',
|
|
858
|
+
// {
|
|
859
|
+
// operation: z.enum(['UNION', 'SUBTRACT', 'INTERSECT', 'EXCLUDE', 'FLATTEN']).describe('Boolean operation type: UNION (combine), SUBTRACT (cut), INTERSECT (overlap only), EXCLUDE (non-overlap only), FLATTEN (destructive vector)'),
|
|
860
|
+
// nodeIds: z.array(z.string()).min(2).describe('Array of node IDs to combine (minimum 2 nodes)'),
|
|
861
|
+
// name: z.string().optional().describe('Name for the resulting node')
|
|
862
|
+
// },
|
|
863
|
+
// async (args) => handleBooleanOperation(bridge, args)
|
|
864
|
+
// );
|
|
865
|
+
|
|
866
|
+
// // figma_zoom_to_node - Zoom viewport to focus on specific nodes
|
|
867
|
+
// server.tool(
|
|
868
|
+
// 'figma_zoom_to_node',
|
|
869
|
+
// 'Scroll and zoom the Figma viewport to focus on specific nodes. Automatically calculates zoom level to fit all specified nodes.',
|
|
870
|
+
// {
|
|
871
|
+
// nodeIds: z.array(z.string()).min(1).describe('Array of node IDs to zoom to')
|
|
872
|
+
// },
|
|
873
|
+
// async (args) => handleZoomToNode(bridge, args)
|
|
874
|
+
// );
|
|
875
|
+
|
|
876
|
+
// // figma_set_blend_mode - Set blend mode on a node
|
|
877
|
+
// server.tool(
|
|
878
|
+
// 'figma_set_blend_mode',
|
|
879
|
+
// 'Set the blend mode (layer blending) of a node. Controls how the node visually blends with layers below it.',
|
|
880
|
+
// {
|
|
881
|
+
// nodeId: z.string().describe('The node ID to modify'),
|
|
882
|
+
// blendMode: z.enum([
|
|
883
|
+
// 'PASS_THROUGH', 'NORMAL', 'DARKEN', 'MULTIPLY', 'LINEAR_BURN', 'COLOR_BURN',
|
|
884
|
+
// 'LIGHTEN', 'SCREEN', 'LINEAR_DODGE', 'COLOR_DODGE', 'OVERLAY', 'SOFT_LIGHT',
|
|
885
|
+
// 'HARD_LIGHT', 'DIFFERENCE', 'EXCLUSION', 'HUE', 'SATURATION', 'COLOR', 'LUMINOSITY'
|
|
886
|
+
// ]).describe('Blend mode: NORMAL (default), MULTIPLY (darken), SCREEN (lighten), OVERLAY (contrast), etc.')
|
|
887
|
+
// },
|
|
888
|
+
// async (args) => handleSetBlendMode(bridge, args)
|
|
889
|
+
// );
|
|
890
|
+
|
|
891
|
+
// figma_detach_instance - Detach instance from component
|
|
892
|
+
server.tool(
|
|
893
|
+
'figma_detach_instance',
|
|
894
|
+
'Detach a component instance, converting it to a regular frame. Preserves overrides but severs the link to the main component.',
|
|
895
|
+
{
|
|
896
|
+
nodeId: z.string().describe('The instance node ID to detach')
|
|
897
|
+
},
|
|
898
|
+
async (args) => handleDetachInstance(bridge, args)
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
// ============================================================
|
|
902
|
+
// Phase 5 Tools: Layout Align, Vector, Rename, Reorder
|
|
903
|
+
// ============================================================
|
|
904
|
+
|
|
905
|
+
// figma_set_layout_align - Set layout alignment for auto-layout children
|
|
906
|
+
server.tool(
|
|
907
|
+
'figma_set_layout_align',
|
|
908
|
+
'Set how a child behaves within an auto-layout frame. Controls individual alignment (STRETCH), growth (fill container), and absolute positioning.',
|
|
909
|
+
{
|
|
910
|
+
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)
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
// DISABLED - Uncomment to enable custom vector paths
|
|
919
|
+
// // figma_create_vector - Create a custom vector path
|
|
920
|
+
// server.tool(
|
|
921
|
+
// 'figma_create_vector',
|
|
922
|
+
// 'Create a custom vector shape using SVG-style path data. Supports M (move), L (line), Q (quadratic curve), C (cubic bezier), Z (close).',
|
|
923
|
+
// {
|
|
924
|
+
// x: z.number().optional().default(0).describe('X position'),
|
|
925
|
+
// y: z.number().optional().default(0).describe('Y position'),
|
|
926
|
+
// data: z.string().describe('SVG path string (e.g., "M 0 100 L 100 100 L 50 0 Z" for triangle)'),
|
|
927
|
+
// windingRule: z.enum(['NONZERO', 'EVENODD', 'NONE']).optional().default('NONZERO').describe('Fill rule: NONZERO (solid), EVENODD (holes), NONE (outline only)'),
|
|
928
|
+
// name: z.string().optional().default('Vector').describe('Node name'),
|
|
929
|
+
// fills: colorSchema.optional().describe('Fill color'),
|
|
930
|
+
// strokes: colorSchema.optional().describe('Stroke color'),
|
|
931
|
+
// strokeWeight: z.number().optional().describe('Stroke weight in pixels'),
|
|
932
|
+
// parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
|
|
933
|
+
// },
|
|
934
|
+
// async (args) => handleCreateVector(bridge, args)
|
|
935
|
+
// );
|
|
936
|
+
|
|
937
|
+
// figma_rename_node - Rename nodes
|
|
938
|
+
server.tool(
|
|
939
|
+
'figma_rename_node',
|
|
940
|
+
'Rename one or more nodes. For batch renaming, all nodes get the same name.',
|
|
941
|
+
{
|
|
942
|
+
nodeId: z.string().optional().describe('Single node ID to rename'),
|
|
943
|
+
nodeIds: z.array(z.string()).optional().describe('Array of node IDs to rename (batch)'),
|
|
944
|
+
name: z.string().describe('The new name for the node(s)')
|
|
945
|
+
},
|
|
946
|
+
async (args) => handleRenameNode(bridge, args)
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
// figma_reorder_node - Change z-order of a node
|
|
950
|
+
server.tool(
|
|
951
|
+
'figma_reorder_node',
|
|
952
|
+
'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.',
|
|
958
|
+
{
|
|
959
|
+
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)')
|
|
965
|
+
},
|
|
966
|
+
async (args) => handleReorderNode(bridge, args)
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
// ============================================================
|
|
970
|
+
// Design System Creation Tools
|
|
971
|
+
// ============================================================
|
|
972
|
+
|
|
973
|
+
// figma_set_text_style - Set font properties on existing text
|
|
974
|
+
server.tool(
|
|
975
|
+
'figma_set_text_style',
|
|
976
|
+
'Set text font properties.',
|
|
977
|
+
{
|
|
978
|
+
nodeId: z.string().describe('Text node ID'),
|
|
979
|
+
fontSize: z.number().optional().describe('Font size in pixels'),
|
|
980
|
+
fontFamily: z.string().optional().describe('Font family (e.g., "Inter")'),
|
|
981
|
+
fontStyle: z.string().optional().describe('Font style (e.g., "Bold", "Regular")'),
|
|
982
|
+
textCase: z.enum(['ORIGINAL', 'UPPER', 'LOWER', 'TITLE']).optional().describe('Text case transformation'),
|
|
983
|
+
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'),
|
|
994
|
+
textAlignVertical: z.enum(['TOP', 'CENTER', 'BOTTOM']).optional().describe('Vertical text alignment')
|
|
995
|
+
},
|
|
996
|
+
async (args) => handleSetTextStyle(bridge, args)
|
|
997
|
+
);
|
|
998
|
+
|
|
999
|
+
// figma_create_paint_style - Create a local paint style
|
|
1000
|
+
server.tool(
|
|
1001
|
+
'figma_create_paint_style',
|
|
1002
|
+
'Create a paint style.',
|
|
1003
|
+
{
|
|
1004
|
+
name: z.string().describe('Style name (use "/" for folders, e.g., "Brand/Primary")'),
|
|
1005
|
+
fills: colorSchema.describe('Fill color - use { color: "#RRGGBB" } for simple colors'),
|
|
1006
|
+
description: z.string().optional().describe('Style description')
|
|
1007
|
+
},
|
|
1008
|
+
async (args) => handleCreatePaintStyle(bridge, args)
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1011
|
+
// figma_create_text_style - Create a local text style
|
|
1012
|
+
server.tool(
|
|
1013
|
+
'figma_create_text_style',
|
|
1014
|
+
'Create a text style.',
|
|
1015
|
+
{
|
|
1016
|
+
name: z.string().describe('Style name (use "/" for folders)'),
|
|
1017
|
+
fontFamily: z.string().optional().default('Inter').describe('Font family'),
|
|
1018
|
+
fontStyle: z.string().optional().default('Regular').describe('Font style (Regular, Bold, etc.)'),
|
|
1019
|
+
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'),
|
|
1029
|
+
textCase: z.enum(['ORIGINAL', 'UPPER', 'LOWER', 'TITLE']).optional().describe('Text case'),
|
|
1030
|
+
textDecoration: z.enum(['NONE', 'UNDERLINE', 'STRIKETHROUGH']).optional().describe('Text decoration'),
|
|
1031
|
+
description: z.string().optional().describe('Style description')
|
|
1032
|
+
},
|
|
1033
|
+
async (args) => handleCreateTextStyle(bridge, args)
|
|
1034
|
+
);
|
|
1035
|
+
|
|
1036
|
+
// figma_delete_style - Delete a local style
|
|
1037
|
+
server.tool(
|
|
1038
|
+
'figma_delete_style',
|
|
1039
|
+
'Delete a local style (paint, text, effect or grid) from the document. Only local styles can be deleted — styles from a subscribed library return a REMOTE_STYLE error. Use with caution: nodes using the style keep their resolved values but lose the link. Find style IDs with figma_search_styles.',
|
|
1040
|
+
{
|
|
1041
|
+
styleId: z.string().describe('The style ID to delete (e.g., "S:abc123...")')
|
|
1042
|
+
},
|
|
1043
|
+
async (args) => handleDeleteStyle(bridge, args)
|
|
1044
|
+
);
|
|
1045
|
+
|
|
1046
|
+
// figma_create_variable_collection - Create a variable collection
|
|
1047
|
+
server.tool(
|
|
1048
|
+
'figma_create_variable_collection',
|
|
1049
|
+
'Create a new variable collection to organize variables.',
|
|
1050
|
+
{
|
|
1051
|
+
name: z.string().describe('Collection name'),
|
|
1052
|
+
modes: z.array(z.string()).optional().describe('Mode names (defaults to ["Mode 1"])')
|
|
1053
|
+
},
|
|
1054
|
+
async (args) => handleCreateVariableCollection(bridge, args)
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1057
|
+
// figma_create_variable - Create a variable
|
|
1058
|
+
server.tool(
|
|
1059
|
+
'figma_create_variable',
|
|
1060
|
+
'Create a new variable in a collection.',
|
|
1061
|
+
{
|
|
1062
|
+
collectionId: z.string().describe('Variable collection ID'),
|
|
1063
|
+
name: z.string().describe('Variable name (use "/" for groups, e.g., "colors/primary")'),
|
|
1064
|
+
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'),
|
|
1072
|
+
aliasOf: z.string().optional().describe('Variable ID to alias (instead of direct value)'),
|
|
1073
|
+
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)
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
// figma_rename_variable - Rename an existing variable
|
|
1086
|
+
server.tool(
|
|
1087
|
+
'figma_rename_variable',
|
|
1088
|
+
'Rename an existing variable. Use "/" in the name to organize into groups (e.g., "font weight/heading/h1").',
|
|
1089
|
+
{
|
|
1090
|
+
variableId: z.string().describe('The variable ID to rename'),
|
|
1091
|
+
name: z.string().describe('The new name for the variable (use "/" for groups)')
|
|
1092
|
+
},
|
|
1093
|
+
async (args) => handleRenameVariable(bridge, args)
|
|
1094
|
+
);
|
|
1095
|
+
|
|
1096
|
+
// figma_delete_variables - Delete one or more variables
|
|
1097
|
+
server.tool(
|
|
1098
|
+
'figma_delete_variables',
|
|
1099
|
+
'Delete one or more variables from the document. Use with caution - this cannot be undone.',
|
|
1100
|
+
{
|
|
1101
|
+
variableIds: z.array(z.string()).describe('Array of variable IDs to delete')
|
|
1102
|
+
},
|
|
1103
|
+
async (args) => handleDeleteVariables(bridge, args)
|
|
1104
|
+
);
|
|
1105
|
+
|
|
1106
|
+
// figma_delete_variable_collection - Delete a variable collection
|
|
1107
|
+
server.tool(
|
|
1108
|
+
'figma_delete_variable_collection',
|
|
1109
|
+
'Delete a variable collection and all its variables. Use with caution - this cannot be undone.',
|
|
1110
|
+
{
|
|
1111
|
+
collectionId: z.string().describe('The collection ID to delete')
|
|
1112
|
+
},
|
|
1113
|
+
async (args) => handleDeleteVariableCollection(bridge, args)
|
|
1114
|
+
);
|
|
1115
|
+
|
|
1116
|
+
// figma_rename_variable_collection - Rename a variable collection
|
|
1117
|
+
server.tool(
|
|
1118
|
+
'figma_rename_variable_collection',
|
|
1119
|
+
'Rename a variable collection.',
|
|
1120
|
+
{
|
|
1121
|
+
collectionId: z.string().describe('The collection ID to rename'),
|
|
1122
|
+
name: z.string().describe('The new name for the collection')
|
|
1123
|
+
},
|
|
1124
|
+
async (args) => handleRenameVariableCollection(bridge, args)
|
|
1125
|
+
);
|
|
1126
|
+
|
|
1127
|
+
// figma_rename_mode - Rename a mode in a collection
|
|
1128
|
+
server.tool(
|
|
1129
|
+
'figma_rename_mode',
|
|
1130
|
+
'Rename a mode in a variable collection (e.g., "Mode 1" to "dark").',
|
|
1131
|
+
{
|
|
1132
|
+
collectionId: z.string().describe('The collection ID containing the mode'),
|
|
1133
|
+
modeId: z.string().describe('The mode ID to rename'),
|
|
1134
|
+
name: z.string().describe('The new name for the mode')
|
|
1135
|
+
},
|
|
1136
|
+
async (args) => handleRenameMode(bridge, args)
|
|
1137
|
+
);
|
|
1138
|
+
|
|
1139
|
+
// figma_add_mode - Add a mode to a collection
|
|
1140
|
+
server.tool(
|
|
1141
|
+
'figma_add_mode',
|
|
1142
|
+
'Add a new mode to a variable collection.',
|
|
1143
|
+
{
|
|
1144
|
+
collectionId: z.string().describe('The collection ID to add mode to'),
|
|
1145
|
+
name: z.string().describe('Name for the new mode')
|
|
1146
|
+
},
|
|
1147
|
+
async (args) => handleAddMode(bridge, args)
|
|
1148
|
+
);
|
|
1149
|
+
|
|
1150
|
+
// figma_delete_mode - Delete a mode from a collection
|
|
1151
|
+
server.tool(
|
|
1152
|
+
'figma_delete_mode',
|
|
1153
|
+
'Delete a mode from a variable collection. Cannot delete the last mode.',
|
|
1154
|
+
{
|
|
1155
|
+
collectionId: z.string().describe('The collection ID containing the mode'),
|
|
1156
|
+
modeId: z.string().describe('The mode ID to delete')
|
|
1157
|
+
},
|
|
1158
|
+
async (args) => handleDeleteMode(bridge, args)
|
|
1159
|
+
);
|
|
1160
|
+
|
|
1161
|
+
// figma_set_variable_mode - Pin or unpin an explicit variable mode on nodes/pages
|
|
1162
|
+
server.tool(
|
|
1163
|
+
'figma_set_variable_mode',
|
|
1164
|
+
'Pin an explicit variable mode on nodes or pages, or clear an existing pin. This is how a preview/page frame is made to resolve a particular mode (e.g. a mobile frame pinned to the Spacing collection\'s "mobile" mode) — no more cloning a frame just to inherit its mode. Pass clear: true to unpin, which fixes a bad pin inherited through a clone or component master. Works on scene nodes AND page IDs. The response echoes each node\'s resulting explicitVariableModes map so the change is verifiable in the same call ({} means nothing is pinned).',
|
|
1165
|
+
{
|
|
1166
|
+
nodeIds: z.array(z.string()).describe('Array of node IDs or page IDs to pin/unpin'),
|
|
1167
|
+
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')
|
|
1170
|
+
},
|
|
1171
|
+
async (args) => handleSetVariableMode(bridge, args)
|
|
1172
|
+
);
|
|
1173
|
+
|
|
1174
|
+
// figma_unbind_variable - Remove variable binding from a node
|
|
1175
|
+
server.tool(
|
|
1176
|
+
'figma_unbind_variable',
|
|
1177
|
+
'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.',
|
|
1182
|
+
{
|
|
1183
|
+
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.)'),
|
|
1185
|
+
paintIndex: z.number().optional().default(0).describe('Paint array index for fills/strokes')
|
|
1186
|
+
},
|
|
1187
|
+
async (args) => handleUnbindVariable(bridge, args)
|
|
1188
|
+
);
|
|
1189
|
+
|
|
1190
|
+
// ============================================================
|
|
1191
|
+
// Page Management Tools
|
|
1192
|
+
// ============================================================
|
|
1193
|
+
|
|
1194
|
+
// figma_create_page - Create a new page (Figma Design only)
|
|
1195
|
+
server.tool(
|
|
1196
|
+
'figma_create_page',
|
|
1197
|
+
'Create a new page in the document. **Figma Design only — not available in FigJam.** Calling this in a FigJam file returns a FIGMA_DESIGN_ONLY error. FigJam files have pages but the plugin API does not expose figma.createPage(); pages must be created via the FigJam UI.',
|
|
1198
|
+
{
|
|
1199
|
+
name: z.string().describe('Name for the new page'),
|
|
1200
|
+
index: z.number().optional().describe('Position in the page list (0 = first). Defaults to end.')
|
|
1201
|
+
},
|
|
1202
|
+
async (args) => handleCreatePage(bridge, args)
|
|
1203
|
+
);
|
|
1204
|
+
|
|
1205
|
+
// figma_rename_page - Rename a page
|
|
1206
|
+
server.tool(
|
|
1207
|
+
'figma_rename_page',
|
|
1208
|
+
'Rename an existing page in the Figma document.',
|
|
1209
|
+
{
|
|
1210
|
+
pageId: z.string().describe('The page ID to rename'),
|
|
1211
|
+
name: z.string().describe('The new name for the page')
|
|
1212
|
+
},
|
|
1213
|
+
async (args) => handleRenamePage(bridge, args)
|
|
1214
|
+
);
|
|
1215
|
+
|
|
1216
|
+
// figma_delete_page - Delete a page
|
|
1217
|
+
server.tool(
|
|
1218
|
+
'figma_delete_page',
|
|
1219
|
+
'Delete a page from the Figma document. Cannot delete the last remaining page.',
|
|
1220
|
+
{
|
|
1221
|
+
pageId: z.string().describe('The page ID to delete')
|
|
1222
|
+
},
|
|
1223
|
+
async (args) => handleDeletePage(bridge, args)
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
// DISABLED - Uncomment to enable page reordering
|
|
1227
|
+
// // figma_reorder_page - Reorder a page
|
|
1228
|
+
// server.tool(
|
|
1229
|
+
// 'figma_reorder_page',
|
|
1230
|
+
// 'Change the position of a page in the page list.',
|
|
1231
|
+
// {
|
|
1232
|
+
// pageId: z.string().describe('The page ID to reorder'),
|
|
1233
|
+
// index: z.number().describe('New position in the page list (0 = first)')
|
|
1234
|
+
// },
|
|
1235
|
+
// async (args) => handleReorderPage(bridge, args)
|
|
1236
|
+
// );
|
|
1237
|
+
|
|
1238
|
+
// ============================================================
|
|
1239
|
+
// Node Structure Tools
|
|
1240
|
+
// ============================================================
|
|
1241
|
+
|
|
1242
|
+
// figma_reparent_nodes - Move nodes to a different parent
|
|
1243
|
+
server.tool(
|
|
1244
|
+
'figma_reparent_nodes',
|
|
1245
|
+
'Move nodes to a different parent container. Useful for reorganizing the layer hierarchy.',
|
|
1246
|
+
{
|
|
1247
|
+
nodeIds: z.array(z.string()).describe('Array of node IDs to move'),
|
|
1248
|
+
newParentId: z.string().describe('The new parent node ID (must be a frame, group, or page)'),
|
|
1249
|
+
index: z.number().optional().describe('Position within the new parent (0 = bottom/back). Defaults to top/front.')
|
|
1250
|
+
},
|
|
1251
|
+
async (args) => handleReparentNodes(bridge, args)
|
|
1252
|
+
);
|
|
1253
|
+
|
|
1254
|
+
// figma_move_to_page - Move nodes to a different page
|
|
1255
|
+
server.tool(
|
|
1256
|
+
'figma_move_to_page',
|
|
1257
|
+
'Move nodes from their current page to a different page.',
|
|
1258
|
+
{
|
|
1259
|
+
nodeIds: z.array(z.string()).describe('Array of node IDs to move'),
|
|
1260
|
+
targetPageId: z.string().describe('The destination page ID'),
|
|
1261
|
+
x: z.number().optional().describe('X position on the target page'),
|
|
1262
|
+
y: z.number().optional().describe('Y position on the target page')
|
|
1263
|
+
},
|
|
1264
|
+
async (args) => handleMoveToPage(bridge, args)
|
|
1265
|
+
);
|
|
1266
|
+
|
|
1267
|
+
// ============================================================
|
|
1268
|
+
// Component Instance Tools
|
|
1269
|
+
// ============================================================
|
|
1270
|
+
|
|
1271
|
+
// figma_swap_instance - Swap instance to different component
|
|
1272
|
+
server.tool(
|
|
1273
|
+
'figma_swap_instance',
|
|
1274
|
+
'Swap a component instance to use a different component. Preserves position and size.',
|
|
1275
|
+
{
|
|
1276
|
+
instanceId: z.string().describe('The instance node ID to swap'),
|
|
1277
|
+
newComponentId: z.string().describe('The component ID to swap to')
|
|
1278
|
+
},
|
|
1279
|
+
async (args) => handleSwapInstance(bridge, args)
|
|
1280
|
+
);
|
|
1281
|
+
|
|
1282
|
+
// ============================================================
|
|
1283
|
+
// Additional Tools
|
|
1284
|
+
// ============================================================
|
|
1285
|
+
|
|
1286
|
+
// figma_duplicate_page - Clone an entire page (Figma Design only)
|
|
1287
|
+
server.tool(
|
|
1288
|
+
'figma_duplicate_page',
|
|
1289
|
+
'Clone an entire page including all its contents. The new page is inserted after the original. **Figma Design only — not available in FigJam.** Returns FIGMA_DESIGN_ONLY in FigJam since the underlying figma.createPage() API is not exposed there.',
|
|
1290
|
+
{
|
|
1291
|
+
pageId: z.string().describe('The page ID to duplicate'),
|
|
1292
|
+
name: z.string().optional().describe('Name for the new page (defaults to "original name + copy")')
|
|
1293
|
+
},
|
|
1294
|
+
async (args) => handleDuplicatePage(bridge, args)
|
|
1295
|
+
);
|
|
1296
|
+
|
|
1297
|
+
// figma_set_rotation - Set rotation on nodes
|
|
1298
|
+
server.tool(
|
|
1299
|
+
'figma_set_rotation',
|
|
1300
|
+
'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.',
|
|
1308
|
+
{
|
|
1309
|
+
nodeIds: z.array(z.string()).describe('Array of node IDs to rotate'),
|
|
1310
|
+
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.')
|
|
1312
|
+
},
|
|
1313
|
+
async (args) => handleSetRotation(bridge, args)
|
|
1314
|
+
);
|
|
1315
|
+
|
|
1316
|
+
// figma_combine_as_variants - Combine components into a component set
|
|
1317
|
+
server.tool(
|
|
1318
|
+
'figma_combine_as_variants',
|
|
1319
|
+
'Combine multiple components into a component set with variants. Components must use variant naming (e.g., "Size=Large", "State=Active"). Returns the new component set.',
|
|
1320
|
+
{
|
|
1321
|
+
componentIds: z.array(z.string()).min(2).describe('Array of component IDs to combine (minimum 2)')
|
|
1322
|
+
},
|
|
1323
|
+
async (args) => handleCombineAsVariants(bridge, args)
|
|
1324
|
+
);
|
|
1325
|
+
|
|
1326
|
+
// DISABLED - Uncomment to enable layout grids
|
|
1327
|
+
// // figma_set_layout_grids - Set layout grids on a frame
|
|
1328
|
+
// server.tool(
|
|
1329
|
+
// 'figma_set_layout_grids',
|
|
1330
|
+
// 'Set layout grids on a frame. Grids help with alignment and spacing. Pass an empty array to remove all grids.',
|
|
1331
|
+
// {
|
|
1332
|
+
// nodeId: z.string().describe('The frame node ID to set grids on'),
|
|
1333
|
+
// layoutGrids: z.array(z.object({
|
|
1334
|
+
// pattern: z.enum(['COLUMNS', 'ROWS', 'GRID']).describe('Grid pattern type'),
|
|
1335
|
+
// sectionSize: z.number().optional().describe('Size of each column/row/cell in pixels'),
|
|
1336
|
+
// visible: z.boolean().optional().default(true).describe('Whether grid is visible'),
|
|
1337
|
+
// color: z.object({
|
|
1338
|
+
// r: z.number().min(0).max(1).describe('Red (0-1)'),
|
|
1339
|
+
// g: z.number().min(0).max(1).describe('Green (0-1)'),
|
|
1340
|
+
// b: z.number().min(0).max(1).describe('Blue (0-1)'),
|
|
1341
|
+
// a: z.number().min(0).max(1).optional().default(0.1).describe('Alpha (0-1)')
|
|
1342
|
+
// }).optional().describe('Grid color with alpha'),
|
|
1343
|
+
// alignment: z.enum(['MIN', 'CENTER', 'MAX', 'STRETCH']).optional().describe('Column/row alignment (for COLUMNS/ROWS pattern)'),
|
|
1344
|
+
// gutterSize: z.number().optional().describe('Gutter size between columns/rows in pixels'),
|
|
1345
|
+
// offset: z.number().optional().describe('Offset from edge in pixels'),
|
|
1346
|
+
// count: z.number().optional().describe('Number of columns/rows (use large number like 100 for auto)')
|
|
1347
|
+
// })).describe('Array of layout grid configurations')
|
|
1348
|
+
// },
|
|
1349
|
+
// async (args) => handleSetLayoutGrids(bridge, args)
|
|
1350
|
+
// );
|
|
1351
|
+
|
|
1352
|
+
// ============================================================
|
|
1353
|
+
// FigJam Tools
|
|
1354
|
+
// ============================================================
|
|
1355
|
+
// These commands target FigJam-only node types (sticky notes, flowchart shapes,
|
|
1356
|
+
// connectors, tables, code blocks, link previews). Most return WRONG_EDITOR
|
|
1357
|
+
// when called outside a FigJam file. Sections work in both Figma and FigJam.
|
|
1358
|
+
|
|
1359
|
+
// figma_create_sticky - Create a sticky note
|
|
1360
|
+
server.tool(
|
|
1361
|
+
'figma_create_sticky',
|
|
1362
|
+
'FigJam only: create a sticky note. Default size is fixed (240×240); width/height are not configurable. Text is set via the embedded sublayer (font auto-loaded). Note: the author name and visibility are auto-populated by Figma from the active user — they cannot be set programmatically.',
|
|
1363
|
+
{
|
|
1364
|
+
x: z.number().optional().default(0).describe('X position'),
|
|
1365
|
+
y: z.number().optional().default(0).describe('Y position'),
|
|
1366
|
+
text: z.string().optional().describe('Sticky note body text'),
|
|
1367
|
+
fills: colorSchema.optional().describe('Background color of the sticky'),
|
|
1368
|
+
isWideWidth: z.boolean().optional().describe('Use the wide rectangular sticky variant'),
|
|
1369
|
+
parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
|
|
1370
|
+
},
|
|
1371
|
+
async (args) => handleCreateSticky(bridge, args)
|
|
1372
|
+
);
|
|
1373
|
+
|
|
1374
|
+
// figma_set_sticky - Update a sticky's metadata
|
|
1375
|
+
server.tool(
|
|
1376
|
+
'figma_set_sticky',
|
|
1377
|
+
'FigJam only: toggle a sticky note between square (240×240) and wide-rectangle variants. Use figma_set_text to change the body text. (Author name/visibility are read-only at runtime — Figma sets them from the active user.)',
|
|
1378
|
+
{
|
|
1379
|
+
nodeId: z.string().describe('The STICKY node ID'),
|
|
1380
|
+
isWideWidth: z.boolean().optional().describe('Wide vs square sticky')
|
|
1381
|
+
},
|
|
1382
|
+
async (args) => handleSetSticky(bridge, args)
|
|
1383
|
+
);
|
|
1384
|
+
|
|
1385
|
+
// figma_create_shape_with_text - Create a flowchart shape with embedded text
|
|
1386
|
+
server.tool(
|
|
1387
|
+
'figma_create_shape_with_text',
|
|
1388
|
+
'FigJam only: create a flowchart shape with embedded text (process box, decision diamond, database cylinder, etc.). 30 shape types are available — use ROUNDED_RECTANGLE for processes, DIAMOND for decisions, ENG_DATABASE for data stores. cornerRadius is fixed and cannot be set.',
|
|
1389
|
+
{
|
|
1390
|
+
x: z.number().optional().default(0).describe('X position'),
|
|
1391
|
+
y: z.number().optional().default(0).describe('Y position'),
|
|
1392
|
+
width: z.number().optional().default(208).describe('Width in pixels'),
|
|
1393
|
+
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'),
|
|
1395
|
+
text: z.string().optional().describe('Embedded text content (font auto-loaded)'),
|
|
1396
|
+
fills: colorSchema.optional().describe('Shape fill color'),
|
|
1397
|
+
strokes: colorSchema.optional().describe('Shape stroke color'),
|
|
1398
|
+
strokeWeight: z.number().optional().describe('Stroke weight in pixels'),
|
|
1399
|
+
parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
|
|
1400
|
+
},
|
|
1401
|
+
async (args) => handleCreateShapeWithText(bridge, args)
|
|
1402
|
+
);
|
|
1403
|
+
|
|
1404
|
+
// figma_set_shape_type - Change the shape variant
|
|
1405
|
+
server.tool(
|
|
1406
|
+
'figma_set_shape_type',
|
|
1407
|
+
'FigJam only: change the shape type of an existing shape-with-text node (e.g., turn a ROUNDED_RECTANGLE into a DIAMOND).',
|
|
1408
|
+
{
|
|
1409
|
+
nodeId: z.string().describe('The SHAPE_WITH_TEXT node ID'),
|
|
1410
|
+
shapeType: z.enum(SHAPE_TYPES).describe('New shape type')
|
|
1411
|
+
},
|
|
1412
|
+
async (args) => handleSetShapeType(bridge, args)
|
|
1413
|
+
);
|
|
1414
|
+
|
|
1415
|
+
// figma_create_connector - Create an arrow/connector between nodes
|
|
1416
|
+
server.tool(
|
|
1417
|
+
'figma_create_connector',
|
|
1418
|
+
'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
|
+
{
|
|
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'),
|
|
1423
|
+
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)'),
|
|
1425
|
+
text: z.string().optional().describe('Center label text on the connector'),
|
|
1426
|
+
strokes: colorSchema.optional().describe('Line color'),
|
|
1427
|
+
strokeWeight: z.number().optional().describe('Line thickness in pixels'),
|
|
1428
|
+
parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
|
|
1429
|
+
},
|
|
1430
|
+
async (args) => handleCreateConnector(bridge, args)
|
|
1431
|
+
);
|
|
1432
|
+
|
|
1433
|
+
// figma_set_connector - Update an existing connector
|
|
1434
|
+
server.tool(
|
|
1435
|
+
'figma_set_connector',
|
|
1436
|
+
'FigJam only: modify an existing connector\'s endpoints, line type, end caps, or label.',
|
|
1437
|
+
{
|
|
1438
|
+
nodeId: z.string().describe('The CONNECTOR node ID'),
|
|
1439
|
+
start: connectorEndpointSchema.optional().describe('Replacement start endpoint'),
|
|
1440
|
+
end: connectorEndpointSchema.optional().describe('Replacement end endpoint'),
|
|
1441
|
+
lineType: z.enum(CONNECTOR_LINE_TYPES).optional().describe('New line routing type'),
|
|
1442
|
+
startCap: z.enum(CONNECTOR_STROKE_CAPS).optional().describe('New start decoration'),
|
|
1443
|
+
endCap: z.enum(CONNECTOR_STROKE_CAPS).optional().describe('New end decoration'),
|
|
1444
|
+
text: z.string().optional().describe('Replacement label text')
|
|
1445
|
+
},
|
|
1446
|
+
async (args) => handleSetConnector(bridge, args)
|
|
1447
|
+
);
|
|
1448
|
+
|
|
1449
|
+
// figma_create_section - Create a labeled section (works in Figma and FigJam)
|
|
1450
|
+
server.tool(
|
|
1451
|
+
'figma_create_section',
|
|
1452
|
+
'Create a labeled section. Sections work in BOTH Figma design files and FigJam — use them to group flowcharts, diagrams, or design areas. Supports dev status (READY_FOR_DEV/COMPLETED) for handoff.',
|
|
1453
|
+
{
|
|
1454
|
+
x: z.number().optional().default(0).describe('X position'),
|
|
1455
|
+
y: z.number().optional().default(0).describe('Y position'),
|
|
1456
|
+
width: z.number().optional().default(600).describe('Width in pixels'),
|
|
1457
|
+
height: z.number().optional().default(400).describe('Height in pixels'),
|
|
1458
|
+
name: z.string().optional().describe('Section label'),
|
|
1459
|
+
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)'),
|
|
1462
|
+
devStatusDescription: z.string().optional().describe('Optional description shown with the dev status'),
|
|
1463
|
+
parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
|
|
1464
|
+
},
|
|
1465
|
+
async (args) => handleCreateSection(bridge, args)
|
|
1466
|
+
);
|
|
1467
|
+
|
|
1468
|
+
// figma_set_section - Update a section
|
|
1469
|
+
server.tool(
|
|
1470
|
+
'figma_set_section',
|
|
1471
|
+
'Update a section\'s name, dev status, or content visibility. Pass devStatus: null to clear.',
|
|
1472
|
+
{
|
|
1473
|
+
nodeId: z.string().describe('The SECTION node ID'),
|
|
1474
|
+
name: z.string().optional().describe('New section label'),
|
|
1475
|
+
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'),
|
|
1477
|
+
devStatusDescription: z.string().optional().describe('Description shown with dev status')
|
|
1478
|
+
},
|
|
1479
|
+
async (args) => handleSetSection(bridge, args)
|
|
1480
|
+
);
|
|
1481
|
+
|
|
1482
|
+
// figma_create_table - Create a table
|
|
1483
|
+
server.tool(
|
|
1484
|
+
'figma_create_table',
|
|
1485
|
+
'FigJam only: create a table for documentation or structured data. Optionally seed initial cell content via the cells array. Defaults to 2×2.',
|
|
1486
|
+
{
|
|
1487
|
+
x: z.number().optional().default(0).describe('X position'),
|
|
1488
|
+
y: z.number().optional().default(0).describe('Y position'),
|
|
1489
|
+
numRows: z.number().int().min(1).optional().default(2).describe('Number of rows'),
|
|
1490
|
+
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.'),
|
|
1497
|
+
fills: colorSchema.optional().describe('Table background fill'),
|
|
1498
|
+
parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
|
|
1499
|
+
},
|
|
1500
|
+
async (args) => handleCreateTable(bridge, args)
|
|
1501
|
+
);
|
|
1502
|
+
|
|
1503
|
+
// figma_set_table_cell - Set the text/fill of a table cell
|
|
1504
|
+
server.tool(
|
|
1505
|
+
'figma_set_table_cell',
|
|
1506
|
+
'FigJam only: set the text and/or fill color of a single table cell.',
|
|
1507
|
+
{
|
|
1508
|
+
nodeId: z.string().describe('The TABLE node ID'),
|
|
1509
|
+
row: z.number().int().min(0).describe('Row index (0-based)'),
|
|
1510
|
+
column: z.number().int().min(0).describe('Column index (0-based)'),
|
|
1511
|
+
text: z.string().optional().describe('New cell text'),
|
|
1512
|
+
fills: colorSchema.optional().describe('New cell background fill')
|
|
1513
|
+
},
|
|
1514
|
+
async (args) => handleSetTableCell(bridge, args)
|
|
1515
|
+
);
|
|
1516
|
+
|
|
1517
|
+
// figma_insert_table_row - Insert a row before the given index
|
|
1518
|
+
server.tool(
|
|
1519
|
+
'figma_insert_table_row',
|
|
1520
|
+
'FigJam only: insert a row at the given index (existing rows at and after the index shift down).',
|
|
1521
|
+
{
|
|
1522
|
+
nodeId: z.string().describe('The TABLE node ID'),
|
|
1523
|
+
rowIndex: z.number().int().min(0).describe('Insert position (0 = top)')
|
|
1524
|
+
},
|
|
1525
|
+
async (args) => handleInsertTableRow(bridge, args)
|
|
1526
|
+
);
|
|
1527
|
+
|
|
1528
|
+
// figma_insert_table_column - Insert a column before the given index
|
|
1529
|
+
server.tool(
|
|
1530
|
+
'figma_insert_table_column',
|
|
1531
|
+
'FigJam only: insert a column at the given index (existing columns at and after the index shift right).',
|
|
1532
|
+
{
|
|
1533
|
+
nodeId: z.string().describe('The TABLE node ID'),
|
|
1534
|
+
columnIndex: z.number().int().min(0).describe('Insert position (0 = leftmost)')
|
|
1535
|
+
},
|
|
1536
|
+
async (args) => handleInsertTableColumn(bridge, args)
|
|
1537
|
+
);
|
|
1538
|
+
|
|
1539
|
+
// figma_remove_table_row - Remove a row
|
|
1540
|
+
server.tool(
|
|
1541
|
+
'figma_remove_table_row',
|
|
1542
|
+
'FigJam only: remove the row at the given index.',
|
|
1543
|
+
{
|
|
1544
|
+
nodeId: z.string().describe('The TABLE node ID'),
|
|
1545
|
+
rowIndex: z.number().int().min(0).describe('Row to remove')
|
|
1546
|
+
},
|
|
1547
|
+
async (args) => handleRemoveTableRow(bridge, args)
|
|
1548
|
+
);
|
|
1549
|
+
|
|
1550
|
+
// figma_remove_table_column - Remove a column
|
|
1551
|
+
server.tool(
|
|
1552
|
+
'figma_remove_table_column',
|
|
1553
|
+
'FigJam only: remove the column at the given index.',
|
|
1554
|
+
{
|
|
1555
|
+
nodeId: z.string().describe('The TABLE node ID'),
|
|
1556
|
+
columnIndex: z.number().int().min(0).describe('Column to remove')
|
|
1557
|
+
},
|
|
1558
|
+
async (args) => handleRemoveTableColumn(bridge, args)
|
|
1559
|
+
);
|
|
1560
|
+
|
|
1561
|
+
// figma_resize_table_row - Set row height
|
|
1562
|
+
server.tool(
|
|
1563
|
+
'figma_resize_table_row',
|
|
1564
|
+
'FigJam only: set the height of a table row.',
|
|
1565
|
+
{
|
|
1566
|
+
nodeId: z.string().describe('The TABLE node ID'),
|
|
1567
|
+
rowIndex: z.number().int().min(0).describe('Row index'),
|
|
1568
|
+
height: z.number().min(1).describe('New height in pixels')
|
|
1569
|
+
},
|
|
1570
|
+
async (args) => handleResizeTableRow(bridge, args)
|
|
1571
|
+
);
|
|
1572
|
+
|
|
1573
|
+
// figma_resize_table_column - Set column width
|
|
1574
|
+
server.tool(
|
|
1575
|
+
'figma_resize_table_column',
|
|
1576
|
+
'FigJam only: set the width of a table column.',
|
|
1577
|
+
{
|
|
1578
|
+
nodeId: z.string().describe('The TABLE node ID'),
|
|
1579
|
+
columnIndex: z.number().int().min(0).describe('Column index'),
|
|
1580
|
+
width: z.number().min(1).describe('New width in pixels')
|
|
1581
|
+
},
|
|
1582
|
+
async (args) => handleResizeTableColumn(bridge, args)
|
|
1583
|
+
);
|
|
1584
|
+
|
|
1585
|
+
// figma_move_table_row - Reorder rows
|
|
1586
|
+
server.tool(
|
|
1587
|
+
'figma_move_table_row',
|
|
1588
|
+
'FigJam only: move a row from one index to another.',
|
|
1589
|
+
{
|
|
1590
|
+
nodeId: z.string().describe('The TABLE node ID'),
|
|
1591
|
+
fromIndex: z.number().int().min(0).describe('Source row index'),
|
|
1592
|
+
toIndex: z.number().int().min(0).describe('Destination row index')
|
|
1593
|
+
},
|
|
1594
|
+
async (args) => handleMoveTableRow(bridge, args)
|
|
1595
|
+
);
|
|
1596
|
+
|
|
1597
|
+
// figma_move_table_column - Reorder columns
|
|
1598
|
+
server.tool(
|
|
1599
|
+
'figma_move_table_column',
|
|
1600
|
+
'FigJam only: move a column from one index to another.',
|
|
1601
|
+
{
|
|
1602
|
+
nodeId: z.string().describe('The TABLE node ID'),
|
|
1603
|
+
fromIndex: z.number().int().min(0).describe('Source column index'),
|
|
1604
|
+
toIndex: z.number().int().min(0).describe('Destination column index')
|
|
1605
|
+
},
|
|
1606
|
+
async (args) => handleMoveTableColumn(bridge, args)
|
|
1607
|
+
);
|
|
1608
|
+
|
|
1609
|
+
// figma_create_code_block - Create a syntax-highlighted code block
|
|
1610
|
+
server.tool(
|
|
1611
|
+
'figma_create_code_block',
|
|
1612
|
+
'FigJam only: create a syntax-highlighted code block for documentation. Code is a plain string property (no font loading required).',
|
|
1613
|
+
{
|
|
1614
|
+
x: z.number().optional().default(0).describe('X position'),
|
|
1615
|
+
y: z.number().optional().default(0).describe('Y position'),
|
|
1616
|
+
code: z.string().describe('The code text content'),
|
|
1617
|
+
codeLanguage: z.enum(CODE_LANGUAGES).optional().default('PLAINTEXT').describe('Syntax highlighting language'),
|
|
1618
|
+
parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
|
|
1619
|
+
},
|
|
1620
|
+
async (args) => handleCreateCodeBlock(bridge, args)
|
|
1621
|
+
);
|
|
1622
|
+
|
|
1623
|
+
// figma_set_code_block - Update a code block
|
|
1624
|
+
server.tool(
|
|
1625
|
+
'figma_set_code_block',
|
|
1626
|
+
'FigJam only: update an existing code block\'s code text or language.',
|
|
1627
|
+
{
|
|
1628
|
+
nodeId: z.string().describe('The CODE_BLOCK node ID'),
|
|
1629
|
+
code: z.string().optional().describe('New code text'),
|
|
1630
|
+
codeLanguage: z.enum(CODE_LANGUAGES).optional().describe('New syntax-highlighting language')
|
|
1631
|
+
},
|
|
1632
|
+
async (args) => handleSetCodeBlock(bridge, args)
|
|
1633
|
+
);
|
|
1634
|
+
|
|
1635
|
+
// figma_create_link_preview - Embed a URL (auto-detects iframe vs. card)
|
|
1636
|
+
server.tool(
|
|
1637
|
+
'figma_create_link_preview',
|
|
1638
|
+
'FigJam only: create a rich link preview from a URL. Returns either an EMBED (iframe; works for OEmbed providers like YouTube/Spotify) or a LINK_UNFURL (rich card from OpenGraph/Twitter Card metadata) — the response includes nodeType so you know which.',
|
|
1639
|
+
{
|
|
1640
|
+
x: z.number().optional().default(0).describe('X position'),
|
|
1641
|
+
y: z.number().optional().default(0).describe('Y position'),
|
|
1642
|
+
url: z.string().describe('The URL to preview'),
|
|
1643
|
+
parentId: z.string().optional().describe('Parent node ID (defaults to current page)')
|
|
1644
|
+
},
|
|
1645
|
+
async (args) => handleCreateLinkPreview(bridge, args)
|
|
1646
|
+
);
|
|
1647
|
+
|
|
1648
|
+
// ---- Prototype tools ----
|
|
1649
|
+
|
|
1650
|
+
// figma_get_reactions - Read all reactions on a node
|
|
1651
|
+
server.tool(
|
|
1652
|
+
'figma_get_reactions',
|
|
1653
|
+
'Prototype (Figma Design only): get all reactions (interactions) on a node. Returns the full reactions array with trigger and action details.',
|
|
1654
|
+
{
|
|
1655
|
+
nodeId: z.string().describe('Node ID to read reactions from')
|
|
1656
|
+
},
|
|
1657
|
+
async (args) => handleGetReactions(bridge, args)
|
|
1658
|
+
);
|
|
1659
|
+
|
|
1660
|
+
// figma_add_reaction - Add a prototype interaction to a node
|
|
1661
|
+
server.tool(
|
|
1662
|
+
'figma_add_reaction',
|
|
1663
|
+
'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
|
+
{
|
|
1665
|
+
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)
|
|
1703
|
+
);
|
|
1704
|
+
|
|
1705
|
+
// figma_remove_reaction - Remove a reaction by index
|
|
1706
|
+
server.tool(
|
|
1707
|
+
'figma_remove_reaction',
|
|
1708
|
+
'Prototype (Figma Design only): remove a reaction from a node by its zero-based index in the reactions array. Use figma_get_reactions first to find the index.',
|
|
1709
|
+
{
|
|
1710
|
+
nodeId: z.string().describe('Node ID to remove the reaction from'),
|
|
1711
|
+
index: z.number().int().min(0).describe('Zero-based index of the reaction to remove')
|
|
1712
|
+
},
|
|
1713
|
+
async (args) => handleRemoveReaction(bridge, args)
|
|
1714
|
+
);
|
|
1715
|
+
|
|
1716
|
+
// figma_set_flow_starting_point - Set or clear a prototype flow starting point
|
|
1717
|
+
server.tool(
|
|
1718
|
+
'figma_set_flow_starting_point',
|
|
1719
|
+
'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
|
+
{
|
|
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.'),
|
|
1723
|
+
clear: z.boolean().optional().describe('If true, remove the flow starting point for this frame from the page')
|
|
1724
|
+
},
|
|
1725
|
+
async (args) => handleSetFlowStartingPoint(bridge, args)
|
|
1726
|
+
);
|
|
1727
|
+
}
|