@ufira/vibma 0.1.0
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/LICENSE +22 -0
- package/README.md +54 -0
- package/dist/mcp.cjs +1997 -0
- package/dist/mcp.cjs.map +1 -0
- package/dist/mcp.d.cts +1 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +1976 -0
- package/dist/mcp.js.map +1 -0
- package/package.json +54 -0
package/dist/mcp.cjs
ADDED
|
@@ -0,0 +1,1997 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
|
|
28
|
+
// src/utils/color.ts
|
|
29
|
+
var init_color = __esm({
|
|
30
|
+
"src/utils/color.ts"() {
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// src/utils/serialize-node.ts
|
|
35
|
+
var init_serialize_node = __esm({
|
|
36
|
+
"src/utils/serialize-node.ts"() {
|
|
37
|
+
init_color();
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// src/tools/helpers.ts
|
|
42
|
+
var init_helpers = __esm({
|
|
43
|
+
"src/tools/helpers.ts"() {
|
|
44
|
+
init_serialize_node();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// src/mcp.ts
|
|
49
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
50
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
51
|
+
var import_zod19 = require("zod");
|
|
52
|
+
var import_ws = __toESM(require("ws"), 1);
|
|
53
|
+
var import_uuid = require("uuid");
|
|
54
|
+
|
|
55
|
+
// src/tools/document.ts
|
|
56
|
+
var import_zod = require("zod");
|
|
57
|
+
|
|
58
|
+
// src/tools/types.ts
|
|
59
|
+
var MAX_RESPONSE_CHARS = 5e4;
|
|
60
|
+
function mcpJson(data) {
|
|
61
|
+
const text = JSON.stringify(data);
|
|
62
|
+
if (text.length <= MAX_RESPONSE_CHARS) {
|
|
63
|
+
return { content: [{ type: "text", text }] };
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
content: [{
|
|
67
|
+
type: "text",
|
|
68
|
+
text: JSON.stringify({
|
|
69
|
+
_error: "response_too_large",
|
|
70
|
+
_sizeKB: Math.round(text.length / 1024),
|
|
71
|
+
warning: "Response exceeds safe size. Use 'depth', 'fields', 'limit', or 'summaryOnly' parameters to reduce response size."
|
|
72
|
+
})
|
|
73
|
+
}]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function mcpError(prefix, error) {
|
|
77
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
78
|
+
return { content: [{ type: "text", text: `${prefix}: ${msg}` }] };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/tools/document.ts
|
|
82
|
+
function registerMcpTools(server2, sendCommand) {
|
|
83
|
+
server2.tool(
|
|
84
|
+
"get_document_info",
|
|
85
|
+
"Get the document name, current page, and list of all pages.",
|
|
86
|
+
{},
|
|
87
|
+
async () => {
|
|
88
|
+
try {
|
|
89
|
+
return mcpJson(await sendCommand("get_document_info"));
|
|
90
|
+
} catch (e) {
|
|
91
|
+
return mcpError("Error getting document info", e);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
server2.tool(
|
|
96
|
+
"get_current_page",
|
|
97
|
+
"Get the current page info and its top-level children. Always safe \u2014 never touches unloaded pages.",
|
|
98
|
+
{},
|
|
99
|
+
async () => {
|
|
100
|
+
try {
|
|
101
|
+
return mcpJson(await sendCommand("get_current_page"));
|
|
102
|
+
} catch (e) {
|
|
103
|
+
return mcpError("Error getting current page", e);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
server2.tool(
|
|
108
|
+
"get_pages",
|
|
109
|
+
"Get all pages in the document with their IDs, names, and child counts.",
|
|
110
|
+
{},
|
|
111
|
+
async () => {
|
|
112
|
+
try {
|
|
113
|
+
return mcpJson(await sendCommand("get_pages"));
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return mcpError("Error getting pages", e);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
server2.tool(
|
|
120
|
+
"set_current_page",
|
|
121
|
+
"Switch to a different page. Provide either pageId or pageName.",
|
|
122
|
+
{
|
|
123
|
+
pageId: import_zod.z.string().optional().describe("The page ID to switch to"),
|
|
124
|
+
pageName: import_zod.z.string().optional().describe("The page name (case-insensitive, partial match)")
|
|
125
|
+
},
|
|
126
|
+
async (params) => {
|
|
127
|
+
try {
|
|
128
|
+
return mcpJson(await sendCommand("set_current_page", params));
|
|
129
|
+
} catch (e) {
|
|
130
|
+
return mcpError("Error setting current page", e);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
server2.tool(
|
|
135
|
+
"create_page",
|
|
136
|
+
"Create a new page in the document",
|
|
137
|
+
{ name: import_zod.z.string().optional().describe("Name for the new page (default: 'New Page')") },
|
|
138
|
+
async ({ name }) => {
|
|
139
|
+
try {
|
|
140
|
+
return mcpJson(await sendCommand("create_page", { name }));
|
|
141
|
+
} catch (e) {
|
|
142
|
+
return mcpError("Error creating page", e);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
server2.tool(
|
|
147
|
+
"rename_page",
|
|
148
|
+
"Rename a page. Defaults to current page if no pageId given.",
|
|
149
|
+
{
|
|
150
|
+
newName: import_zod.z.string().describe("New name for the page"),
|
|
151
|
+
pageId: import_zod.z.string().optional().describe("Page ID (default: current page)")
|
|
152
|
+
},
|
|
153
|
+
async (params) => {
|
|
154
|
+
try {
|
|
155
|
+
return mcpJson(await sendCommand("rename_page", params));
|
|
156
|
+
} catch (e) {
|
|
157
|
+
return mcpError("Error renaming page", e);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/tools/selection.ts
|
|
164
|
+
var import_zod3 = require("zod");
|
|
165
|
+
|
|
166
|
+
// src/utils/coercion.ts
|
|
167
|
+
var import_zod2 = require("zod");
|
|
168
|
+
var flexBool = (inner) => import_zod2.z.preprocess((v) => {
|
|
169
|
+
if (v === "true" || v === "1") return true;
|
|
170
|
+
if (v === "false" || v === "0") return false;
|
|
171
|
+
return v;
|
|
172
|
+
}, inner);
|
|
173
|
+
var flexJson = (inner) => import_zod2.z.preprocess((v) => {
|
|
174
|
+
if (typeof v === "string") {
|
|
175
|
+
try {
|
|
176
|
+
return JSON.parse(v);
|
|
177
|
+
} catch {
|
|
178
|
+
return v;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return v;
|
|
182
|
+
}, inner);
|
|
183
|
+
var flexNum = (inner) => import_zod2.z.preprocess((v) => {
|
|
184
|
+
if (typeof v === "string") {
|
|
185
|
+
const n = Number(v);
|
|
186
|
+
if (!isNaN(n) && v.trim() !== "") return n;
|
|
187
|
+
}
|
|
188
|
+
return v;
|
|
189
|
+
}, inner);
|
|
190
|
+
|
|
191
|
+
// src/tools/selection.ts
|
|
192
|
+
function registerMcpTools2(server2, sendCommand) {
|
|
193
|
+
server2.tool(
|
|
194
|
+
"get_selection",
|
|
195
|
+
"Get information about the current selection in Figma",
|
|
196
|
+
{},
|
|
197
|
+
async () => {
|
|
198
|
+
try {
|
|
199
|
+
return mcpJson(await sendCommand("get_selection"));
|
|
200
|
+
} catch (e) {
|
|
201
|
+
return mcpError("Error getting selection", e);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
server2.tool(
|
|
206
|
+
"read_my_design",
|
|
207
|
+
"Get detailed information about the current selection, including all node details. Use depth to control traversal.",
|
|
208
|
+
{ depth: import_zod3.z.coerce.number().optional().describe("Levels of children to recurse. 0=selection only, -1 or omit for unlimited.") },
|
|
209
|
+
async ({ depth: depth2 }) => {
|
|
210
|
+
try {
|
|
211
|
+
return mcpJson(await sendCommand("read_my_design", { depth: depth2 }));
|
|
212
|
+
} catch (e) {
|
|
213
|
+
return mcpError("Error reading design", e);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
server2.tool(
|
|
218
|
+
"set_selection",
|
|
219
|
+
"Set selection to nodes and scroll viewport to show them. Also works as focus (single node).",
|
|
220
|
+
{
|
|
221
|
+
nodeIds: flexJson(import_zod3.z.array(import_zod3.z.string())).describe('Array of node IDs to select. Example: ["1:2","1:3"]')
|
|
222
|
+
},
|
|
223
|
+
async ({ nodeIds: nodeIds2 }) => {
|
|
224
|
+
try {
|
|
225
|
+
return mcpJson(await sendCommand("set_selection", { nodeIds: nodeIds2 }));
|
|
226
|
+
} catch (e) {
|
|
227
|
+
return mcpError("Error setting selection", e);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
server2.tool(
|
|
232
|
+
"zoom_into_view",
|
|
233
|
+
"Zoom the viewport to fit specific nodes (like pressing Shift+1)",
|
|
234
|
+
{
|
|
235
|
+
nodeIds: flexJson(import_zod3.z.array(import_zod3.z.string())).describe("Array of node IDs to zoom into")
|
|
236
|
+
},
|
|
237
|
+
async ({ nodeIds: nodeIds2 }) => {
|
|
238
|
+
try {
|
|
239
|
+
return mcpJson(await sendCommand("zoom_into_view", { nodeIds: nodeIds2 }));
|
|
240
|
+
} catch (e) {
|
|
241
|
+
return mcpError("Error zooming", e);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
server2.tool(
|
|
246
|
+
"set_viewport",
|
|
247
|
+
"Set viewport center position and/or zoom level",
|
|
248
|
+
{
|
|
249
|
+
center: flexJson(import_zod3.z.object({ x: import_zod3.z.coerce.number(), y: import_zod3.z.coerce.number() })).optional().describe("Viewport center point. Omit to keep current center."),
|
|
250
|
+
zoom: import_zod3.z.coerce.number().optional().describe("Zoom level (1 = 100%). Omit to keep current zoom.")
|
|
251
|
+
},
|
|
252
|
+
async (params) => {
|
|
253
|
+
try {
|
|
254
|
+
return mcpJson(await sendCommand("set_viewport", params));
|
|
255
|
+
} catch (e) {
|
|
256
|
+
return mcpError("Error setting viewport", e);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/tools/node-info.ts
|
|
263
|
+
var import_zod4 = require("zod");
|
|
264
|
+
init_serialize_node();
|
|
265
|
+
function registerMcpTools3(server2, sendCommand) {
|
|
266
|
+
server2.tool(
|
|
267
|
+
"get_node_info",
|
|
268
|
+
"Get detailed information about one or more nodes. Always pass an array of IDs. Use `fields` to select only the properties you need (reduces context size).",
|
|
269
|
+
{
|
|
270
|
+
nodeIds: flexJson(import_zod4.z.array(import_zod4.z.string())).describe('Array of node IDs. Example: ["1:2","1:3"]'),
|
|
271
|
+
depth: import_zod4.z.coerce.number().optional().describe("Child recursion depth (default: unlimited). 0=stubs only."),
|
|
272
|
+
fields: flexJson(import_zod4.z.array(import_zod4.z.string())).optional().describe('Whitelist of property names to include. Always includes id, name, type. Example: ["absoluteBoundingBox","layoutMode","fills"]. Omit to return all properties.')
|
|
273
|
+
},
|
|
274
|
+
async (params) => {
|
|
275
|
+
try {
|
|
276
|
+
const result = await sendCommand("get_node_info", params);
|
|
277
|
+
return mcpJson(result);
|
|
278
|
+
} catch (e) {
|
|
279
|
+
return mcpError("Error getting node info", e);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
server2.tool(
|
|
284
|
+
"get_node_css",
|
|
285
|
+
"Get CSS properties for a node (useful for dev handoff)",
|
|
286
|
+
{ nodeId: import_zod4.z.string().describe("The node ID to get CSS for") },
|
|
287
|
+
async ({ nodeId: nodeId2 }) => {
|
|
288
|
+
try {
|
|
289
|
+
return mcpJson(await sendCommand("get_node_css", { nodeId: nodeId2 }));
|
|
290
|
+
} catch (e) {
|
|
291
|
+
return mcpError("Error getting CSS", e);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
server2.tool(
|
|
296
|
+
"search_nodes",
|
|
297
|
+
"Search for nodes by layer name and/or type. Searches current page only \u2014 use set_current_page to switch pages first. Matches layer names (text nodes are often auto-named from their content). Returns paginated results.",
|
|
298
|
+
{
|
|
299
|
+
query: import_zod4.z.string().optional().describe("Name search (case-insensitive substring). Omit to match all names."),
|
|
300
|
+
types: flexJson(import_zod4.z.array(import_zod4.z.string())).optional().describe('Filter by types. Example: ["FRAME","TEXT"]. Omit to match all types.'),
|
|
301
|
+
scopeNodeId: import_zod4.z.string().optional().describe("Node ID to search within (defaults to current page)"),
|
|
302
|
+
caseSensitive: flexBool(import_zod4.z.boolean()).optional().describe("Case-sensitive name match (default false)"),
|
|
303
|
+
limit: import_zod4.z.coerce.number().optional().describe("Max results (default 50)"),
|
|
304
|
+
offset: import_zod4.z.coerce.number().optional().describe("Skip N results for pagination (default 0)")
|
|
305
|
+
},
|
|
306
|
+
async (params) => {
|
|
307
|
+
try {
|
|
308
|
+
return mcpJson(await sendCommand("search_nodes", params));
|
|
309
|
+
} catch (e) {
|
|
310
|
+
return mcpError("Error searching nodes", e);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
server2.tool(
|
|
315
|
+
"export_node_as_image",
|
|
316
|
+
"Export a node as an image from Figma",
|
|
317
|
+
{
|
|
318
|
+
nodeId: import_zod4.z.string().describe("The node ID to export"),
|
|
319
|
+
format: import_zod4.z.enum(["PNG", "JPG", "SVG", "PDF"]).optional().describe("Export format (default: PNG)"),
|
|
320
|
+
scale: import_zod4.z.coerce.number().positive().optional().describe("Export scale (default: 1)")
|
|
321
|
+
},
|
|
322
|
+
async ({ nodeId: nodeId2, format, scale }) => {
|
|
323
|
+
try {
|
|
324
|
+
const result = await sendCommand("export_node_as_image", { nodeId: nodeId2, format, scale });
|
|
325
|
+
return {
|
|
326
|
+
content: [{ type: "image", data: result.imageData, mimeType: result.mimeType || "image/png" }]
|
|
327
|
+
};
|
|
328
|
+
} catch (e) {
|
|
329
|
+
return mcpError("Error exporting image", e);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/tools/create-shape.ts
|
|
336
|
+
var import_zod6 = require("zod");
|
|
337
|
+
|
|
338
|
+
// src/tools/schemas.ts
|
|
339
|
+
var import_zod5 = require("zod");
|
|
340
|
+
var nodeId = import_zod5.z.string().describe("Node ID");
|
|
341
|
+
var nodeIds = flexJson(import_zod5.z.array(import_zod5.z.string())).describe("Array of node IDs");
|
|
342
|
+
var parentId = import_zod5.z.string().optional().describe("Parent node ID. Omit to place on current page.");
|
|
343
|
+
var depth = import_zod5.z.coerce.number().optional().describe("Response detail: omit for id+name only. 0=properties + child stubs. N=recurse N levels. -1=unlimited.");
|
|
344
|
+
var xPos = import_zod5.z.coerce.number().optional().describe("X position (default: 0)");
|
|
345
|
+
var yPos = import_zod5.z.coerce.number().optional().describe("Y position (default: 0)");
|
|
346
|
+
function parseHex(hex) {
|
|
347
|
+
const m = hex.match(/^#?([0-9a-f]{3,8})$/i);
|
|
348
|
+
if (!m) return null;
|
|
349
|
+
let h = m[1];
|
|
350
|
+
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
|
351
|
+
if (h.length === 4) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2] + h[3] + h[3];
|
|
352
|
+
if (h.length !== 6 && h.length !== 8) return null;
|
|
353
|
+
const r = parseInt(h.slice(0, 2), 16) / 255;
|
|
354
|
+
const g = parseInt(h.slice(2, 4), 16) / 255;
|
|
355
|
+
const b = parseInt(h.slice(4, 6), 16) / 255;
|
|
356
|
+
if (h.length === 8) return { r, g, b, a: parseInt(h.slice(6, 8), 16) / 255 };
|
|
357
|
+
return { r, g, b };
|
|
358
|
+
}
|
|
359
|
+
var colorRgba = import_zod5.z.preprocess((v) => {
|
|
360
|
+
if (typeof v === "string") return parseHex(v) ?? v;
|
|
361
|
+
return v;
|
|
362
|
+
}, import_zod5.z.object({
|
|
363
|
+
r: import_zod5.z.coerce.number().min(0).max(1),
|
|
364
|
+
g: import_zod5.z.coerce.number().min(0).max(1),
|
|
365
|
+
b: import_zod5.z.coerce.number().min(0).max(1),
|
|
366
|
+
a: import_zod5.z.coerce.number().min(0).max(1).optional()
|
|
367
|
+
}));
|
|
368
|
+
var effectEntry = import_zod5.z.object({
|
|
369
|
+
type: import_zod5.z.enum(["DROP_SHADOW", "INNER_SHADOW", "LAYER_BLUR", "BACKGROUND_BLUR"]),
|
|
370
|
+
color: flexJson(colorRgba).optional(),
|
|
371
|
+
offset: flexJson(import_zod5.z.object({ x: import_zod5.z.coerce.number(), y: import_zod5.z.coerce.number() })).optional(),
|
|
372
|
+
radius: import_zod5.z.coerce.number(),
|
|
373
|
+
spread: import_zod5.z.coerce.number().optional(),
|
|
374
|
+
visible: flexBool(import_zod5.z.boolean()).optional(),
|
|
375
|
+
blendMode: import_zod5.z.string().optional()
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// src/tools/create-shape.ts
|
|
379
|
+
init_helpers();
|
|
380
|
+
var rectItem = import_zod6.z.object({
|
|
381
|
+
name: import_zod6.z.string().optional().describe("Name (default: 'Rectangle')"),
|
|
382
|
+
x: xPos,
|
|
383
|
+
y: yPos,
|
|
384
|
+
width: import_zod6.z.coerce.number().optional().describe("Width (default: 100)"),
|
|
385
|
+
height: import_zod6.z.coerce.number().optional().describe("Height (default: 100)"),
|
|
386
|
+
parentId
|
|
387
|
+
});
|
|
388
|
+
var ellipseItem = import_zod6.z.object({
|
|
389
|
+
name: import_zod6.z.string().optional().describe("Layer name (default: 'Ellipse')"),
|
|
390
|
+
x: xPos,
|
|
391
|
+
y: yPos,
|
|
392
|
+
width: import_zod6.z.coerce.number().optional().describe("Width (default: 100)"),
|
|
393
|
+
height: import_zod6.z.coerce.number().optional().describe("Height (default: 100)"),
|
|
394
|
+
parentId
|
|
395
|
+
});
|
|
396
|
+
var lineItem = import_zod6.z.object({
|
|
397
|
+
name: import_zod6.z.string().optional().describe("Layer name (default: 'Line')"),
|
|
398
|
+
x: xPos,
|
|
399
|
+
y: yPos,
|
|
400
|
+
length: import_zod6.z.coerce.number().optional().describe("Length (default: 100)"),
|
|
401
|
+
rotation: import_zod6.z.coerce.number().optional().describe("Rotation in degrees (default: 0)"),
|
|
402
|
+
parentId
|
|
403
|
+
});
|
|
404
|
+
var sectionItem = import_zod6.z.object({
|
|
405
|
+
name: import_zod6.z.string().optional().describe("Name (default: 'Section')"),
|
|
406
|
+
x: xPos,
|
|
407
|
+
y: yPos,
|
|
408
|
+
width: import_zod6.z.coerce.number().optional().describe("Width (default: 500)"),
|
|
409
|
+
height: import_zod6.z.coerce.number().optional().describe("Height (default: 500)"),
|
|
410
|
+
parentId
|
|
411
|
+
});
|
|
412
|
+
var svgItem = import_zod6.z.object({
|
|
413
|
+
svg: import_zod6.z.string().describe("SVG markup string"),
|
|
414
|
+
name: import_zod6.z.string().optional().describe("Layer name (default: 'SVG')"),
|
|
415
|
+
x: xPos,
|
|
416
|
+
y: yPos,
|
|
417
|
+
parentId
|
|
418
|
+
});
|
|
419
|
+
var boolOpItem = import_zod6.z.object({
|
|
420
|
+
nodeIds: flexJson(import_zod6.z.array(import_zod6.z.string())).describe("Array of node IDs (min 2)"),
|
|
421
|
+
operation: import_zod6.z.enum(["UNION", "INTERSECT", "SUBTRACT", "EXCLUDE"]).describe("Boolean operation type"),
|
|
422
|
+
name: import_zod6.z.string().optional().describe("Name for the result. Omit to auto-generate.")
|
|
423
|
+
});
|
|
424
|
+
function registerMcpTools4(server2, sendCommand) {
|
|
425
|
+
server2.tool(
|
|
426
|
+
"create_rectangle",
|
|
427
|
+
"Create rectangles (leaf nodes \u2014 cannot have children). For containers/cards/panels, use create_frame instead. Batch: pass multiple items.",
|
|
428
|
+
{ items: flexJson(import_zod6.z.array(rectItem)).describe("Array of rectangles to create"), depth },
|
|
429
|
+
async (params) => {
|
|
430
|
+
try {
|
|
431
|
+
return mcpJson(await sendCommand("create_rectangle", params));
|
|
432
|
+
} catch (e) {
|
|
433
|
+
return mcpError("Error creating rectangles", e);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
);
|
|
437
|
+
server2.tool(
|
|
438
|
+
"create_ellipse",
|
|
439
|
+
"Create ellipses (leaf nodes \u2014 cannot have children). For circular containers, use create_frame with cornerRadius instead. Batch: pass multiple items.",
|
|
440
|
+
{ items: flexJson(import_zod6.z.array(ellipseItem)).describe("Array of ellipses to create"), depth },
|
|
441
|
+
async (params) => {
|
|
442
|
+
try {
|
|
443
|
+
return mcpJson(await sendCommand("create_ellipse", params));
|
|
444
|
+
} catch (e) {
|
|
445
|
+
return mcpError("Error creating ellipses", e);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
server2.tool(
|
|
450
|
+
"create_line",
|
|
451
|
+
"Create lines (leaf nodes \u2014 cannot have children). For dividers inside layouts, use create_frame with a thin height and fill color instead. Batch: pass multiple items.",
|
|
452
|
+
{ items: flexJson(import_zod6.z.array(lineItem)).describe("Array of lines to create"), depth },
|
|
453
|
+
async (params) => {
|
|
454
|
+
try {
|
|
455
|
+
return mcpJson(await sendCommand("create_line", params));
|
|
456
|
+
} catch (e) {
|
|
457
|
+
return mcpError("Error creating lines", e);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
);
|
|
461
|
+
server2.tool(
|
|
462
|
+
"create_section",
|
|
463
|
+
"Create section nodes to organize content on the canvas.",
|
|
464
|
+
{ items: flexJson(import_zod6.z.array(sectionItem)).describe("Array of sections to create"), depth },
|
|
465
|
+
async (params) => {
|
|
466
|
+
try {
|
|
467
|
+
return mcpJson(await sendCommand("create_section", params));
|
|
468
|
+
} catch (e) {
|
|
469
|
+
return mcpError("Error creating sections", e);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
);
|
|
473
|
+
server2.tool(
|
|
474
|
+
"create_node_from_svg",
|
|
475
|
+
"Create nodes from SVG strings.",
|
|
476
|
+
{ items: flexJson(import_zod6.z.array(svgItem)).describe("Array of SVG items to create"), depth },
|
|
477
|
+
async (params) => {
|
|
478
|
+
try {
|
|
479
|
+
return mcpJson(await sendCommand("create_node_from_svg", params));
|
|
480
|
+
} catch (e) {
|
|
481
|
+
return mcpError("Error creating SVG nodes", e);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
server2.tool(
|
|
486
|
+
"create_boolean_operation",
|
|
487
|
+
"Create a boolean operation (union, intersect, subtract, exclude) from multiple nodes.",
|
|
488
|
+
{ items: flexJson(import_zod6.z.array(boolOpItem)).describe("Array of boolean operations to create"), depth },
|
|
489
|
+
async (params) => {
|
|
490
|
+
try {
|
|
491
|
+
return mcpJson(await sendCommand("create_boolean_operation", params));
|
|
492
|
+
} catch (e) {
|
|
493
|
+
return mcpError("Error creating boolean operations", e);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/tools/create-frame.ts
|
|
500
|
+
var import_zod7 = require("zod");
|
|
501
|
+
init_helpers();
|
|
502
|
+
var frameItem = import_zod7.z.object({
|
|
503
|
+
name: import_zod7.z.string().optional().describe("Frame name (default: 'Frame')"),
|
|
504
|
+
x: xPos,
|
|
505
|
+
y: yPos,
|
|
506
|
+
width: import_zod7.z.coerce.number().optional().describe("Width (default: 100)"),
|
|
507
|
+
height: import_zod7.z.coerce.number().optional().describe("Height (default: 100)"),
|
|
508
|
+
parentId,
|
|
509
|
+
fillColor: flexJson(colorRgba).optional().describe('Fill color. Hex "#FF0000" or {r,g,b,a?} 0-1. Default: no fill (empty fills array).'),
|
|
510
|
+
strokeColor: flexJson(colorRgba).optional().describe('Stroke color. Hex "#FF0000" or {r,g,b,a?} 0-1. Default: none.'),
|
|
511
|
+
strokeWeight: import_zod7.z.coerce.number().positive().optional().describe("Stroke weight (default: 1)"),
|
|
512
|
+
cornerRadius: import_zod7.z.coerce.number().min(0).optional().describe("Corner radius (default: 0)"),
|
|
513
|
+
layoutMode: import_zod7.z.enum(["NONE", "HORIZONTAL", "VERTICAL"]).optional().describe("Auto-layout direction (default: NONE)"),
|
|
514
|
+
layoutWrap: import_zod7.z.enum(["NO_WRAP", "WRAP"]).optional().describe("Wrap (default: NO_WRAP)"),
|
|
515
|
+
paddingTop: import_zod7.z.coerce.number().optional().describe("Top padding (default: 0)"),
|
|
516
|
+
paddingRight: import_zod7.z.coerce.number().optional().describe("Right padding (default: 0)"),
|
|
517
|
+
paddingBottom: import_zod7.z.coerce.number().optional().describe("Bottom padding (default: 0)"),
|
|
518
|
+
paddingLeft: import_zod7.z.coerce.number().optional().describe("Left padding (default: 0)"),
|
|
519
|
+
primaryAxisAlignItems: import_zod7.z.enum(["MIN", "MAX", "CENTER", "SPACE_BETWEEN"]).optional(),
|
|
520
|
+
counterAxisAlignItems: import_zod7.z.enum(["MIN", "MAX", "CENTER", "BASELINE"]).optional(),
|
|
521
|
+
layoutSizingHorizontal: import_zod7.z.enum(["FIXED", "HUG", "FILL"]).optional(),
|
|
522
|
+
layoutSizingVertical: import_zod7.z.enum(["FIXED", "HUG", "FILL"]).optional(),
|
|
523
|
+
itemSpacing: import_zod7.z.coerce.number().optional().describe("Spacing between children (default: 0)"),
|
|
524
|
+
// Style/variable references
|
|
525
|
+
fillStyleName: import_zod7.z.string().optional().describe("Apply a fill paint style by name (case-insensitive). Omit to skip."),
|
|
526
|
+
strokeStyleName: import_zod7.z.string().optional().describe("Apply a stroke paint style by name. Omit to skip."),
|
|
527
|
+
fillVariableId: import_zod7.z.string().optional().describe("Bind a color variable to the fill. Creates a solid fill and binds the variable to fills/0/color."),
|
|
528
|
+
strokeVariableId: import_zod7.z.string().optional().describe("Bind a color variable to the stroke. Creates a solid stroke and binds the variable to strokes/0/color.")
|
|
529
|
+
});
|
|
530
|
+
var autoLayoutItem = import_zod7.z.object({
|
|
531
|
+
nodeIds: flexJson(import_zod7.z.array(import_zod7.z.string())).describe("Array of node IDs to wrap"),
|
|
532
|
+
name: import_zod7.z.string().optional().describe("Frame name (default: 'Auto Layout')"),
|
|
533
|
+
layoutMode: import_zod7.z.enum(["HORIZONTAL", "VERTICAL"]).optional().describe("Direction (default: VERTICAL)"),
|
|
534
|
+
itemSpacing: import_zod7.z.coerce.number().optional().describe("Spacing between children (default: 0)"),
|
|
535
|
+
paddingTop: import_zod7.z.coerce.number().optional().describe("Top padding (default: 0)"),
|
|
536
|
+
paddingRight: import_zod7.z.coerce.number().optional().describe("Right padding (default: 0)"),
|
|
537
|
+
paddingBottom: import_zod7.z.coerce.number().optional().describe("Bottom padding (default: 0)"),
|
|
538
|
+
paddingLeft: import_zod7.z.coerce.number().optional().describe("Left padding (default: 0)"),
|
|
539
|
+
primaryAxisAlignItems: import_zod7.z.enum(["MIN", "MAX", "CENTER", "SPACE_BETWEEN"]).optional(),
|
|
540
|
+
counterAxisAlignItems: import_zod7.z.enum(["MIN", "MAX", "CENTER", "BASELINE"]).optional(),
|
|
541
|
+
layoutSizingHorizontal: import_zod7.z.enum(["FIXED", "HUG", "FILL"]).optional(),
|
|
542
|
+
layoutSizingVertical: import_zod7.z.enum(["FIXED", "HUG", "FILL"]).optional(),
|
|
543
|
+
layoutWrap: import_zod7.z.enum(["NO_WRAP", "WRAP"]).optional()
|
|
544
|
+
});
|
|
545
|
+
function registerMcpTools5(server2, sendCommand) {
|
|
546
|
+
server2.tool(
|
|
547
|
+
"create_frame",
|
|
548
|
+
"Create frames in Figma. Supports batch. Prefer fillStyleName or fillVariableId over hardcoded fillColor for design token consistency.",
|
|
549
|
+
{ items: flexJson(import_zod7.z.array(frameItem)).describe("Array of frames to create"), depth },
|
|
550
|
+
async (params) => {
|
|
551
|
+
try {
|
|
552
|
+
return mcpJson(await sendCommand("create_frame", params));
|
|
553
|
+
} catch (e) {
|
|
554
|
+
return mcpError("Error creating frames", e);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
);
|
|
558
|
+
server2.tool(
|
|
559
|
+
"create_auto_layout",
|
|
560
|
+
"Wrap existing nodes in an auto-layout frame. One call replaces create_frame + update_frame + insert_child \xD7 N.",
|
|
561
|
+
{ items: flexJson(import_zod7.z.array(autoLayoutItem)).describe("Array of auto-layout wraps to perform"), depth },
|
|
562
|
+
async (params) => {
|
|
563
|
+
try {
|
|
564
|
+
return mcpJson(await sendCommand("create_auto_layout", params));
|
|
565
|
+
} catch (e) {
|
|
566
|
+
return mcpError("Error creating auto layout", e);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/tools/create-text.ts
|
|
573
|
+
var import_zod8 = require("zod");
|
|
574
|
+
init_helpers();
|
|
575
|
+
var textItem = import_zod8.z.object({
|
|
576
|
+
text: import_zod8.z.string().describe("Text content"),
|
|
577
|
+
name: import_zod8.z.string().optional().describe("Layer name (default: text content)"),
|
|
578
|
+
x: xPos,
|
|
579
|
+
y: yPos,
|
|
580
|
+
fontSize: import_zod8.z.coerce.number().optional().describe("Font size (default: 14)"),
|
|
581
|
+
fontWeight: import_zod8.z.coerce.number().optional().describe("Font weight: 100-900 (default: 400)"),
|
|
582
|
+
fontColor: flexJson(colorRgba).optional().describe('Font color. Hex "#000000" or {r,g,b,a?} 0-1. Default: black.'),
|
|
583
|
+
fontColorVariableId: import_zod8.z.string().optional().describe("Bind a color variable to the text fill instead of hardcoded fontColor."),
|
|
584
|
+
fontColorStyleName: import_zod8.z.string().optional().describe("Apply a paint style to the text fill by name (case-insensitive). Overrides fontColor."),
|
|
585
|
+
parentId,
|
|
586
|
+
textStyleId: import_zod8.z.string().optional().describe("Text style ID to apply (overrides fontSize/fontWeight). Omit to skip."),
|
|
587
|
+
textStyleName: import_zod8.z.string().optional().describe("Text style name (case-insensitive match). Omit to skip."),
|
|
588
|
+
textAlignHorizontal: import_zod8.z.enum(["LEFT", "CENTER", "RIGHT", "JUSTIFIED"]).optional().describe("Horizontal text alignment (default: LEFT)"),
|
|
589
|
+
textAlignVertical: import_zod8.z.enum(["TOP", "CENTER", "BOTTOM"]).optional().describe("Vertical text alignment (default: TOP)"),
|
|
590
|
+
layoutSizingHorizontal: import_zod8.z.enum(["FIXED", "HUG", "FILL"]).optional().describe("Horizontal sizing. FILL auto-sets textAutoResize to HEIGHT."),
|
|
591
|
+
layoutSizingVertical: import_zod8.z.enum(["FIXED", "HUG", "FILL"]).optional().describe("Vertical sizing (default: HUG)"),
|
|
592
|
+
textAutoResize: import_zod8.z.enum(["NONE", "WIDTH_AND_HEIGHT", "HEIGHT", "TRUNCATE"]).optional().describe("Text auto-resize behavior (default: WIDTH_AND_HEIGHT when FILL)")
|
|
593
|
+
});
|
|
594
|
+
function registerMcpTools6(server2, sendCommand) {
|
|
595
|
+
server2.tool(
|
|
596
|
+
"create_text",
|
|
597
|
+
"Create text nodes in Figma. Uses Inter font. Max 10 items per batch. Use textStyleName for typography and fontColorStyleName for fill color.",
|
|
598
|
+
{ items: flexJson(import_zod8.z.array(textItem).max(10)).describe("Array of text nodes to create (max 10)"), depth },
|
|
599
|
+
async (params) => {
|
|
600
|
+
try {
|
|
601
|
+
return mcpJson(await sendCommand("create_text", params));
|
|
602
|
+
} catch (e) {
|
|
603
|
+
return mcpError("Error creating text", e);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/tools/modify-node.ts
|
|
610
|
+
var import_zod9 = require("zod");
|
|
611
|
+
init_helpers();
|
|
612
|
+
var moveItem = import_zod9.z.object({
|
|
613
|
+
nodeId,
|
|
614
|
+
x: import_zod9.z.coerce.number().describe("New X"),
|
|
615
|
+
y: import_zod9.z.coerce.number().describe("New Y")
|
|
616
|
+
});
|
|
617
|
+
var resizeItem = import_zod9.z.object({
|
|
618
|
+
nodeId,
|
|
619
|
+
width: import_zod9.z.coerce.number().positive().describe("New width"),
|
|
620
|
+
height: import_zod9.z.coerce.number().positive().describe("New height")
|
|
621
|
+
});
|
|
622
|
+
var deleteItem = import_zod9.z.object({
|
|
623
|
+
nodeId: import_zod9.z.string().describe("Node ID to delete")
|
|
624
|
+
});
|
|
625
|
+
var cloneItem = import_zod9.z.object({
|
|
626
|
+
nodeId: import_zod9.z.string().describe("Node ID to clone"),
|
|
627
|
+
parentId: import_zod9.z.string().optional().describe("Parent for the clone (e.g. a page ID). Defaults to same parent as original."),
|
|
628
|
+
x: import_zod9.z.coerce.number().optional().describe("New X for clone. Omit to keep original position."),
|
|
629
|
+
y: import_zod9.z.coerce.number().optional().describe("New Y for clone. Omit to keep original position.")
|
|
630
|
+
});
|
|
631
|
+
var insertItem = import_zod9.z.object({
|
|
632
|
+
parentId: import_zod9.z.string().describe("Parent node ID"),
|
|
633
|
+
childId: import_zod9.z.string().describe("Child node ID to move"),
|
|
634
|
+
index: import_zod9.z.coerce.number().optional().describe("Index to insert at (0=first). Omit to append.")
|
|
635
|
+
});
|
|
636
|
+
function registerMcpTools7(server2, sendCommand) {
|
|
637
|
+
server2.tool(
|
|
638
|
+
"move_node",
|
|
639
|
+
"Move nodes to new positions. Batch: pass multiple items.",
|
|
640
|
+
{ items: flexJson(import_zod9.z.array(moveItem)).describe("Array of {nodeId, x, y}"), depth },
|
|
641
|
+
async (params) => {
|
|
642
|
+
try {
|
|
643
|
+
return mcpJson(await sendCommand("move_node", params));
|
|
644
|
+
} catch (e) {
|
|
645
|
+
return mcpError("Error moving nodes", e);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
);
|
|
649
|
+
server2.tool(
|
|
650
|
+
"resize_node",
|
|
651
|
+
"Resize nodes. Batch: pass multiple items.",
|
|
652
|
+
{ items: flexJson(import_zod9.z.array(resizeItem)).describe("Array of {nodeId, width, height}"), depth },
|
|
653
|
+
async (params) => {
|
|
654
|
+
try {
|
|
655
|
+
return mcpJson(await sendCommand("resize_node", params));
|
|
656
|
+
} catch (e) {
|
|
657
|
+
return mcpError("Error resizing nodes", e);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
);
|
|
661
|
+
server2.tool(
|
|
662
|
+
"delete_node",
|
|
663
|
+
"Delete nodes. Batch: pass multiple items.",
|
|
664
|
+
{ items: flexJson(import_zod9.z.array(deleteItem)).describe("Array of {nodeId}") },
|
|
665
|
+
async (params) => {
|
|
666
|
+
try {
|
|
667
|
+
return mcpJson(await sendCommand("delete_node", params));
|
|
668
|
+
} catch (e) {
|
|
669
|
+
return mcpError("Error deleting nodes", e);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
);
|
|
673
|
+
server2.tool(
|
|
674
|
+
"clone_node",
|
|
675
|
+
"Clone nodes. Batch: pass multiple items.",
|
|
676
|
+
{ items: flexJson(import_zod9.z.array(cloneItem)).describe("Array of {nodeId, x?, y?}"), depth },
|
|
677
|
+
async (params) => {
|
|
678
|
+
try {
|
|
679
|
+
return mcpJson(await sendCommand("clone_node", params));
|
|
680
|
+
} catch (e) {
|
|
681
|
+
return mcpError("Error cloning nodes", e);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
);
|
|
685
|
+
server2.tool(
|
|
686
|
+
"insert_child",
|
|
687
|
+
"Move nodes into a parent at a specific index (reorder/reparent). Batch: pass multiple items.",
|
|
688
|
+
{ items: flexJson(import_zod9.z.array(insertItem)).describe("Array of {parentId, childId, index?}"), depth },
|
|
689
|
+
async (params) => {
|
|
690
|
+
try {
|
|
691
|
+
return mcpJson(await sendCommand("insert_child", params));
|
|
692
|
+
} catch (e) {
|
|
693
|
+
return mcpError("Error inserting children", e);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/tools/fill-stroke.ts
|
|
700
|
+
var import_zod10 = require("zod");
|
|
701
|
+
init_helpers();
|
|
702
|
+
var fillItem = import_zod10.z.object({
|
|
703
|
+
nodeId,
|
|
704
|
+
color: flexJson(colorRgba).optional().describe('Fill color. Hex "#FF0000" or {r,g,b,a?} 0-1. Ignored when styleName is set.'),
|
|
705
|
+
styleName: import_zod10.z.string().optional().describe("Apply fill paint style by name instead of color. Omit to use color.")
|
|
706
|
+
});
|
|
707
|
+
var strokeItem = import_zod10.z.object({
|
|
708
|
+
nodeId,
|
|
709
|
+
color: flexJson(colorRgba).optional().describe('Stroke color. Hex "#FF0000" or {r,g,b,a?} 0-1. Ignored when styleName is set.'),
|
|
710
|
+
strokeWeight: import_zod10.z.coerce.number().positive().optional().describe("Stroke weight (default: 1)"),
|
|
711
|
+
styleName: import_zod10.z.string().optional().describe("Apply stroke paint style by name instead of color. Omit to use color.")
|
|
712
|
+
});
|
|
713
|
+
var cornerItem = import_zod10.z.object({
|
|
714
|
+
nodeId,
|
|
715
|
+
radius: import_zod10.z.coerce.number().min(0).describe("Corner radius"),
|
|
716
|
+
corners: flexJson(import_zod10.z.array(flexBool(import_zod10.z.boolean())).length(4)).optional().describe("Which corners to round [topLeft, topRight, bottomRight, bottomLeft]. Default: all corners [true,true,true,true].")
|
|
717
|
+
});
|
|
718
|
+
var opacityItem = import_zod10.z.object({
|
|
719
|
+
nodeId,
|
|
720
|
+
opacity: import_zod10.z.coerce.number().min(0).max(1).describe("Opacity (0-1)")
|
|
721
|
+
});
|
|
722
|
+
function registerMcpTools8(server2, sendCommand) {
|
|
723
|
+
server2.tool(
|
|
724
|
+
"set_fill_color",
|
|
725
|
+
"Set fill color on nodes. Use styleName to apply a paint style by name, or provide color directly. Batch: pass multiple items.",
|
|
726
|
+
{ items: flexJson(import_zod10.z.array(fillItem)).describe("Array of {nodeId, color?, styleName?}"), depth },
|
|
727
|
+
async (params) => {
|
|
728
|
+
try {
|
|
729
|
+
return mcpJson(await sendCommand("set_fill_color", params));
|
|
730
|
+
} catch (e) {
|
|
731
|
+
return mcpError("Error setting fill", e);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
);
|
|
735
|
+
server2.tool(
|
|
736
|
+
"set_stroke_color",
|
|
737
|
+
"Set stroke color on nodes. Use styleName to apply a paint style by name. Batch: pass multiple items.",
|
|
738
|
+
{ items: flexJson(import_zod10.z.array(strokeItem)).describe("Array of {nodeId, color?, strokeWeight?, styleName?}"), depth },
|
|
739
|
+
async (params) => {
|
|
740
|
+
try {
|
|
741
|
+
return mcpJson(await sendCommand("set_stroke_color", params));
|
|
742
|
+
} catch (e) {
|
|
743
|
+
return mcpError("Error setting stroke", e);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
);
|
|
747
|
+
server2.tool(
|
|
748
|
+
"set_corner_radius",
|
|
749
|
+
"Set corner radius on nodes. Batch: pass multiple items.",
|
|
750
|
+
{ items: flexJson(import_zod10.z.array(cornerItem)).describe("Array of {nodeId, radius, corners?}"), depth },
|
|
751
|
+
async (params) => {
|
|
752
|
+
try {
|
|
753
|
+
return mcpJson(await sendCommand("set_corner_radius", params));
|
|
754
|
+
} catch (e) {
|
|
755
|
+
return mcpError("Error setting corner radius", e);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
);
|
|
759
|
+
server2.tool(
|
|
760
|
+
"set_opacity",
|
|
761
|
+
"Set opacity on nodes. Batch: pass multiple items.",
|
|
762
|
+
{ items: flexJson(import_zod10.z.array(opacityItem)).describe("Array of {nodeId, opacity}"), depth },
|
|
763
|
+
async (params) => {
|
|
764
|
+
try {
|
|
765
|
+
return mcpJson(await sendCommand("set_opacity", params));
|
|
766
|
+
} catch (e) {
|
|
767
|
+
return mcpError("Error setting opacity", e);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/tools/update-frame.ts
|
|
774
|
+
var import_zod11 = require("zod");
|
|
775
|
+
init_helpers();
|
|
776
|
+
var updateFrameItem = import_zod11.z.object({
|
|
777
|
+
nodeId,
|
|
778
|
+
layoutMode: import_zod11.z.enum(["NONE", "HORIZONTAL", "VERTICAL"]).optional().describe("Auto-layout direction"),
|
|
779
|
+
layoutWrap: import_zod11.z.enum(["NO_WRAP", "WRAP"]).optional().describe("Wrap (default: NO_WRAP)"),
|
|
780
|
+
paddingTop: import_zod11.z.coerce.number().optional().describe("Top padding"),
|
|
781
|
+
paddingRight: import_zod11.z.coerce.number().optional().describe("Right padding"),
|
|
782
|
+
paddingBottom: import_zod11.z.coerce.number().optional().describe("Bottom padding"),
|
|
783
|
+
paddingLeft: import_zod11.z.coerce.number().optional().describe("Left padding"),
|
|
784
|
+
primaryAxisAlignItems: import_zod11.z.enum(["MIN", "MAX", "CENTER", "SPACE_BETWEEN"]).optional().describe("Primary axis alignment"),
|
|
785
|
+
counterAxisAlignItems: import_zod11.z.enum(["MIN", "MAX", "CENTER", "BASELINE"]).optional().describe("Counter axis alignment"),
|
|
786
|
+
layoutSizingHorizontal: import_zod11.z.enum(["FIXED", "HUG", "FILL"]).optional().describe("Horizontal sizing (works on any node in auto-layout)"),
|
|
787
|
+
layoutSizingVertical: import_zod11.z.enum(["FIXED", "HUG", "FILL"]).optional().describe("Vertical sizing (works on any node in auto-layout)"),
|
|
788
|
+
itemSpacing: import_zod11.z.coerce.number().optional().describe("Spacing between children"),
|
|
789
|
+
counterAxisSpacing: import_zod11.z.coerce.number().optional().describe("Spacing between wrapped rows/columns (WRAP only)")
|
|
790
|
+
});
|
|
791
|
+
function registerMcpTools9(server2, sendCommand) {
|
|
792
|
+
server2.tool(
|
|
793
|
+
"update_frame",
|
|
794
|
+
"Update layout properties on frames. Combines layout mode, padding, alignment, sizing, and spacing in one call. Batch: pass multiple items.",
|
|
795
|
+
{ items: flexJson(import_zod11.z.array(updateFrameItem)).describe("Array of {nodeId, ...layout properties}"), depth },
|
|
796
|
+
async (params) => {
|
|
797
|
+
try {
|
|
798
|
+
return mcpJson(await sendCommand("update_frame", params));
|
|
799
|
+
} catch (e) {
|
|
800
|
+
return mcpError("Error updating frame", e);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/tools/effects.ts
|
|
807
|
+
var import_zod12 = require("zod");
|
|
808
|
+
init_helpers();
|
|
809
|
+
var effectItem = import_zod12.z.object({
|
|
810
|
+
nodeId,
|
|
811
|
+
effects: flexJson(import_zod12.z.array(effectEntry)).optional().describe("Array of effect objects. Ignored when effectStyleName is set."),
|
|
812
|
+
effectStyleName: import_zod12.z.string().optional().describe("Apply an effect style by name (case-insensitive). Omit to use raw effects.")
|
|
813
|
+
});
|
|
814
|
+
var constraintItem = import_zod12.z.object({
|
|
815
|
+
nodeId,
|
|
816
|
+
horizontal: import_zod12.z.enum(["MIN", "CENTER", "MAX", "STRETCH", "SCALE"]),
|
|
817
|
+
vertical: import_zod12.z.enum(["MIN", "CENTER", "MAX", "STRETCH", "SCALE"])
|
|
818
|
+
});
|
|
819
|
+
var exportSettingEntry = import_zod12.z.object({
|
|
820
|
+
format: import_zod12.z.enum(["PNG", "JPG", "SVG", "PDF"]),
|
|
821
|
+
suffix: import_zod12.z.string().optional(),
|
|
822
|
+
contentsOnly: flexBool(import_zod12.z.boolean()).optional(),
|
|
823
|
+
constraint: flexJson(import_zod12.z.object({
|
|
824
|
+
type: import_zod12.z.enum(["SCALE", "WIDTH", "HEIGHT"]),
|
|
825
|
+
value: import_zod12.z.coerce.number()
|
|
826
|
+
})).optional()
|
|
827
|
+
});
|
|
828
|
+
var exportSettingsItem = import_zod12.z.object({
|
|
829
|
+
nodeId,
|
|
830
|
+
settings: flexJson(import_zod12.z.array(exportSettingEntry)).describe("Export settings array")
|
|
831
|
+
});
|
|
832
|
+
var nodePropertiesItem = import_zod12.z.object({
|
|
833
|
+
nodeId,
|
|
834
|
+
properties: flexJson(import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown())).describe("Key-value properties to set")
|
|
835
|
+
});
|
|
836
|
+
function registerMcpTools10(server2, sendCommand) {
|
|
837
|
+
server2.tool(
|
|
838
|
+
"set_effects",
|
|
839
|
+
"Set effects (shadows, blurs) on nodes. Use effectStyleName to apply by name, or provide raw effects. Batch: pass multiple items.",
|
|
840
|
+
{ items: flexJson(import_zod12.z.array(effectItem)).describe("Array of {nodeId, effects}"), depth },
|
|
841
|
+
async (params) => {
|
|
842
|
+
try {
|
|
843
|
+
return mcpJson(await sendCommand("set_effects", params));
|
|
844
|
+
} catch (e) {
|
|
845
|
+
return mcpError("Error setting effects", e);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
);
|
|
849
|
+
server2.tool(
|
|
850
|
+
"set_constraints",
|
|
851
|
+
"Set constraints on nodes. Batch: pass multiple items.",
|
|
852
|
+
{ items: flexJson(import_zod12.z.array(constraintItem)).describe("Array of {nodeId, horizontal, vertical}"), depth },
|
|
853
|
+
async (params) => {
|
|
854
|
+
try {
|
|
855
|
+
return mcpJson(await sendCommand("set_constraints", params));
|
|
856
|
+
} catch (e) {
|
|
857
|
+
return mcpError("Error setting constraints", e);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
);
|
|
861
|
+
server2.tool(
|
|
862
|
+
"set_export_settings",
|
|
863
|
+
"Set export settings on nodes. Batch: pass multiple items.",
|
|
864
|
+
{ items: flexJson(import_zod12.z.array(exportSettingsItem)).describe("Array of {nodeId, settings}"), depth },
|
|
865
|
+
async (params) => {
|
|
866
|
+
try {
|
|
867
|
+
return mcpJson(await sendCommand("set_export_settings", params));
|
|
868
|
+
} catch (e) {
|
|
869
|
+
return mcpError("Error setting export settings", e);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
);
|
|
873
|
+
server2.tool(
|
|
874
|
+
"set_node_properties",
|
|
875
|
+
"Set arbitrary properties on nodes. Batch: pass multiple items.",
|
|
876
|
+
{ items: flexJson(import_zod12.z.array(nodePropertiesItem)).describe("Array of {nodeId, properties}"), depth },
|
|
877
|
+
async (params) => {
|
|
878
|
+
try {
|
|
879
|
+
return mcpJson(await sendCommand("set_node_properties", params));
|
|
880
|
+
} catch (e) {
|
|
881
|
+
return mcpError("Error setting node properties", e);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/tools/text.ts
|
|
888
|
+
var import_zod13 = require("zod");
|
|
889
|
+
init_helpers();
|
|
890
|
+
var textContentItem = import_zod13.z.object({
|
|
891
|
+
nodeId: import_zod13.z.string().describe("Text node ID"),
|
|
892
|
+
text: import_zod13.z.string().describe("New text content")
|
|
893
|
+
});
|
|
894
|
+
var textPropsItem = import_zod13.z.object({
|
|
895
|
+
nodeId: import_zod13.z.string().describe("Text node ID"),
|
|
896
|
+
fontSize: import_zod13.z.coerce.number().optional().describe("Font size"),
|
|
897
|
+
fontWeight: import_zod13.z.coerce.number().optional().describe("Font weight: 100-900"),
|
|
898
|
+
fontColor: flexJson(colorRgba).optional().describe('Font color. Hex "#000" or {r,g,b,a?} 0-1.'),
|
|
899
|
+
textStyleId: import_zod13.z.string().optional().describe("Text style ID to apply (overrides font props)"),
|
|
900
|
+
textStyleName: import_zod13.z.string().optional().describe("Text style name (case-insensitive match)"),
|
|
901
|
+
textAlignHorizontal: import_zod13.z.enum(["LEFT", "CENTER", "RIGHT", "JUSTIFIED"]).optional().describe("Horizontal text alignment"),
|
|
902
|
+
textAlignVertical: import_zod13.z.enum(["TOP", "CENTER", "BOTTOM"]).optional().describe("Vertical text alignment"),
|
|
903
|
+
textAutoResize: import_zod13.z.enum(["NONE", "WIDTH_AND_HEIGHT", "HEIGHT", "TRUNCATE"]).optional(),
|
|
904
|
+
layoutSizingHorizontal: import_zod13.z.enum(["FIXED", "HUG", "FILL"]).optional(),
|
|
905
|
+
layoutSizingVertical: import_zod13.z.enum(["FIXED", "HUG", "FILL"]).optional()
|
|
906
|
+
});
|
|
907
|
+
var scanTextItem = import_zod13.z.object({
|
|
908
|
+
nodeId,
|
|
909
|
+
limit: import_zod13.z.coerce.number().optional().describe("Max text nodes to return (default: 50)"),
|
|
910
|
+
includePath: flexBool(import_zod13.z.boolean()).optional().describe("Include ancestor path strings (default: true). Set false to reduce payload."),
|
|
911
|
+
includeGeometry: flexBool(import_zod13.z.boolean()).optional().describe("Include absoluteX/absoluteY/width/height (default: true). Set false to reduce payload.")
|
|
912
|
+
});
|
|
913
|
+
function registerMcpTools11(server2, sendCommand) {
|
|
914
|
+
server2.tool(
|
|
915
|
+
"set_text_content",
|
|
916
|
+
"Set text content on text nodes. Batch: pass multiple items to replace text in multiple nodes at once.",
|
|
917
|
+
{ items: flexJson(import_zod13.z.array(textContentItem)).describe("Array of {nodeId, text}"), depth },
|
|
918
|
+
async (params) => {
|
|
919
|
+
try {
|
|
920
|
+
return mcpJson(await sendCommand("set_text_content", params));
|
|
921
|
+
} catch (e) {
|
|
922
|
+
return mcpError("Error setting text content", e);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
);
|
|
926
|
+
server2.tool(
|
|
927
|
+
"set_text_properties",
|
|
928
|
+
"Set font properties on existing text nodes (fontSize, fontWeight, fontColor, textStyle). Batch: pass multiple items.",
|
|
929
|
+
{ items: flexJson(import_zod13.z.array(textPropsItem)).describe("Array of {nodeId, fontSize?, fontWeight?, fontColor?, ...}"), depth },
|
|
930
|
+
async (params) => {
|
|
931
|
+
try {
|
|
932
|
+
return mcpJson(await sendCommand("set_text_properties", params));
|
|
933
|
+
} catch (e) {
|
|
934
|
+
return mcpError("Error setting text properties", e);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
);
|
|
938
|
+
server2.tool(
|
|
939
|
+
"scan_text_nodes",
|
|
940
|
+
"Scan all text nodes within a node tree. Batch: pass multiple items.",
|
|
941
|
+
{ items: flexJson(import_zod13.z.array(scanTextItem)).describe("Array of {nodeId}") },
|
|
942
|
+
async (params) => {
|
|
943
|
+
try {
|
|
944
|
+
return mcpJson(await sendCommand("scan_text_nodes", params));
|
|
945
|
+
} catch (e) {
|
|
946
|
+
return mcpError("Error scanning text nodes", e);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// src/tools/fonts.ts
|
|
953
|
+
var import_zod14 = require("zod");
|
|
954
|
+
function registerMcpTools12(server2, sendCommand) {
|
|
955
|
+
server2.tool(
|
|
956
|
+
"get_available_fonts",
|
|
957
|
+
"Get available fonts in Figma. Optionally filter by query string.",
|
|
958
|
+
{ query: import_zod14.z.string().optional().describe("Filter fonts by name (case-insensitive). Omit to list all fonts.") },
|
|
959
|
+
async ({ query }) => {
|
|
960
|
+
try {
|
|
961
|
+
return mcpJson(await sendCommand("get_available_fonts", { query }));
|
|
962
|
+
} catch (e) {
|
|
963
|
+
return mcpError("Error getting fonts", e);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/tools/components.ts
|
|
970
|
+
var import_zod15 = require("zod");
|
|
971
|
+
init_helpers();
|
|
972
|
+
var componentItem = import_zod15.z.object({
|
|
973
|
+
name: import_zod15.z.string().describe("Component name"),
|
|
974
|
+
x: xPos,
|
|
975
|
+
y: yPos,
|
|
976
|
+
width: import_zod15.z.coerce.number().optional().describe("Width (default: 100)"),
|
|
977
|
+
height: import_zod15.z.coerce.number().optional().describe("Height (default: 100)"),
|
|
978
|
+
parentId,
|
|
979
|
+
fillColor: flexJson(colorRgba).optional().describe('Fill color. Hex "#FF0000" or {r,g,b,a?} 0-1. Omit for no fill.'),
|
|
980
|
+
fillStyleName: import_zod15.z.string().optional().describe("Apply a fill paint style by name (case-insensitive)."),
|
|
981
|
+
fillVariableId: import_zod15.z.string().optional().describe("Bind a color variable to the fill."),
|
|
982
|
+
strokeColor: flexJson(colorRgba).optional().describe('Stroke color. Hex "#FF0000" or {r,g,b,a?} 0-1. Omit for no stroke.'),
|
|
983
|
+
strokeStyleName: import_zod15.z.string().optional().describe("Apply a stroke paint style by name."),
|
|
984
|
+
strokeVariableId: import_zod15.z.string().optional().describe("Bind a color variable to the stroke."),
|
|
985
|
+
strokeWeight: import_zod15.z.coerce.number().positive().optional().describe("Stroke weight (default: 1)"),
|
|
986
|
+
cornerRadius: import_zod15.z.coerce.number().optional().describe("Corner radius (default: 0)"),
|
|
987
|
+
layoutMode: import_zod15.z.enum(["NONE", "HORIZONTAL", "VERTICAL"]).optional().describe("Layout direction (default: NONE)"),
|
|
988
|
+
layoutWrap: import_zod15.z.enum(["NO_WRAP", "WRAP"]).optional().describe("Wrap behavior (default: NO_WRAP)"),
|
|
989
|
+
paddingTop: import_zod15.z.coerce.number().optional().describe("Top padding (default: 0)"),
|
|
990
|
+
paddingRight: import_zod15.z.coerce.number().optional().describe("Right padding (default: 0)"),
|
|
991
|
+
paddingBottom: import_zod15.z.coerce.number().optional().describe("Bottom padding (default: 0)"),
|
|
992
|
+
paddingLeft: import_zod15.z.coerce.number().optional().describe("Left padding (default: 0)"),
|
|
993
|
+
primaryAxisAlignItems: import_zod15.z.enum(["MIN", "MAX", "CENTER", "SPACE_BETWEEN"]).optional().describe("Primary axis alignment (default: MIN)"),
|
|
994
|
+
counterAxisAlignItems: import_zod15.z.enum(["MIN", "MAX", "CENTER", "BASELINE"]).optional().describe("Counter axis alignment (default: MIN)"),
|
|
995
|
+
layoutSizingHorizontal: import_zod15.z.enum(["FIXED", "HUG", "FILL"]).optional().describe("Horizontal sizing (default: FIXED)"),
|
|
996
|
+
layoutSizingVertical: import_zod15.z.enum(["FIXED", "HUG", "FILL"]).optional().describe("Vertical sizing (default: FIXED)"),
|
|
997
|
+
itemSpacing: import_zod15.z.coerce.number().optional().describe("Spacing between children (default: 0)")
|
|
998
|
+
});
|
|
999
|
+
var fromNodeItem = import_zod15.z.object({
|
|
1000
|
+
nodeId
|
|
1001
|
+
});
|
|
1002
|
+
var combineItem = import_zod15.z.object({
|
|
1003
|
+
componentIds: flexJson(import_zod15.z.array(import_zod15.z.string())).describe("Component IDs to combine (min 2)"),
|
|
1004
|
+
name: import_zod15.z.string().optional().describe("Name for the component set. Omit to auto-generate.")
|
|
1005
|
+
});
|
|
1006
|
+
var propItem = import_zod15.z.object({
|
|
1007
|
+
componentId: import_zod15.z.string().describe("Component node ID"),
|
|
1008
|
+
propertyName: import_zod15.z.string().describe("Property name"),
|
|
1009
|
+
type: import_zod15.z.enum(["BOOLEAN", "TEXT", "INSTANCE_SWAP", "VARIANT"]).describe("Property type"),
|
|
1010
|
+
defaultValue: flexBool(import_zod15.z.union([import_zod15.z.string(), import_zod15.z.boolean()])).describe("Default value (string for TEXT/VARIANT, boolean for BOOLEAN)"),
|
|
1011
|
+
preferredValues: flexJson(import_zod15.z.array(import_zod15.z.object({
|
|
1012
|
+
type: import_zod15.z.enum(["COMPONENT", "COMPONENT_SET"]),
|
|
1013
|
+
key: import_zod15.z.string()
|
|
1014
|
+
})).optional()).describe("Preferred values for INSTANCE_SWAP type. Omit for none.")
|
|
1015
|
+
});
|
|
1016
|
+
var instanceItem = import_zod15.z.object({
|
|
1017
|
+
componentId: import_zod15.z.string().describe("Component or component set ID"),
|
|
1018
|
+
variantProperties: flexJson(import_zod15.z.record(import_zod15.z.string(), import_zod15.z.string())).optional().describe('Pick variant by properties, e.g. {"Style":"Secondary","Size":"Large"}. Ignored for plain COMPONENT IDs.'),
|
|
1019
|
+
x: import_zod15.z.coerce.number().optional().describe("X position. Omit to keep default."),
|
|
1020
|
+
y: import_zod15.z.coerce.number().optional().describe("Y position. Omit to keep default."),
|
|
1021
|
+
parentId
|
|
1022
|
+
});
|
|
1023
|
+
function registerMcpTools13(server2, sendCommand) {
|
|
1024
|
+
server2.tool(
|
|
1025
|
+
"create_component",
|
|
1026
|
+
"Create components in Figma. Same layout params as create_frame. Name with 'Property=Value' pattern (e.g. 'Size=Small') if you plan to combine_as_variants later. Batch: pass multiple items.",
|
|
1027
|
+
{ items: flexJson(import_zod15.z.array(componentItem)).describe("Array of components to create"), depth },
|
|
1028
|
+
async (params) => {
|
|
1029
|
+
try {
|
|
1030
|
+
return mcpJson(await sendCommand("create_component", params));
|
|
1031
|
+
} catch (e) {
|
|
1032
|
+
return mcpError("Error creating component", e);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
);
|
|
1036
|
+
server2.tool(
|
|
1037
|
+
"create_component_from_node",
|
|
1038
|
+
"Convert existing nodes into components. Batch: pass multiple items.",
|
|
1039
|
+
{ items: flexJson(import_zod15.z.array(fromNodeItem)).describe("Array of {nodeId}"), depth },
|
|
1040
|
+
async (params) => {
|
|
1041
|
+
try {
|
|
1042
|
+
return mcpJson(await sendCommand("create_component_from_node", params));
|
|
1043
|
+
} catch (e) {
|
|
1044
|
+
return mcpError("Error creating component from node", e);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
);
|
|
1048
|
+
server2.tool(
|
|
1049
|
+
"combine_as_variants",
|
|
1050
|
+
"Combine components into variant sets. Name components with 'Property=Value' pattern (e.g. 'Style=Primary', 'Size=Large') BEFORE combining \u2014 Figma derives variant properties from component names. Avoid slashes in names. The resulting set is placed in the components' shared parent (or page root if parents differ). Batch: pass multiple items.",
|
|
1051
|
+
{ items: flexJson(import_zod15.z.array(combineItem)).describe("Array of {componentIds, name?}"), depth },
|
|
1052
|
+
async (params) => {
|
|
1053
|
+
try {
|
|
1054
|
+
return mcpJson(await sendCommand("combine_as_variants", params));
|
|
1055
|
+
} catch (e) {
|
|
1056
|
+
return mcpError("Error combining variants", e);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
);
|
|
1060
|
+
server2.tool(
|
|
1061
|
+
"add_component_property",
|
|
1062
|
+
"Add properties to components. Batch: pass multiple items.",
|
|
1063
|
+
{ items: flexJson(import_zod15.z.array(propItem)).describe("Array of {componentId, propertyName, type, defaultValue, preferredValues?}") },
|
|
1064
|
+
async (params) => {
|
|
1065
|
+
try {
|
|
1066
|
+
return mcpJson(await sendCommand("add_component_property", params));
|
|
1067
|
+
} catch (e) {
|
|
1068
|
+
return mcpError("Error adding component property", e);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
);
|
|
1072
|
+
server2.tool(
|
|
1073
|
+
"create_instance_from_local",
|
|
1074
|
+
'Create instances of local components. For COMPONENT_SET, use variantProperties to pick a specific variant (e.g. {"Style":"Secondary"}). Batch: pass multiple items.',
|
|
1075
|
+
{ items: flexJson(import_zod15.z.array(instanceItem)).describe("Array of {componentId, x?, y?, parentId?}") },
|
|
1076
|
+
async (params) => {
|
|
1077
|
+
try {
|
|
1078
|
+
return mcpJson(await sendCommand("create_instance_from_local", params));
|
|
1079
|
+
} catch (e) {
|
|
1080
|
+
return mcpError("Error creating instance", e);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
);
|
|
1084
|
+
server2.tool(
|
|
1085
|
+
"search_components",
|
|
1086
|
+
"Search local components and component sets across all pages. Returns component id, name, and which page it lives on.",
|
|
1087
|
+
{
|
|
1088
|
+
query: import_zod15.z.string().optional().describe("Filter by name (case-insensitive substring). Omit to list all."),
|
|
1089
|
+
setsOnly: flexBool(import_zod15.z.boolean()).optional().describe("If true, return only COMPONENT_SET nodes"),
|
|
1090
|
+
limit: import_zod15.z.coerce.number().optional().describe("Max results (default 100)"),
|
|
1091
|
+
offset: import_zod15.z.coerce.number().optional().describe("Skip N results (default 0)")
|
|
1092
|
+
},
|
|
1093
|
+
async (params) => {
|
|
1094
|
+
try {
|
|
1095
|
+
return mcpJson(await sendCommand("search_components", params));
|
|
1096
|
+
} catch (e) {
|
|
1097
|
+
return mcpError("Error searching components", e);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
);
|
|
1101
|
+
server2.tool(
|
|
1102
|
+
"get_component_by_id",
|
|
1103
|
+
"Get detailed component info including property definitions and variants.",
|
|
1104
|
+
{
|
|
1105
|
+
componentId: import_zod15.z.string().describe("Component node ID"),
|
|
1106
|
+
includeChildren: flexBool(import_zod15.z.boolean()).optional().describe("For COMPONENT_SETs: include variant children (default false)")
|
|
1107
|
+
},
|
|
1108
|
+
async (params) => {
|
|
1109
|
+
try {
|
|
1110
|
+
return mcpJson(await sendCommand("get_component_by_id", params));
|
|
1111
|
+
} catch (e) {
|
|
1112
|
+
return mcpError("Error getting component", e);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
);
|
|
1116
|
+
server2.tool(
|
|
1117
|
+
"get_instance_overrides",
|
|
1118
|
+
"Get override properties from a component instance.",
|
|
1119
|
+
{ nodeId: import_zod15.z.string().optional().describe("Instance node ID (uses selection if omitted)") },
|
|
1120
|
+
async ({ nodeId: nodeId2 }) => {
|
|
1121
|
+
try {
|
|
1122
|
+
return mcpJson(await sendCommand("get_instance_overrides", { instanceNodeId: nodeId2 || null }));
|
|
1123
|
+
} catch (e) {
|
|
1124
|
+
return mcpError("Error getting overrides", e);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// src/tools/styles.ts
|
|
1131
|
+
var import_zod16 = require("zod");
|
|
1132
|
+
init_helpers();
|
|
1133
|
+
var paintStyleItem = import_zod16.z.object({
|
|
1134
|
+
name: import_zod16.z.string().describe("Style name"),
|
|
1135
|
+
color: flexJson(colorRgba).describe('Color. Hex "#FF0000" or {r,g,b,a?} 0-1.')
|
|
1136
|
+
});
|
|
1137
|
+
var textStyleItem = import_zod16.z.object({
|
|
1138
|
+
name: import_zod16.z.string().describe("Style name"),
|
|
1139
|
+
fontFamily: import_zod16.z.string().describe("Font family"),
|
|
1140
|
+
fontStyle: import_zod16.z.string().optional().describe("Font style (default: Regular)"),
|
|
1141
|
+
fontSize: import_zod16.z.coerce.number().describe("Font size"),
|
|
1142
|
+
lineHeight: flexNum(import_zod16.z.union([
|
|
1143
|
+
import_zod16.z.number(),
|
|
1144
|
+
import_zod16.z.object({ value: import_zod16.z.coerce.number(), unit: import_zod16.z.enum(["PIXELS", "PERCENT", "AUTO"]) })
|
|
1145
|
+
])).optional().describe("Line height \u2014 number (px) or {value, unit}. Default: auto."),
|
|
1146
|
+
letterSpacing: flexNum(import_zod16.z.union([
|
|
1147
|
+
import_zod16.z.number(),
|
|
1148
|
+
import_zod16.z.object({ value: import_zod16.z.coerce.number(), unit: import_zod16.z.enum(["PIXELS", "PERCENT"]) })
|
|
1149
|
+
])).optional().describe("Letter spacing \u2014 number (px) or {value, unit}. Default: 0."),
|
|
1150
|
+
textCase: import_zod16.z.enum(["ORIGINAL", "UPPER", "LOWER", "TITLE"]).optional(),
|
|
1151
|
+
textDecoration: import_zod16.z.enum(["NONE", "UNDERLINE", "STRIKETHROUGH"]).optional()
|
|
1152
|
+
});
|
|
1153
|
+
var effectStyleItem = import_zod16.z.object({
|
|
1154
|
+
name: import_zod16.z.string().describe("Style name"),
|
|
1155
|
+
effects: flexJson(import_zod16.z.array(effectEntry)).describe("Array of effects")
|
|
1156
|
+
});
|
|
1157
|
+
var applyStyleItem = import_zod16.z.object({
|
|
1158
|
+
nodeId,
|
|
1159
|
+
styleId: import_zod16.z.string().optional().describe("Style ID. Provide either styleId or styleName."),
|
|
1160
|
+
styleName: import_zod16.z.string().optional().describe("Style name (case-insensitive substring match). Provide either styleId or styleName."),
|
|
1161
|
+
styleType: import_zod16.z.preprocess((v) => typeof v === "string" ? v.toLowerCase() : v, import_zod16.z.enum(["fill", "stroke", "text", "effect"])).describe("Type of style: fill, stroke, text, or effect (case-insensitive)")
|
|
1162
|
+
});
|
|
1163
|
+
function registerMcpTools14(server2, sendCommand) {
|
|
1164
|
+
server2.tool(
|
|
1165
|
+
"get_styles",
|
|
1166
|
+
"List local styles (paint, text, effect, grid). Returns IDs and names only.",
|
|
1167
|
+
{},
|
|
1168
|
+
async () => {
|
|
1169
|
+
try {
|
|
1170
|
+
return mcpJson(await sendCommand("get_styles"));
|
|
1171
|
+
} catch (e) {
|
|
1172
|
+
return mcpError("Error getting styles", e);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
);
|
|
1176
|
+
server2.tool(
|
|
1177
|
+
"get_style_by_id",
|
|
1178
|
+
"Get detailed style info by ID. Returns full paint/font/effect/grid details.",
|
|
1179
|
+
{ styleId: import_zod16.z.string().describe("Style ID") },
|
|
1180
|
+
async ({ styleId }) => {
|
|
1181
|
+
try {
|
|
1182
|
+
return mcpJson(await sendCommand("get_style_by_id", { styleId }));
|
|
1183
|
+
} catch (e) {
|
|
1184
|
+
return mcpError("Error getting style", e);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
);
|
|
1188
|
+
server2.tool(
|
|
1189
|
+
"remove_style",
|
|
1190
|
+
"Delete a style by ID.",
|
|
1191
|
+
{ styleId: import_zod16.z.string().describe("Style ID to remove") },
|
|
1192
|
+
async ({ styleId }) => {
|
|
1193
|
+
try {
|
|
1194
|
+
return mcpJson(await sendCommand("remove_style", { styleId }));
|
|
1195
|
+
} catch (e) {
|
|
1196
|
+
return mcpError("Error removing style", e);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
);
|
|
1200
|
+
server2.tool(
|
|
1201
|
+
"create_paint_style",
|
|
1202
|
+
"Create color/paint styles. Batch: pass multiple items.",
|
|
1203
|
+
{ items: flexJson(import_zod16.z.array(paintStyleItem)).describe("Array of {name, color}") },
|
|
1204
|
+
async (params) => {
|
|
1205
|
+
try {
|
|
1206
|
+
return mcpJson(await sendCommand("create_paint_style", params));
|
|
1207
|
+
} catch (e) {
|
|
1208
|
+
return mcpError("Error creating paint style", e);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
);
|
|
1212
|
+
server2.tool(
|
|
1213
|
+
"create_text_style",
|
|
1214
|
+
"Create text styles. Batch: pass multiple items.",
|
|
1215
|
+
{ items: flexJson(import_zod16.z.array(textStyleItem)).describe("Array of text style definitions") },
|
|
1216
|
+
async (params) => {
|
|
1217
|
+
try {
|
|
1218
|
+
return mcpJson(await sendCommand("create_text_style", params));
|
|
1219
|
+
} catch (e) {
|
|
1220
|
+
return mcpError("Error creating text style", e);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
);
|
|
1224
|
+
server2.tool(
|
|
1225
|
+
"create_effect_style",
|
|
1226
|
+
"Create effect styles (shadows, blurs). Batch: pass multiple items.",
|
|
1227
|
+
{ items: flexJson(import_zod16.z.array(effectStyleItem)).describe("Array of {name, effects}") },
|
|
1228
|
+
async (params) => {
|
|
1229
|
+
try {
|
|
1230
|
+
return mcpJson(await sendCommand("create_effect_style", params));
|
|
1231
|
+
} catch (e) {
|
|
1232
|
+
return mcpError("Error creating effect style", e);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
);
|
|
1236
|
+
server2.tool(
|
|
1237
|
+
"apply_style_to_node",
|
|
1238
|
+
"Apply a style to nodes by ID or name. Use styleName for convenience (case-insensitive). Batch: pass multiple items.",
|
|
1239
|
+
{ items: flexJson(import_zod16.z.array(applyStyleItem)).describe("Array of {nodeId, styleId?, styleName?, styleType}"), depth },
|
|
1240
|
+
async (params) => {
|
|
1241
|
+
try {
|
|
1242
|
+
return mcpJson(await sendCommand("apply_style_to_node", params));
|
|
1243
|
+
} catch (e) {
|
|
1244
|
+
return mcpError("Error applying style", e);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// src/tools/variables.ts
|
|
1251
|
+
var import_zod17 = require("zod");
|
|
1252
|
+
init_helpers();
|
|
1253
|
+
var collectionItem = import_zod17.z.object({
|
|
1254
|
+
name: import_zod17.z.string().describe("Collection name")
|
|
1255
|
+
});
|
|
1256
|
+
var variableItem = import_zod17.z.object({
|
|
1257
|
+
collectionId: import_zod17.z.string().describe("Variable collection ID"),
|
|
1258
|
+
name: import_zod17.z.string().describe("Variable name"),
|
|
1259
|
+
resolvedType: import_zod17.z.enum(["COLOR", "FLOAT", "STRING", "BOOLEAN"]).describe("Variable type")
|
|
1260
|
+
});
|
|
1261
|
+
var setValueItem = import_zod17.z.object({
|
|
1262
|
+
variableId: import_zod17.z.string().describe("Variable ID (use full ID from create_variable response, e.g. VariableID:1:6)"),
|
|
1263
|
+
modeId: import_zod17.z.string().describe("Mode ID"),
|
|
1264
|
+
value: flexJson(import_zod17.z.union([
|
|
1265
|
+
import_zod17.z.number(),
|
|
1266
|
+
import_zod17.z.string(),
|
|
1267
|
+
import_zod17.z.boolean(),
|
|
1268
|
+
import_zod17.z.object({ r: import_zod17.z.coerce.number(), g: import_zod17.z.coerce.number(), b: import_zod17.z.coerce.number(), a: import_zod17.z.coerce.number().optional() })
|
|
1269
|
+
])).describe("Value: number, string, boolean, or {r,g,b,a} color")
|
|
1270
|
+
});
|
|
1271
|
+
var bindingItem = import_zod17.z.object({
|
|
1272
|
+
nodeId: import_zod17.z.string().describe("Node ID"),
|
|
1273
|
+
field: import_zod17.z.string().describe("Property field (e.g., 'opacity', 'fills/0/color')"),
|
|
1274
|
+
variableId: import_zod17.z.string().describe("Variable ID (use full ID from create_variable response, e.g. VariableID:1:6)")
|
|
1275
|
+
});
|
|
1276
|
+
var addModeItem = import_zod17.z.object({
|
|
1277
|
+
collectionId: import_zod17.z.string().describe("Collection ID"),
|
|
1278
|
+
name: import_zod17.z.string().describe("Mode name")
|
|
1279
|
+
});
|
|
1280
|
+
var renameModeItem = import_zod17.z.object({
|
|
1281
|
+
collectionId: import_zod17.z.string().describe("Collection ID"),
|
|
1282
|
+
modeId: import_zod17.z.string().describe("Mode ID"),
|
|
1283
|
+
name: import_zod17.z.string().describe("New name")
|
|
1284
|
+
});
|
|
1285
|
+
var removeModeItem = import_zod17.z.object({
|
|
1286
|
+
collectionId: import_zod17.z.string().describe("Collection ID"),
|
|
1287
|
+
modeId: import_zod17.z.string().describe("Mode ID")
|
|
1288
|
+
});
|
|
1289
|
+
var setExplicitModeItem = import_zod17.z.object({
|
|
1290
|
+
nodeId,
|
|
1291
|
+
collectionId: import_zod17.z.string().describe("Variable collection ID"),
|
|
1292
|
+
modeId: import_zod17.z.string().describe("Mode ID to pin (e.g. Dark mode)")
|
|
1293
|
+
});
|
|
1294
|
+
function registerMcpTools15(server2, sendCommand) {
|
|
1295
|
+
server2.tool(
|
|
1296
|
+
"create_variable_collection",
|
|
1297
|
+
"Create variable collections. Batch: pass multiple items.",
|
|
1298
|
+
{ items: flexJson(import_zod17.z.array(collectionItem)).describe("Array of {name}") },
|
|
1299
|
+
async ({ items }) => {
|
|
1300
|
+
try {
|
|
1301
|
+
return mcpJson(await sendCommand("create_variable_collection", { items }));
|
|
1302
|
+
} catch (e) {
|
|
1303
|
+
return mcpError("Error creating variable collection", e);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
);
|
|
1307
|
+
server2.tool(
|
|
1308
|
+
"create_variable",
|
|
1309
|
+
"Create variables in a collection. Batch: pass multiple items.",
|
|
1310
|
+
{ items: flexJson(import_zod17.z.array(variableItem)).describe("Array of {collectionId, name, resolvedType}") },
|
|
1311
|
+
async ({ items }) => {
|
|
1312
|
+
try {
|
|
1313
|
+
return mcpJson(await sendCommand("create_variable", { items }));
|
|
1314
|
+
} catch (e) {
|
|
1315
|
+
return mcpError("Error creating variable", e);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
);
|
|
1319
|
+
server2.tool(
|
|
1320
|
+
"set_variable_value",
|
|
1321
|
+
"Set variable values for modes. Batch: pass multiple items.",
|
|
1322
|
+
{ items: flexJson(import_zod17.z.array(setValueItem)).describe("Array of {variableId, modeId, value}") },
|
|
1323
|
+
async ({ items }) => {
|
|
1324
|
+
try {
|
|
1325
|
+
return mcpJson(await sendCommand("set_variable_value", { items }));
|
|
1326
|
+
} catch (e) {
|
|
1327
|
+
return mcpError("Error setting variable value", e);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
);
|
|
1331
|
+
server2.tool(
|
|
1332
|
+
"get_local_variables",
|
|
1333
|
+
"List local variables. Pass includeValues:true to get all mode values in bulk (avoids N separate get_variable_by_id calls).",
|
|
1334
|
+
{
|
|
1335
|
+
type: import_zod17.z.enum(["COLOR", "FLOAT", "STRING", "BOOLEAN"]).optional().describe("Filter by type"),
|
|
1336
|
+
collectionId: import_zod17.z.string().optional().describe("Filter by collection. Omit for all collections."),
|
|
1337
|
+
includeValues: flexBool(import_zod17.z.boolean()).optional().describe("Include valuesByMode for each variable (default: false)")
|
|
1338
|
+
},
|
|
1339
|
+
async (params) => {
|
|
1340
|
+
try {
|
|
1341
|
+
return mcpJson(await sendCommand("get_local_variables", params));
|
|
1342
|
+
} catch (e) {
|
|
1343
|
+
return mcpError("Error getting variables", e);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
);
|
|
1347
|
+
server2.tool(
|
|
1348
|
+
"get_local_variable_collections",
|
|
1349
|
+
"List all local variable collections.",
|
|
1350
|
+
{},
|
|
1351
|
+
async () => {
|
|
1352
|
+
try {
|
|
1353
|
+
return mcpJson(await sendCommand("get_local_variable_collections"));
|
|
1354
|
+
} catch (e) {
|
|
1355
|
+
return mcpError("Error getting variable collections", e);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
);
|
|
1359
|
+
server2.tool(
|
|
1360
|
+
"get_variable_by_id",
|
|
1361
|
+
"Get detailed variable info including all mode values.",
|
|
1362
|
+
{ variableId: import_zod17.z.string().describe("Variable ID") },
|
|
1363
|
+
async ({ variableId }) => {
|
|
1364
|
+
try {
|
|
1365
|
+
return mcpJson(await sendCommand("get_variable_by_id", { variableId }));
|
|
1366
|
+
} catch (e) {
|
|
1367
|
+
return mcpError("Error getting variable", e);
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
);
|
|
1371
|
+
server2.tool(
|
|
1372
|
+
"get_variable_collection_by_id",
|
|
1373
|
+
"Get detailed variable collection info including modes and variable IDs.",
|
|
1374
|
+
{ collectionId: import_zod17.z.string().describe("Collection ID") },
|
|
1375
|
+
async ({ collectionId }) => {
|
|
1376
|
+
try {
|
|
1377
|
+
return mcpJson(await sendCommand("get_variable_collection_by_id", { collectionId }));
|
|
1378
|
+
} catch (e) {
|
|
1379
|
+
return mcpError("Error getting variable collection", e);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
);
|
|
1383
|
+
server2.tool(
|
|
1384
|
+
"set_variable_binding",
|
|
1385
|
+
"Bind variables to node properties. Common fields: 'fills/0/color', 'strokes/0/color', 'opacity', 'topLeftRadius', 'itemSpacing'. Batch: pass multiple items.",
|
|
1386
|
+
{ items: flexJson(import_zod17.z.array(bindingItem)).describe("Array of {nodeId, field, variableId}") },
|
|
1387
|
+
async ({ items }) => {
|
|
1388
|
+
try {
|
|
1389
|
+
return mcpJson(await sendCommand("set_variable_binding", { items }));
|
|
1390
|
+
} catch (e) {
|
|
1391
|
+
return mcpError("Error binding variable", e);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
);
|
|
1395
|
+
server2.tool(
|
|
1396
|
+
"add_mode",
|
|
1397
|
+
"Add modes to variable collections. Batch: pass multiple items.",
|
|
1398
|
+
{ items: flexJson(import_zod17.z.array(addModeItem)).describe("Array of {collectionId, name}") },
|
|
1399
|
+
async ({ items }) => {
|
|
1400
|
+
try {
|
|
1401
|
+
return mcpJson(await sendCommand("add_mode", { items }));
|
|
1402
|
+
} catch (e) {
|
|
1403
|
+
return mcpError("Error adding mode", e);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
);
|
|
1407
|
+
server2.tool(
|
|
1408
|
+
"rename_mode",
|
|
1409
|
+
"Rename modes in variable collections. Batch: pass multiple items.",
|
|
1410
|
+
{ items: flexJson(import_zod17.z.array(renameModeItem)).describe("Array of {collectionId, modeId, name}") },
|
|
1411
|
+
async ({ items }) => {
|
|
1412
|
+
try {
|
|
1413
|
+
return mcpJson(await sendCommand("rename_mode", { items }));
|
|
1414
|
+
} catch (e) {
|
|
1415
|
+
return mcpError("Error renaming mode", e);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
);
|
|
1419
|
+
server2.tool(
|
|
1420
|
+
"remove_mode",
|
|
1421
|
+
"Remove modes from variable collections. Batch: pass multiple items.",
|
|
1422
|
+
{ items: flexJson(import_zod17.z.array(removeModeItem)).describe("Array of {collectionId, modeId}") },
|
|
1423
|
+
async ({ items }) => {
|
|
1424
|
+
try {
|
|
1425
|
+
return mcpJson(await sendCommand("remove_mode", { items }));
|
|
1426
|
+
} catch (e) {
|
|
1427
|
+
return mcpError("Error removing mode", e);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
);
|
|
1431
|
+
server2.tool(
|
|
1432
|
+
"set_explicit_variable_mode",
|
|
1433
|
+
"Pin a variable collection mode on a frame (e.g. show Dark mode). Batch: pass multiple items.",
|
|
1434
|
+
{ items: flexJson(import_zod17.z.array(setExplicitModeItem)).describe("Array of {nodeId, collectionId, modeId}") },
|
|
1435
|
+
async ({ items }) => {
|
|
1436
|
+
try {
|
|
1437
|
+
return mcpJson(await sendCommand("set_explicit_variable_mode", { items }));
|
|
1438
|
+
} catch (e) {
|
|
1439
|
+
return mcpError("Error setting variable mode", e);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
);
|
|
1443
|
+
server2.tool(
|
|
1444
|
+
"get_node_variables",
|
|
1445
|
+
"Get variable bindings on a node. Returns which variables are bound to fills, strokes, opacity, corner radius, etc.",
|
|
1446
|
+
{ nodeId },
|
|
1447
|
+
async ({ nodeId: nodeId2 }) => {
|
|
1448
|
+
try {
|
|
1449
|
+
return mcpJson(await sendCommand("get_node_variables", { nodeId: nodeId2 }));
|
|
1450
|
+
} catch (e) {
|
|
1451
|
+
return mcpError("Error getting node variables", e);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// src/tools/lint.ts
|
|
1458
|
+
var import_zod18 = require("zod");
|
|
1459
|
+
init_helpers();
|
|
1460
|
+
var lintRules = import_zod18.z.enum([
|
|
1461
|
+
"no-autolayout",
|
|
1462
|
+
// Frames with >1 child and no auto-layout
|
|
1463
|
+
"shape-instead-of-frame",
|
|
1464
|
+
// Shapes used where FRAME should be
|
|
1465
|
+
"hardcoded-color",
|
|
1466
|
+
// Fills/strokes not using styles
|
|
1467
|
+
"no-text-style",
|
|
1468
|
+
// Text nodes without text style
|
|
1469
|
+
"fixed-in-autolayout",
|
|
1470
|
+
// Fixed-size children in auto-layout parents
|
|
1471
|
+
"default-name",
|
|
1472
|
+
// Nodes with default/unnamed names
|
|
1473
|
+
"empty-container",
|
|
1474
|
+
// Frames/components with layout but no children
|
|
1475
|
+
"stale-text-name",
|
|
1476
|
+
// Text nodes where layer name diverges from content
|
|
1477
|
+
"all"
|
|
1478
|
+
// Run all rules
|
|
1479
|
+
]);
|
|
1480
|
+
function registerMcpTools16(server2, sendCommand) {
|
|
1481
|
+
server2.tool(
|
|
1482
|
+
"lint_node",
|
|
1483
|
+
"Run design linter on a node tree. Returns issues grouped by category with affected node IDs and fix instructions. Lint child nodes individually for large trees.",
|
|
1484
|
+
{
|
|
1485
|
+
nodeId: import_zod18.z.string().optional().describe("Node ID to lint. Omit to lint current selection."),
|
|
1486
|
+
rules: flexJson(import_zod18.z.array(lintRules)).optional().describe('Rules to run. Default: ["all"]. Options: no-autolayout, shape-instead-of-frame, hardcoded-color, no-text-style, fixed-in-autolayout, default-name, empty-container, stale-text-name, all'),
|
|
1487
|
+
maxDepth: import_zod18.z.coerce.number().optional().describe("Max depth to recurse (default: 10)"),
|
|
1488
|
+
maxFindings: import_zod18.z.coerce.number().optional().describe("Stop after N findings (default: 50)")
|
|
1489
|
+
},
|
|
1490
|
+
async (params) => {
|
|
1491
|
+
try {
|
|
1492
|
+
return mcpJson(await sendCommand("lint_node", params));
|
|
1493
|
+
} catch (e) {
|
|
1494
|
+
return mcpError("Error running lint", e);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
);
|
|
1498
|
+
server2.tool(
|
|
1499
|
+
"lint_fix_autolayout",
|
|
1500
|
+
"Auto-fix: convert frames with multiple children to auto-layout. Takes node IDs from lint_node 'no-autolayout' results.",
|
|
1501
|
+
{
|
|
1502
|
+
items: flexJson(import_zod18.z.array(import_zod18.z.object({
|
|
1503
|
+
nodeId,
|
|
1504
|
+
layoutMode: import_zod18.z.enum(["HORIZONTAL", "VERTICAL"]).optional().describe("Layout direction (default: auto-detect based on child positions)"),
|
|
1505
|
+
itemSpacing: import_zod18.z.coerce.number().optional().describe("Spacing between children (default: 0)")
|
|
1506
|
+
}))).describe("Array of frames to convert to auto-layout"),
|
|
1507
|
+
depth
|
|
1508
|
+
},
|
|
1509
|
+
async (params) => {
|
|
1510
|
+
try {
|
|
1511
|
+
return mcpJson(await sendCommand("lint_fix_autolayout", params));
|
|
1512
|
+
} catch (e) {
|
|
1513
|
+
return mcpError("Error fixing auto-layout", e);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
);
|
|
1517
|
+
server2.tool(
|
|
1518
|
+
"lint_fix_replace_shape_with_frame",
|
|
1519
|
+
"Auto-fix: replace shapes with frames preserving visual properties. Overlapping siblings are re-parented into the new frame. Use after lint_node 'shape-instead-of-frame' results.",
|
|
1520
|
+
{
|
|
1521
|
+
items: flexJson(import_zod18.z.array(import_zod18.z.object({
|
|
1522
|
+
nodeId,
|
|
1523
|
+
adoptChildren: flexBool(import_zod18.z.boolean()).optional().describe("Re-parent overlapping siblings into the new frame (default: true)")
|
|
1524
|
+
}))).describe("Array of shapes to convert to frames"),
|
|
1525
|
+
depth
|
|
1526
|
+
},
|
|
1527
|
+
async (params) => {
|
|
1528
|
+
try {
|
|
1529
|
+
return mcpJson(await sendCommand("lint_fix_replace_shape_with_frame", params));
|
|
1530
|
+
} catch (e) {
|
|
1531
|
+
return mcpError("Error converting shapes to frames", e);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// src/tools/connection.ts
|
|
1538
|
+
function registerMcpTools17(server2, sendCommand) {
|
|
1539
|
+
server2.tool(
|
|
1540
|
+
"ping",
|
|
1541
|
+
"Verify end-to-end connection to Figma. Call this right after join_channel. Returns { status: 'pong', documentName, currentPage } if the full chain (MCP \u2192 relay \u2192 plugin \u2192 Figma) is working. If this times out, the Figma plugin is not connected \u2014 ask the user to check the plugin window for the correct port and channel name.",
|
|
1542
|
+
{},
|
|
1543
|
+
async () => {
|
|
1544
|
+
try {
|
|
1545
|
+
return mcpJson(await sendCommand("ping", {}, 5e3));
|
|
1546
|
+
} catch (e) {
|
|
1547
|
+
return mcpError("Connection verification failed", e);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// src/tools/prompts.ts
|
|
1554
|
+
function registerPrompts(server2) {
|
|
1555
|
+
server2.prompt(
|
|
1556
|
+
"design_strategy",
|
|
1557
|
+
"Best practices for working with Figma designs",
|
|
1558
|
+
() => ({
|
|
1559
|
+
messages: [{
|
|
1560
|
+
role: "assistant",
|
|
1561
|
+
content: {
|
|
1562
|
+
type: "text",
|
|
1563
|
+
text: `When working with Figma designs, follow these best practices:
|
|
1564
|
+
|
|
1565
|
+
1. Understand Before Creating:
|
|
1566
|
+
- Use get_document_info() to see pages and current page
|
|
1567
|
+
- Use get_styles() and get_local_variables() to discover existing design tokens
|
|
1568
|
+
- Plan layout hierarchy before creating elements
|
|
1569
|
+
|
|
1570
|
+
2. Use Design Tokens \u2014 Never Hardcode:
|
|
1571
|
+
- Colors: use fillStyleName/strokeStyleName (paint styles) or fillVariableId/strokeVariableId (variables)
|
|
1572
|
+
- Text: use textStyleName to apply text styles that control font size, weight, and line height together
|
|
1573
|
+
- Effects: use effectStyleName to apply shadow/blur styles
|
|
1574
|
+
- Only use raw fillColor/fontColor for one-off values not in the design system
|
|
1575
|
+
|
|
1576
|
+
3. Auto-Layout First:
|
|
1577
|
+
- Use create_frame() with layoutMode: "VERTICAL" or "HORIZONTAL" for every container
|
|
1578
|
+
- Set itemSpacing, padding, and alignment at creation time
|
|
1579
|
+
- Use layoutSizingHorizontal/Vertical: "FILL" for responsive children
|
|
1580
|
+
- Avoid absolute positioning \u2014 let auto-layout handle spacing
|
|
1581
|
+
|
|
1582
|
+
4. Naming Conventions:
|
|
1583
|
+
- Use descriptive, semantic names for all elements
|
|
1584
|
+
- Name components with Property=Value pattern (e.g. "Size=Small") before combine_as_variants
|
|
1585
|
+
|
|
1586
|
+
5. Variable Modes:
|
|
1587
|
+
- Use set_explicit_variable_mode() to pin a frame to a specific mode (e.g. Dark)
|
|
1588
|
+
- Use get_node_variables() to verify which variables are bound to a node
|
|
1589
|
+
|
|
1590
|
+
6. Quality Check \u2014 Run Lint:
|
|
1591
|
+
- After building a section, run lint_node() to catch common issues:
|
|
1592
|
+
* hardcoded-color: fills/strokes not using styles or variables
|
|
1593
|
+
* no-text-style: text without a text style applied
|
|
1594
|
+
* no-autolayout: frames with children but no auto-layout
|
|
1595
|
+
* default-name: nodes still named "Frame", "Rectangle", etc.
|
|
1596
|
+
- Use lint_fix_autolayout() and lint_fix_replace_shape_with_frame() to auto-fix
|
|
1597
|
+
- Lint early and often \u2014 it is cheaper to fix issues during creation than after`
|
|
1598
|
+
}
|
|
1599
|
+
}],
|
|
1600
|
+
description: "Best practices for working with Figma designs"
|
|
1601
|
+
})
|
|
1602
|
+
);
|
|
1603
|
+
server2.prompt(
|
|
1604
|
+
"read_design_strategy",
|
|
1605
|
+
"Best practices for reading Figma designs",
|
|
1606
|
+
() => ({
|
|
1607
|
+
messages: [{
|
|
1608
|
+
role: "assistant",
|
|
1609
|
+
content: {
|
|
1610
|
+
type: "text",
|
|
1611
|
+
text: `When reading Figma designs, follow these best practices:
|
|
1612
|
+
|
|
1613
|
+
1. Start with selection:
|
|
1614
|
+
- First use read_my_design() to understand the current selection
|
|
1615
|
+
- If no selection ask user to select single or multiple nodes
|
|
1616
|
+
`
|
|
1617
|
+
}
|
|
1618
|
+
}],
|
|
1619
|
+
description: "Best practices for reading Figma designs"
|
|
1620
|
+
})
|
|
1621
|
+
);
|
|
1622
|
+
server2.prompt(
|
|
1623
|
+
"text_replacement_strategy",
|
|
1624
|
+
"Systematic approach for replacing text in Figma designs",
|
|
1625
|
+
() => ({
|
|
1626
|
+
messages: [{
|
|
1627
|
+
role: "assistant",
|
|
1628
|
+
content: {
|
|
1629
|
+
type: "text",
|
|
1630
|
+
text: `# Intelligent Text Replacement Strategy
|
|
1631
|
+
|
|
1632
|
+
## 1. Analyze Design & Identify Structure
|
|
1633
|
+
- Scan text nodes to understand the overall structure of the design
|
|
1634
|
+
- Use AI pattern recognition to identify logical groupings:
|
|
1635
|
+
* Tables (rows, columns, headers, cells)
|
|
1636
|
+
* Lists (items, headers, nested lists)
|
|
1637
|
+
* Card groups (similar cards with recurring text fields)
|
|
1638
|
+
* Forms (labels, input fields, validation text)
|
|
1639
|
+
* Navigation (menu items, breadcrumbs)
|
|
1640
|
+
\`\`\`
|
|
1641
|
+
scan_text_nodes(nodeId: "node-id")
|
|
1642
|
+
get_node_info(nodeId: "node-id") // optional
|
|
1643
|
+
\`\`\`
|
|
1644
|
+
|
|
1645
|
+
## 2. Strategic Chunking for Complex Designs
|
|
1646
|
+
- Divide replacement tasks into logical content chunks based on design structure
|
|
1647
|
+
- Use one of these chunking strategies that best fits the design:
|
|
1648
|
+
* **Structural Chunking**: Table rows/columns, list sections, card groups
|
|
1649
|
+
* **Spatial Chunking**: Top-to-bottom, left-to-right in screen areas
|
|
1650
|
+
* **Semantic Chunking**: Content related to the same topic or functionality
|
|
1651
|
+
* **Component-Based Chunking**: Process similar component instances together
|
|
1652
|
+
|
|
1653
|
+
## 3. Progressive Replacement with Verification
|
|
1654
|
+
- Create a safe copy of the node for text replacement
|
|
1655
|
+
- Replace text chunk by chunk with continuous progress updates
|
|
1656
|
+
- After each chunk is processed:
|
|
1657
|
+
* Export that section as a small, manageable image
|
|
1658
|
+
* Verify text fits properly and maintain design integrity
|
|
1659
|
+
* Fix issues before proceeding to the next chunk
|
|
1660
|
+
|
|
1661
|
+
\`\`\`
|
|
1662
|
+
// Clone the node to create a safe copy
|
|
1663
|
+
clone_node(nodeId: "selected-node-id", x: [new-x], y: [new-y])
|
|
1664
|
+
|
|
1665
|
+
// Replace text chunk by chunk
|
|
1666
|
+
set_text_content(
|
|
1667
|
+
items: [
|
|
1668
|
+
{ nodeId: "node-id-1", text: "New text 1" },
|
|
1669
|
+
// More nodes in this chunk...
|
|
1670
|
+
]
|
|
1671
|
+
)
|
|
1672
|
+
|
|
1673
|
+
// Verify chunk with small, targeted image exports
|
|
1674
|
+
export_node_as_image(nodeId: "chunk-node-id", format: "PNG", scale: 0.5)
|
|
1675
|
+
\`\`\`
|
|
1676
|
+
|
|
1677
|
+
## 4. Intelligent Handling for Table Data
|
|
1678
|
+
- For tabular content:
|
|
1679
|
+
* Process one row or column at a time
|
|
1680
|
+
* Maintain alignment and spacing between cells
|
|
1681
|
+
* Consider conditional formatting based on cell content
|
|
1682
|
+
* Preserve header/data relationships
|
|
1683
|
+
|
|
1684
|
+
## 5. Smart Text Adaptation
|
|
1685
|
+
- Adaptively handle text based on container constraints:
|
|
1686
|
+
* Auto-detect space constraints and adjust text length
|
|
1687
|
+
* Apply line breaks at appropriate linguistic points
|
|
1688
|
+
* Maintain text hierarchy and emphasis
|
|
1689
|
+
* Consider font scaling for critical content that must fit
|
|
1690
|
+
|
|
1691
|
+
## 6. Progressive Feedback Loop
|
|
1692
|
+
- Establish a continuous feedback loop during replacement:
|
|
1693
|
+
* Real-time progress updates (0-100%)
|
|
1694
|
+
* Small image exports after each chunk for verification
|
|
1695
|
+
* Issues identified early and resolved incrementally
|
|
1696
|
+
* Quick adjustments applied to subsequent chunks
|
|
1697
|
+
|
|
1698
|
+
## 7. Final Verification & Context-Aware QA
|
|
1699
|
+
- After all chunks are processed:
|
|
1700
|
+
* Export the entire design at reduced scale for final verification
|
|
1701
|
+
* Check for cross-chunk consistency issues
|
|
1702
|
+
* Verify proper text flow between different sections
|
|
1703
|
+
* Ensure design harmony across the full composition
|
|
1704
|
+
|
|
1705
|
+
## 8. Chunk-Specific Export Scale Guidelines
|
|
1706
|
+
- Scale exports appropriately based on chunk size:
|
|
1707
|
+
* Small chunks (1-5 elements): scale 1.0
|
|
1708
|
+
* Medium chunks (6-20 elements): scale 0.7
|
|
1709
|
+
* Large chunks (21-50 elements): scale 0.5
|
|
1710
|
+
* Very large chunks (50+ elements): scale 0.3
|
|
1711
|
+
* Full design verification: scale 0.2
|
|
1712
|
+
|
|
1713
|
+
## Sample Chunking Strategy for Common Design Types
|
|
1714
|
+
|
|
1715
|
+
### Tables
|
|
1716
|
+
- Process by logical rows (5-10 rows per chunk)
|
|
1717
|
+
- Alternative: Process by column for columnar analysis
|
|
1718
|
+
- Tip: Always include header row in first chunk for reference
|
|
1719
|
+
|
|
1720
|
+
### Card Lists
|
|
1721
|
+
- Group 3-5 similar cards per chunk
|
|
1722
|
+
- Process entire cards to maintain internal consistency
|
|
1723
|
+
- Verify text-to-image ratio within cards after each chunk
|
|
1724
|
+
|
|
1725
|
+
### Forms
|
|
1726
|
+
- Group related fields (e.g., "Personal Information", "Payment Details")
|
|
1727
|
+
- Process labels and input fields together
|
|
1728
|
+
- Ensure validation messages and hints are updated with their fields
|
|
1729
|
+
|
|
1730
|
+
### Navigation & Menus
|
|
1731
|
+
- Process hierarchical levels together (main menu, submenu)
|
|
1732
|
+
- Respect information architecture relationships
|
|
1733
|
+
- Verify menu fit and alignment after replacement
|
|
1734
|
+
|
|
1735
|
+
## Best Practices
|
|
1736
|
+
- **Preserve Design Intent**: Always prioritize design integrity
|
|
1737
|
+
- **Structural Consistency**: Maintain alignment, spacing, and hierarchy
|
|
1738
|
+
- **Visual Feedback**: Verify each chunk visually before proceeding
|
|
1739
|
+
- **Incremental Improvement**: Learn from each chunk to improve subsequent ones
|
|
1740
|
+
- **Balance Automation & Control**: Let AI handle repetitive replacements but maintain oversight
|
|
1741
|
+
- **Respect Content Relationships**: Keep related content consistent across chunks
|
|
1742
|
+
|
|
1743
|
+
Remember that text is never just text\u2014it's a core design element that must work harmoniously with the overall composition. This chunk-based strategy allows you to methodically transform text while maintaining design integrity.`
|
|
1744
|
+
}
|
|
1745
|
+
}],
|
|
1746
|
+
description: "Systematic approach for replacing text in Figma designs"
|
|
1747
|
+
})
|
|
1748
|
+
);
|
|
1749
|
+
server2.prompt(
|
|
1750
|
+
"swap_overrides_instances",
|
|
1751
|
+
"Guide to swap instance overrides between instances",
|
|
1752
|
+
() => ({
|
|
1753
|
+
messages: [{
|
|
1754
|
+
role: "assistant",
|
|
1755
|
+
content: {
|
|
1756
|
+
type: "text",
|
|
1757
|
+
text: `# Swap Component Instance Overrides
|
|
1758
|
+
|
|
1759
|
+
## Overview
|
|
1760
|
+
Transfer content overrides from a source instance to target instances.
|
|
1761
|
+
|
|
1762
|
+
## Process
|
|
1763
|
+
|
|
1764
|
+
### 1. Identify Instances
|
|
1765
|
+
- Use \`get_selection()\` to identify selected instances
|
|
1766
|
+
- Use \`search_nodes(types: ["INSTANCE"])\` to find instances on the page
|
|
1767
|
+
|
|
1768
|
+
### 2. Extract Source Overrides
|
|
1769
|
+
- \`get_instance_overrides(nodeId: "source-instance-id")\`
|
|
1770
|
+
- Returns mainComponentId and per-child override fields (characters, fills, fontSize, etc.)
|
|
1771
|
+
|
|
1772
|
+
### 3. Apply to Targets
|
|
1773
|
+
- For text overrides: use \`set_text_content\` on matching child node IDs
|
|
1774
|
+
- For style overrides: use \`set_fill_color\`, \`apply_style_to_node\`, etc.
|
|
1775
|
+
- Match children by name path \u2014 source and target instances share the same internal structure
|
|
1776
|
+
|
|
1777
|
+
### 4. Verify
|
|
1778
|
+
- \`get_node_info(nodeId, depth: 1)\` on target instances
|
|
1779
|
+
- \`export_node_as_image\` for visual verification`
|
|
1780
|
+
}
|
|
1781
|
+
}],
|
|
1782
|
+
description: "Strategy for transferring overrides between component instances in Figma"
|
|
1783
|
+
})
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// src/tools/mcp-registry.ts
|
|
1788
|
+
function registerAllTools(server2, sendCommand) {
|
|
1789
|
+
registerMcpTools(server2, sendCommand);
|
|
1790
|
+
registerMcpTools2(server2, sendCommand);
|
|
1791
|
+
registerMcpTools3(server2, sendCommand);
|
|
1792
|
+
registerMcpTools4(server2, sendCommand);
|
|
1793
|
+
registerMcpTools5(server2, sendCommand);
|
|
1794
|
+
registerMcpTools6(server2, sendCommand);
|
|
1795
|
+
registerMcpTools7(server2, sendCommand);
|
|
1796
|
+
registerMcpTools8(server2, sendCommand);
|
|
1797
|
+
registerMcpTools9(server2, sendCommand);
|
|
1798
|
+
registerMcpTools10(server2, sendCommand);
|
|
1799
|
+
registerMcpTools11(server2, sendCommand);
|
|
1800
|
+
registerMcpTools12(server2, sendCommand);
|
|
1801
|
+
registerMcpTools13(server2, sendCommand);
|
|
1802
|
+
registerMcpTools14(server2, sendCommand);
|
|
1803
|
+
registerMcpTools15(server2, sendCommand);
|
|
1804
|
+
registerMcpTools16(server2, sendCommand);
|
|
1805
|
+
registerMcpTools17(server2, sendCommand);
|
|
1806
|
+
registerPrompts(server2);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// src/mcp.ts
|
|
1810
|
+
var logger = {
|
|
1811
|
+
info: (msg) => process.stderr.write(`[INFO] ${msg}
|
|
1812
|
+
`),
|
|
1813
|
+
debug: (msg) => process.stderr.write(`[DEBUG] ${msg}
|
|
1814
|
+
`),
|
|
1815
|
+
warn: (msg) => process.stderr.write(`[WARN] ${msg}
|
|
1816
|
+
`),
|
|
1817
|
+
error: (msg) => process.stderr.write(`[ERROR] ${msg}
|
|
1818
|
+
`),
|
|
1819
|
+
log: (msg) => process.stderr.write(`[LOG] ${msg}
|
|
1820
|
+
`)
|
|
1821
|
+
};
|
|
1822
|
+
var ws = null;
|
|
1823
|
+
var pendingRequests = /* @__PURE__ */ new Map();
|
|
1824
|
+
var currentChannel = null;
|
|
1825
|
+
var activePort = parseInt(process.env.VIBMA_PORT || "3055");
|
|
1826
|
+
var args = process.argv.slice(2);
|
|
1827
|
+
var serverArg = args.find((a) => a.startsWith("--server="));
|
|
1828
|
+
var portArg = args.find((a) => a.startsWith("--port="));
|
|
1829
|
+
var serverUrl = serverArg ? serverArg.split("=")[1] : "localhost";
|
|
1830
|
+
if (portArg) activePort = parseInt(portArg.split("=")[1]);
|
|
1831
|
+
var WS_URL = serverUrl === "localhost" ? `ws://${serverUrl}` : `wss://${serverUrl}`;
|
|
1832
|
+
function connectToFigma(port = activePort) {
|
|
1833
|
+
activePort = port;
|
|
1834
|
+
if (ws && ws.readyState === import_ws.default.OPEN) {
|
|
1835
|
+
logger.info("Already connected to Figma");
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
const wsUrl = serverUrl === "localhost" ? `${WS_URL}:${port}` : WS_URL;
|
|
1839
|
+
logger.info(`Connecting to Figma socket server at ${wsUrl}...`);
|
|
1840
|
+
ws = new import_ws.default(wsUrl);
|
|
1841
|
+
ws.on("open", () => {
|
|
1842
|
+
logger.info("Connected to Figma socket server");
|
|
1843
|
+
currentChannel = null;
|
|
1844
|
+
});
|
|
1845
|
+
ws.on("message", (data) => {
|
|
1846
|
+
try {
|
|
1847
|
+
const json = JSON.parse(data);
|
|
1848
|
+
if (json.type === "progress_update") {
|
|
1849
|
+
const progressData = json.message.data;
|
|
1850
|
+
const requestId = json.id || "";
|
|
1851
|
+
if (requestId && pendingRequests.has(requestId)) {
|
|
1852
|
+
const request = pendingRequests.get(requestId);
|
|
1853
|
+
request.lastActivity = Date.now();
|
|
1854
|
+
clearTimeout(request.timeout);
|
|
1855
|
+
request.timeout = setTimeout(() => {
|
|
1856
|
+
if (pendingRequests.has(requestId)) {
|
|
1857
|
+
logger.error(`Request ${requestId} timed out after extended period of inactivity`);
|
|
1858
|
+
pendingRequests.delete(requestId);
|
|
1859
|
+
request.reject(new Error("Request to Figma timed out"));
|
|
1860
|
+
}
|
|
1861
|
+
}, 6e4);
|
|
1862
|
+
logger.info(`Progress update for ${progressData.commandType}: ${progressData.progress}% - ${progressData.message}`);
|
|
1863
|
+
if (progressData.status === "completed" && progressData.progress === 100) {
|
|
1864
|
+
logger.info(`Operation ${progressData.commandType} completed, waiting for final result`);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
const myResponse = json.message;
|
|
1870
|
+
logger.debug(`Received message: ${JSON.stringify(myResponse)}`);
|
|
1871
|
+
if (myResponse.id && pendingRequests.has(myResponse.id) && myResponse.result) {
|
|
1872
|
+
const request = pendingRequests.get(myResponse.id);
|
|
1873
|
+
clearTimeout(request.timeout);
|
|
1874
|
+
if (myResponse.error) {
|
|
1875
|
+
logger.error(`Error from Figma: ${myResponse.error}`);
|
|
1876
|
+
request.reject(new Error(myResponse.error));
|
|
1877
|
+
} else {
|
|
1878
|
+
request.resolve(myResponse.result);
|
|
1879
|
+
}
|
|
1880
|
+
pendingRequests.delete(myResponse.id);
|
|
1881
|
+
} else {
|
|
1882
|
+
logger.info(`Received broadcast message: ${JSON.stringify(myResponse)}`);
|
|
1883
|
+
}
|
|
1884
|
+
} catch (error) {
|
|
1885
|
+
logger.error(`Error parsing message: ${error instanceof Error ? error.message : String(error)}`);
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
ws.on("error", (error) => {
|
|
1889
|
+
logger.error(`Socket error: ${error}`);
|
|
1890
|
+
});
|
|
1891
|
+
ws.on("close", () => {
|
|
1892
|
+
logger.info("Disconnected from Figma socket server");
|
|
1893
|
+
ws = null;
|
|
1894
|
+
for (const [id, request] of pendingRequests.entries()) {
|
|
1895
|
+
clearTimeout(request.timeout);
|
|
1896
|
+
request.reject(new Error("Connection closed"));
|
|
1897
|
+
pendingRequests.delete(id);
|
|
1898
|
+
}
|
|
1899
|
+
logger.info("Attempting to reconnect in 2 seconds...");
|
|
1900
|
+
setTimeout(() => connectToFigma(port), 2e3);
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
async function joinChannel(channelName) {
|
|
1904
|
+
if (!ws || ws.readyState !== import_ws.default.OPEN) {
|
|
1905
|
+
throw new Error("Not connected to Figma");
|
|
1906
|
+
}
|
|
1907
|
+
try {
|
|
1908
|
+
await sendCommandToFigma("join", { channel: channelName });
|
|
1909
|
+
currentChannel = channelName;
|
|
1910
|
+
logger.info(`Joined channel: ${channelName}`);
|
|
1911
|
+
} catch (error) {
|
|
1912
|
+
logger.error(`Failed to join channel: ${error instanceof Error ? error.message : String(error)}`);
|
|
1913
|
+
throw error;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
function sendCommandToFigma(command, params = {}, timeoutMs = 3e4) {
|
|
1917
|
+
return new Promise((resolve, reject) => {
|
|
1918
|
+
if (!ws || ws.readyState !== import_ws.default.OPEN) {
|
|
1919
|
+
connectToFigma();
|
|
1920
|
+
reject(new Error("Not connected to Figma. Attempting to connect..."));
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
const requiresChannel = command !== "join";
|
|
1924
|
+
if (requiresChannel && !currentChannel) {
|
|
1925
|
+
reject(new Error("No channel joined. Call join_channel first with the channel name shown in the Figma plugin panel."));
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
const id = (0, import_uuid.v4)();
|
|
1929
|
+
const request = {
|
|
1930
|
+
id,
|
|
1931
|
+
type: command === "join" ? "join" : "message",
|
|
1932
|
+
...command === "join" ? { channel: params.channel } : { channel: currentChannel },
|
|
1933
|
+
message: {
|
|
1934
|
+
id,
|
|
1935
|
+
command,
|
|
1936
|
+
params: {
|
|
1937
|
+
...params,
|
|
1938
|
+
commandId: id
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
};
|
|
1942
|
+
const timeout = setTimeout(() => {
|
|
1943
|
+
if (pendingRequests.has(id)) {
|
|
1944
|
+
pendingRequests.delete(id);
|
|
1945
|
+
logger.error(`Request ${id} to Figma timed out after ${timeoutMs / 1e3} seconds`);
|
|
1946
|
+
reject(new Error(
|
|
1947
|
+
`Request to Figma timed out. This usually means the Figma plugin is not connected to the relay. MCP is using port ${activePort}, channel "${currentChannel}". Check the Figma plugin window: the port and channel name must match.`
|
|
1948
|
+
));
|
|
1949
|
+
}
|
|
1950
|
+
}, timeoutMs);
|
|
1951
|
+
pendingRequests.set(id, { resolve, reject, timeout, lastActivity: Date.now() });
|
|
1952
|
+
logger.info(`Sending command to Figma: ${command}`);
|
|
1953
|
+
logger.debug(`Request details: ${JSON.stringify(request)}`);
|
|
1954
|
+
ws.send(JSON.stringify(request));
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
var server = new import_mcp.McpServer({
|
|
1958
|
+
name: "VibmaMCP",
|
|
1959
|
+
version: "1.0.0"
|
|
1960
|
+
});
|
|
1961
|
+
server.tool(
|
|
1962
|
+
"join_channel",
|
|
1963
|
+
"REQUIRED FIRST STEP: Join a channel before using any other tool. The channel name is shown in the Figma plugin UI (defaults to 'vibma' if not customised). After joining, call `ping` to verify the Figma plugin is connected. All subsequent commands are sent through this channel.",
|
|
1964
|
+
{ channel: import_zod19.z.string().describe("The channel name displayed in the Figma plugin panel. Defaults to 'vibma' if omitted.").default("vibma") },
|
|
1965
|
+
async ({ channel }) => {
|
|
1966
|
+
try {
|
|
1967
|
+
await joinChannel(channel);
|
|
1968
|
+
return {
|
|
1969
|
+
content: [{ type: "text", text: `Joined channel "${channel}" on port ${activePort}. Call \`ping\` now to verify the Figma plugin is connected.` }]
|
|
1970
|
+
};
|
|
1971
|
+
} catch (error) {
|
|
1972
|
+
return {
|
|
1973
|
+
content: [{
|
|
1974
|
+
type: "text",
|
|
1975
|
+
text: `Error joining channel: ${error instanceof Error ? error.message : String(error)}. MCP is using port ${activePort} \u2014 confirm the relay is running on the same port.`
|
|
1976
|
+
}]
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
);
|
|
1981
|
+
registerAllTools(server, sendCommandToFigma);
|
|
1982
|
+
async function main() {
|
|
1983
|
+
try {
|
|
1984
|
+
connectToFigma();
|
|
1985
|
+
} catch (error) {
|
|
1986
|
+
logger.warn(`Could not connect to Figma initially: ${error instanceof Error ? error.message : String(error)}`);
|
|
1987
|
+
logger.warn("Will try to connect when the first command is sent");
|
|
1988
|
+
}
|
|
1989
|
+
const transport = new import_stdio.StdioServerTransport();
|
|
1990
|
+
await server.connect(transport);
|
|
1991
|
+
logger.info("FigmaMCP server running on stdio");
|
|
1992
|
+
}
|
|
1993
|
+
main().catch((error) => {
|
|
1994
|
+
logger.error(`Error starting FigmaMCP server: ${error instanceof Error ? error.message : String(error)}`);
|
|
1995
|
+
process.exit(1);
|
|
1996
|
+
});
|
|
1997
|
+
//# sourceMappingURL=mcp.cjs.map
|