@atezer/figma-mcp-bridge 1.7.25 → 1.7.27
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/CHANGELOG.md +71 -0
- package/agents/ds-auditor.md +6 -0
- package/agents/screen-builder.md +6 -0
- package/agents/token-syncer.md +6 -0
- package/dist/core/audit-log.d.ts +4 -0
- package/dist/core/audit-log.d.ts.map +1 -1
- package/dist/core/audit-log.js +12 -0
- package/dist/core/audit-log.js.map +1 -1
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +6 -2
- package/dist/core/config.js.map +1 -1
- package/dist/core/plugin-bridge-connector.d.ts +16 -16
- package/dist/core/plugin-bridge-connector.d.ts.map +1 -1
- package/dist/core/plugin-bridge-connector.js +24 -3
- package/dist/core/plugin-bridge-connector.js.map +1 -1
- package/dist/core/plugin-bridge-server.d.ts +5 -3
- package/dist/core/plugin-bridge-server.d.ts.map +1 -1
- package/dist/core/plugin-bridge-server.js +34 -22
- package/dist/core/plugin-bridge-server.js.map +1 -1
- package/dist/core/response-cache.d.ts +16 -0
- package/dist/core/response-cache.d.ts.map +1 -0
- package/dist/core/response-cache.js +46 -0
- package/dist/core/response-cache.js.map +1 -0
- package/dist/core/response-guard.d.ts.map +1 -1
- package/dist/core/response-guard.js +19 -13
- package/dist/core/response-guard.js.map +1 -1
- package/dist/core/types/figma.d.ts +20 -0
- package/dist/core/types/figma.d.ts.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/local-plugin-only.d.ts.map +1 -1
- package/dist/local-plugin-only.js +271 -143
- package/dist/local-plugin-only.js.map +1 -1
- package/f-mcp-plugin/code.js +40 -3
- package/f-mcp-plugin/manifest.json +1 -1
- package/f-mcp-plugin/ui.html +45 -9
- package/hooks/hooks.json +1 -1
- package/package.json +1 -1
- package/skills/ai-handoff-export/SKILL.md +8 -0
- package/skills/component-documentation/SKILL.md +8 -0
- package/skills/figjam-diagram-builder/SKILL.md +8 -0
- package/skills/figma-screen-analyzer/SKILL.md +8 -0
- package/skills/ux-copy-guidance/SKILL.md +8 -0
- package/skills/visual-qa-compare/SKILL.md +8 -0
|
@@ -18,6 +18,9 @@ import { PluginBridgeServer } from "./core/plugin-bridge-server.js";
|
|
|
18
18
|
import { PluginBridgeConnector } from "./core/plugin-bridge-connector.js";
|
|
19
19
|
import { parseFigmaUrl } from "./core/figma-url.js";
|
|
20
20
|
import { truncateRestResponse } from "./core/response-guard.js";
|
|
21
|
+
import { closeAuditLog } from "./core/audit-log.js";
|
|
22
|
+
import { FMCP_VERSION } from "./core/version.js";
|
|
23
|
+
import { ResponseCache } from "./core/response-cache.js";
|
|
21
24
|
const logger = createChildLogger({ component: "plugin-only-mcp" });
|
|
22
25
|
/** Resolve fileKey from figmaUrl (parse) or explicit fileKey. Returns undefined if neither yields a key. */
|
|
23
26
|
function resolveFileKey(figmaUrl, explicitFileKey) {
|
|
@@ -67,6 +70,49 @@ function normalizeForCompare(s) {
|
|
|
67
70
|
return s.replace(/\s/g, "");
|
|
68
71
|
}
|
|
69
72
|
const PLUGIN_NOT_CONNECTED = "F-MCP ATezer Bridge plugin not connected. Open Figma → Plugins → Development → F-MCP ATezer Bridge, wait for 'ready'.";
|
|
73
|
+
/** Categorize figma_execute errors for actionable user feedback. */
|
|
74
|
+
function categorizeExecuteError(message, durationMs, timeoutMs) {
|
|
75
|
+
const msg = message.toLowerCase();
|
|
76
|
+
if (msg.includes("timed out") || msg.includes("timeout") || (durationMs >= timeoutMs * 0.9))
|
|
77
|
+
return "TIMEOUT";
|
|
78
|
+
if (msg.includes("syntax error") || msg.includes("unexpected token") || msg.includes("unexpected identifier"))
|
|
79
|
+
return "SYNTAX";
|
|
80
|
+
if (msg.includes("not connected") || msg.includes("plugin bridge") || msg.includes("websocket") || msg.includes("bridge active"))
|
|
81
|
+
return "CONNECTION";
|
|
82
|
+
if (msg.includes("serialization") || msg.includes("could not be serialized") || msg.includes("circular"))
|
|
83
|
+
return "SERIALIZATION";
|
|
84
|
+
if (msg.includes("font") && (msg.includes("not loaded") || msg.includes("loadfontasync")))
|
|
85
|
+
return "FONT_NOT_LOADED";
|
|
86
|
+
if (msg.includes("cannot read") || msg.includes("is not a function") || msg.includes("undefined"))
|
|
87
|
+
return "RUNTIME";
|
|
88
|
+
return "RUNTIME";
|
|
89
|
+
}
|
|
90
|
+
function getErrorHint(category) {
|
|
91
|
+
switch (category) {
|
|
92
|
+
case "TIMEOUT": return "Islem cok uzun surdu. timeout parametresini artir (max 120000ms), veya islemi daha kucuk parcalara bol.";
|
|
93
|
+
case "SYNTAX": return "JavaScript syntax hatasi. Kaçis karakterleri, eksik parantez veya reserved word kontrol et.";
|
|
94
|
+
case "CONNECTION": return "Plugin bagli degil. Figma'da F-MCP ATezer Bridge plugin'ini ac ve 'Bridge active' gosterdigini dogrula.";
|
|
95
|
+
case "SERIALIZATION": return "Sonuc JSON serialize edilemedi. Figma node objesi degil, plain object don: { id: node.id, name: node.name }";
|
|
96
|
+
case "FONT_NOT_LOADED": return "Font yuklenmemis. Kodun basina await figma.loadFontAsync({family, style}) ekle.";
|
|
97
|
+
case "RUNTIME": return "Kod calisma hatasi. Yaygin: yanlis sayfa (setCurrentPageAsync eksik), null node, undefined property.";
|
|
98
|
+
default: return "Hata mesajini kontrol et.";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Wrap a tool handler with try-catch to prevent unhandled rejections. */
|
|
102
|
+
function safeToolHandler(handler) {
|
|
103
|
+
return async (params) => {
|
|
104
|
+
try {
|
|
105
|
+
return await handler(params);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }) }],
|
|
111
|
+
isError: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
70
116
|
function getConnector(bridge, fileKey) {
|
|
71
117
|
if (!bridge.isConnected(fileKey)) {
|
|
72
118
|
if (fileKey) {
|
|
@@ -87,9 +133,12 @@ export async function main() {
|
|
|
87
133
|
const auditLogPath = config.local?.auditLogPath;
|
|
88
134
|
const bridge = new PluginBridgeServer(port, { auditLogPath });
|
|
89
135
|
bridge.start();
|
|
136
|
+
const cache = new ResponseCache();
|
|
137
|
+
/** Invalidate cache after any mutating operation. */
|
|
138
|
+
function invalidateCache() { cache.invalidate(); }
|
|
90
139
|
const server = new McpServer({
|
|
91
140
|
name: "F-MCP ATezer Bridge (Plugin-only)",
|
|
92
|
-
version:
|
|
141
|
+
version: FMCP_VERSION,
|
|
93
142
|
});
|
|
94
143
|
// ---- figma_list_connected_files (multi-client discovery) ----
|
|
95
144
|
server.registerTool("figma_list_connected_files", {
|
|
@@ -108,7 +157,7 @@ export async function main() {
|
|
|
108
157
|
message: files.length === 0
|
|
109
158
|
? "No plugins connected. Open Figma and run the F-MCP ATezer Bridge plugin."
|
|
110
159
|
: `${files.length} plugin(s) connected. Use fileKey parameter in other tools to target a specific file.`,
|
|
111
|
-
}
|
|
160
|
+
}),
|
|
112
161
|
}],
|
|
113
162
|
};
|
|
114
163
|
});
|
|
@@ -132,7 +181,7 @@ export async function main() {
|
|
|
132
181
|
const resolvedKey = resolveFileKey(figmaUrl, fileKey);
|
|
133
182
|
if (figmaUrl && !resolvedKey) {
|
|
134
183
|
return {
|
|
135
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Invalid Figma/FigJam URL: could not extract file key." }
|
|
184
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Invalid Figma/FigJam URL: could not extract file key." }) }],
|
|
136
185
|
isError: true,
|
|
137
186
|
};
|
|
138
187
|
}
|
|
@@ -149,13 +198,13 @@ export async function main() {
|
|
|
149
198
|
? JSON.stringify({ success: false, error: "No data from plugin" })
|
|
150
199
|
: typeof data === "string"
|
|
151
200
|
? data
|
|
152
|
-
: JSON.stringify(data
|
|
201
|
+
: JSON.stringify(data);
|
|
153
202
|
return { content: [{ type: "text", text }] };
|
|
154
203
|
}
|
|
155
204
|
catch (err) {
|
|
156
205
|
const msg = err instanceof Error ? err.message : String(err);
|
|
157
206
|
return {
|
|
158
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }
|
|
207
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }) }],
|
|
159
208
|
isError: true,
|
|
160
209
|
};
|
|
161
210
|
}
|
|
@@ -182,7 +231,7 @@ export async function main() {
|
|
|
182
231
|
const { fileKey: resolvedKey, nodeId: resolvedNodeId } = resolveDesignContextParams({ figmaUrl, fileKey, nodeId });
|
|
183
232
|
if (figmaUrl && !resolvedKey) {
|
|
184
233
|
return {
|
|
185
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Invalid Figma/FigJam URL: could not extract file key." }
|
|
234
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Invalid Figma/FigJam URL: could not extract file key." }) }],
|
|
186
235
|
isError: true,
|
|
187
236
|
};
|
|
188
237
|
}
|
|
@@ -203,13 +252,13 @@ export async function main() {
|
|
|
203
252
|
? JSON.stringify({ success: false, error: "No data from plugin" })
|
|
204
253
|
: typeof data === "string"
|
|
205
254
|
? data
|
|
206
|
-
: JSON.stringify(data
|
|
255
|
+
: JSON.stringify(data);
|
|
207
256
|
return { content: [{ type: "text", text }] };
|
|
208
257
|
}
|
|
209
258
|
catch (err) {
|
|
210
259
|
const msg = err instanceof Error ? err.message : String(err);
|
|
211
260
|
return {
|
|
212
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }
|
|
261
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }) }],
|
|
213
262
|
isError: true,
|
|
214
263
|
};
|
|
215
264
|
}
|
|
@@ -223,7 +272,7 @@ export async function main() {
|
|
|
223
272
|
verbosity: z.enum(["inventory", "summary", "standard", "full"]).optional().default("summary"),
|
|
224
273
|
},
|
|
225
274
|
annotations: { readOnlyHint: true },
|
|
226
|
-
}, async ({ figmaUrl, fileKey, verbosity }) => {
|
|
275
|
+
}, safeToolHandler(async ({ figmaUrl, fileKey, verbosity }) => {
|
|
227
276
|
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
228
277
|
const raw = await conn.getVariablesFromPluginUI();
|
|
229
278
|
if (!raw || !raw.variables) {
|
|
@@ -247,8 +296,8 @@ export async function main() {
|
|
|
247
296
|
valuesByMode: v.valuesByMode,
|
|
248
297
|
}));
|
|
249
298
|
}
|
|
250
|
-
return { content: [{ type: "text", text: JSON.stringify(out
|
|
251
|
-
});
|
|
299
|
+
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
300
|
+
}));
|
|
252
301
|
// ---- figma_get_component ----
|
|
253
302
|
server.registerTool("figma_get_component", {
|
|
254
303
|
description: "Get component metadata by node ID from the open Figma file. No REST API. Use fileKey or figmaUrl to target a specific file.",
|
|
@@ -258,11 +307,11 @@ export async function main() {
|
|
|
258
307
|
nodeId: z.string(),
|
|
259
308
|
},
|
|
260
309
|
annotations: { readOnlyHint: true },
|
|
261
|
-
}, async ({ figmaUrl, fileKey, nodeId }) => {
|
|
310
|
+
}, safeToolHandler(async ({ figmaUrl, fileKey, nodeId }) => {
|
|
262
311
|
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
263
312
|
const result = await conn.getComponentFromPluginUI(nodeId);
|
|
264
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
265
|
-
});
|
|
313
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
314
|
+
}));
|
|
266
315
|
// ---- figma_get_styles (plugin only) ----
|
|
267
316
|
server.registerTool("figma_get_styles", {
|
|
268
317
|
description: "Get local paint, text, and effect styles from the open Figma file. No REST API. Use fileKey or figmaUrl to target a specific file.",
|
|
@@ -272,11 +321,11 @@ export async function main() {
|
|
|
272
321
|
verbosity: z.enum(["summary", "full"]).optional().default("summary"),
|
|
273
322
|
},
|
|
274
323
|
annotations: { readOnlyHint: true },
|
|
275
|
-
}, async ({ figmaUrl, fileKey, verbosity }) => {
|
|
324
|
+
}, safeToolHandler(async ({ figmaUrl, fileKey, verbosity }) => {
|
|
276
325
|
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
277
326
|
const data = await conn.getLocalStyles(verbosity);
|
|
278
|
-
return { content: [{ type: "text", text: JSON.stringify(data || {}
|
|
279
|
-
});
|
|
327
|
+
return { content: [{ type: "text", text: JSON.stringify(data || {}) }] };
|
|
328
|
+
}));
|
|
280
329
|
// ---- figma_execute ----
|
|
281
330
|
server.registerTool("figma_execute", {
|
|
282
331
|
description: "Run JavaScript in the Figma plugin context. Full Plugin API available. Use fileKey or figmaUrl to target a specific file.",
|
|
@@ -284,14 +333,59 @@ export async function main() {
|
|
|
284
333
|
figmaUrl: z.string().optional().describe("Figma or FigJam file URL for routing."),
|
|
285
334
|
fileKey: z.string().optional().describe("Target a specific connected file."),
|
|
286
335
|
code: z.string(),
|
|
287
|
-
timeout: z.number().optional().default(
|
|
336
|
+
timeout: z.number().optional().default(15000),
|
|
288
337
|
},
|
|
289
338
|
annotations: { destructiveHint: true },
|
|
290
|
-
}, async ({ figmaUrl, fileKey, code, timeout }) => {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
339
|
+
}, safeToolHandler(async ({ figmaUrl, fileKey, code, timeout }) => {
|
|
340
|
+
if (code.length > 50000) {
|
|
341
|
+
return {
|
|
342
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, errorCategory: "VALIDATION", error: "Code too long (max 50,000 characters). Break into smaller pieces." }) }],
|
|
343
|
+
isError: true,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
const clampedTimeout = Math.max(3000, Math.min(timeout ?? 15000, 120000));
|
|
347
|
+
invalidateCache();
|
|
348
|
+
const startTime = Date.now();
|
|
349
|
+
try {
|
|
350
|
+
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
351
|
+
const result = await conn.executeCodeViaUI(code, clampedTimeout);
|
|
352
|
+
const durationMs = Date.now() - startTime;
|
|
353
|
+
// Plugin may return { success: false, error: "..." } without throwing
|
|
354
|
+
if (typeof result === "object" && result !== null && result.success === false) {
|
|
355
|
+
const pluginError = String(result.error || "Unknown plugin error");
|
|
356
|
+
const category = categorizeExecuteError(pluginError, durationMs, clampedTimeout);
|
|
357
|
+
return {
|
|
358
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
359
|
+
...result,
|
|
360
|
+
errorCategory: category,
|
|
361
|
+
_metrics: { durationMs, timeoutMs: clampedTimeout },
|
|
362
|
+
hint: getErrorHint(category),
|
|
363
|
+
}) }],
|
|
364
|
+
isError: true,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
const enriched = typeof result === "object" && result !== null
|
|
368
|
+
? { ...result, _metrics: { durationMs, timeoutMs: clampedTimeout } }
|
|
369
|
+
: result;
|
|
370
|
+
return { content: [{ type: "text", text: JSON.stringify(enriched) }] };
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
const durationMs = Date.now() - startTime;
|
|
374
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
375
|
+
const category = categorizeExecuteError(msg, durationMs, clampedTimeout);
|
|
376
|
+
logger.warn({ errorCategory: category, durationMs, timeout: clampedTimeout }, "figma_execute failed: %s", msg);
|
|
377
|
+
return {
|
|
378
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
379
|
+
success: false,
|
|
380
|
+
errorCategory: category,
|
|
381
|
+
error: msg,
|
|
382
|
+
_metrics: { durationMs, timeoutMs: clampedTimeout },
|
|
383
|
+
hint: getErrorHint(category),
|
|
384
|
+
}) }],
|
|
385
|
+
isError: true,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}));
|
|
295
389
|
// ---- figma_capture_screenshot ----
|
|
296
390
|
server.registerTool("figma_capture_screenshot", {
|
|
297
391
|
description: "Capture screenshot of a node or current view from the plugin. No REST API. Use fileKey or figmaUrl to target a specific file.",
|
|
@@ -303,11 +397,11 @@ export async function main() {
|
|
|
303
397
|
scale: z.number().optional().default(2),
|
|
304
398
|
},
|
|
305
399
|
annotations: { readOnlyHint: true },
|
|
306
|
-
}, async ({ figmaUrl, fileKey, nodeId, format, scale }) => {
|
|
400
|
+
}, safeToolHandler(async ({ figmaUrl, fileKey, nodeId, format, scale }) => {
|
|
307
401
|
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
308
402
|
const result = await conn.captureScreenshot(nodeId ?? null, { format, scale });
|
|
309
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
310
|
-
});
|
|
403
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
404
|
+
}));
|
|
311
405
|
// ---- figma_set_instance_properties ----
|
|
312
406
|
server.registerTool("figma_set_instance_properties", {
|
|
313
407
|
description: "Set component instance properties (TEXT, BOOLEAN, VARIANT, etc.). Use fileKey or figmaUrl to target a specific file.",
|
|
@@ -318,11 +412,12 @@ export async function main() {
|
|
|
318
412
|
properties: z.record(z.union([z.string(), z.boolean()])),
|
|
319
413
|
},
|
|
320
414
|
annotations: { destructiveHint: true },
|
|
321
|
-
}, async ({ figmaUrl, fileKey, nodeId, properties }) => {
|
|
415
|
+
}, safeToolHandler(async ({ figmaUrl, fileKey, nodeId, properties }) => {
|
|
416
|
+
invalidateCache();
|
|
322
417
|
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
323
418
|
const result = await conn.setInstanceProperties(nodeId, properties);
|
|
324
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
325
|
-
});
|
|
419
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
420
|
+
}));
|
|
326
421
|
// ---- Variable CRUD ----
|
|
327
422
|
server.registerTool("figma_update_variable", {
|
|
328
423
|
description: "Update a variable value in a mode. Get IDs from figma_get_variables.",
|
|
@@ -332,11 +427,12 @@ export async function main() {
|
|
|
332
427
|
value: z.union([z.string(), z.number(), z.boolean()]),
|
|
333
428
|
},
|
|
334
429
|
annotations: { destructiveHint: true },
|
|
335
|
-
}, async (p) => {
|
|
430
|
+
}, safeToolHandler(async (p) => {
|
|
431
|
+
invalidateCache();
|
|
336
432
|
const conn = getConnector(bridge);
|
|
337
433
|
const result = await conn.updateVariable(p.variableId, p.modeId, p.value);
|
|
338
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
339
|
-
});
|
|
434
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
435
|
+
}));
|
|
340
436
|
server.registerTool("figma_create_variable", {
|
|
341
437
|
description: "Create a variable in a collection. Get collectionId from figma_get_variables.",
|
|
342
438
|
inputSchema: {
|
|
@@ -346,65 +442,72 @@ export async function main() {
|
|
|
346
442
|
options: z.record(z.any()).optional(),
|
|
347
443
|
},
|
|
348
444
|
annotations: { destructiveHint: true },
|
|
349
|
-
}, async (p) => {
|
|
445
|
+
}, safeToolHandler(async (p) => {
|
|
446
|
+
invalidateCache();
|
|
350
447
|
const conn = getConnector(bridge);
|
|
351
448
|
const result = await conn.createVariable(p.name, p.collectionId, p.resolvedType, p.options);
|
|
352
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
353
|
-
});
|
|
449
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
450
|
+
}));
|
|
354
451
|
server.registerTool("figma_create_variable_collection", {
|
|
355
452
|
description: "Create a variable collection.",
|
|
356
453
|
inputSchema: { name: z.string(), options: z.record(z.any()).optional() },
|
|
357
454
|
annotations: { destructiveHint: true },
|
|
358
|
-
}, async (p) => {
|
|
455
|
+
}, safeToolHandler(async (p) => {
|
|
456
|
+
invalidateCache();
|
|
359
457
|
const conn = getConnector(bridge);
|
|
360
458
|
const result = await conn.createVariableCollection(p.name, p.options);
|
|
361
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
362
|
-
});
|
|
459
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
460
|
+
}));
|
|
363
461
|
server.registerTool("figma_delete_variable", {
|
|
364
462
|
description: "Delete a variable.",
|
|
365
463
|
inputSchema: { variableId: z.string() },
|
|
366
464
|
annotations: { destructiveHint: true },
|
|
367
|
-
}, async (p) => {
|
|
465
|
+
}, safeToolHandler(async (p) => {
|
|
466
|
+
invalidateCache();
|
|
368
467
|
const conn = getConnector(bridge);
|
|
369
468
|
const result = await conn.deleteVariable(p.variableId);
|
|
370
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
371
|
-
});
|
|
469
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
470
|
+
}));
|
|
372
471
|
server.registerTool("figma_delete_variable_collection", {
|
|
373
472
|
description: "Delete a variable collection.",
|
|
374
473
|
inputSchema: { collectionId: z.string() },
|
|
375
474
|
annotations: { destructiveHint: true },
|
|
376
|
-
}, async (p) => {
|
|
475
|
+
}, safeToolHandler(async (p) => {
|
|
476
|
+
invalidateCache();
|
|
377
477
|
const conn = getConnector(bridge);
|
|
378
478
|
const result = await conn.deleteVariableCollection(p.collectionId);
|
|
379
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
380
|
-
});
|
|
479
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
480
|
+
}));
|
|
381
481
|
server.registerTool("figma_rename_variable", {
|
|
382
482
|
description: "Rename a variable.",
|
|
383
483
|
inputSchema: { variableId: z.string(), newName: z.string() },
|
|
384
484
|
annotations: { destructiveHint: true },
|
|
385
|
-
}, async (p) => {
|
|
485
|
+
}, safeToolHandler(async (p) => {
|
|
486
|
+
invalidateCache();
|
|
386
487
|
const conn = getConnector(bridge);
|
|
387
488
|
const result = await conn.renameVariable(p.variableId, p.newName);
|
|
388
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
389
|
-
});
|
|
489
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
490
|
+
}));
|
|
390
491
|
server.registerTool("figma_add_mode", {
|
|
391
492
|
description: "Add a mode to a collection.",
|
|
392
493
|
inputSchema: { collectionId: z.string(), modeName: z.string() },
|
|
393
494
|
annotations: { destructiveHint: true },
|
|
394
|
-
}, async (p) => {
|
|
495
|
+
}, safeToolHandler(async (p) => {
|
|
496
|
+
invalidateCache();
|
|
395
497
|
const conn = getConnector(bridge);
|
|
396
498
|
const result = await conn.addMode(p.collectionId, p.modeName);
|
|
397
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
398
|
-
});
|
|
499
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
500
|
+
}));
|
|
399
501
|
server.registerTool("figma_rename_mode", {
|
|
400
502
|
description: "Rename a mode in a collection.",
|
|
401
503
|
inputSchema: { collectionId: z.string(), modeId: z.string(), newName: z.string() },
|
|
402
504
|
annotations: { destructiveHint: true },
|
|
403
|
-
}, async (p) => {
|
|
505
|
+
}, safeToolHandler(async (p) => {
|
|
506
|
+
invalidateCache();
|
|
404
507
|
const conn = getConnector(bridge);
|
|
405
508
|
const result = await conn.renameMode(p.collectionId, p.modeId, p.newName);
|
|
406
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
407
|
-
});
|
|
509
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
510
|
+
}));
|
|
408
511
|
// ---- Design system summary (minimal tokens) ----
|
|
409
512
|
server.registerTool("figma_get_design_system_summary", {
|
|
410
513
|
description: "Get a compact overview: variable collection names and component counts. Minimal tokens. Use fileKey or figmaUrl to target a specific file.",
|
|
@@ -415,7 +518,7 @@ export async function main() {
|
|
|
415
518
|
limit: z.number().min(0).optional(),
|
|
416
519
|
},
|
|
417
520
|
annotations: { readOnlyHint: true },
|
|
418
|
-
}, async ({ figmaUrl, fileKey, currentPageOnly, limit }) => {
|
|
521
|
+
}, safeToolHandler(async ({ figmaUrl, fileKey, currentPageOnly, limit }) => {
|
|
419
522
|
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
420
523
|
const [vars, components] = await Promise.all([
|
|
421
524
|
conn.getVariablesFromPluginUI(),
|
|
@@ -430,8 +533,8 @@ export async function main() {
|
|
|
430
533
|
components: compData?.totalComponents ?? 0,
|
|
431
534
|
componentSets: compData?.totalComponentSets ?? 0,
|
|
432
535
|
};
|
|
433
|
-
return { content: [{ type: "text", text: JSON.stringify(out
|
|
434
|
-
});
|
|
536
|
+
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
537
|
+
}));
|
|
435
538
|
// ---- figma_search_components ----
|
|
436
539
|
server.registerTool("figma_search_components", {
|
|
437
540
|
description: "Search local components by name. Returns nodeIds and names. No REST API. Use fileKey or figmaUrl to target a specific file.",
|
|
@@ -443,7 +546,7 @@ export async function main() {
|
|
|
443
546
|
limit: z.number().min(0).optional(),
|
|
444
547
|
},
|
|
445
548
|
annotations: { readOnlyHint: true },
|
|
446
|
-
}, async ({ figmaUrl, fileKey, query, currentPageOnly, limit }) => {
|
|
549
|
+
}, safeToolHandler(async ({ figmaUrl, fileKey, query, currentPageOnly, limit }) => {
|
|
447
550
|
const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
|
|
448
551
|
const result = (await conn.getLocalComponents({ currentPageOnly, limit }));
|
|
449
552
|
const data = result?.data;
|
|
@@ -456,8 +559,8 @@ export async function main() {
|
|
|
456
559
|
list = list.filter((c) => (c.name || "").toLowerCase().includes(q));
|
|
457
560
|
}
|
|
458
561
|
const summary = list.map((c) => ({ id: c.id, name: c.name, key: c.key, type: c.type }));
|
|
459
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: true, components: summary }
|
|
460
|
-
});
|
|
562
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, components: summary }) }] };
|
|
563
|
+
}));
|
|
461
564
|
// ---- Node operations (short list) ----
|
|
462
565
|
server.registerTool("figma_instantiate_component", {
|
|
463
566
|
description: "Create a component instance. Use componentKey from figma_search_components or nodeId for local components.",
|
|
@@ -473,63 +576,80 @@ export async function main() {
|
|
|
473
576
|
.optional(),
|
|
474
577
|
},
|
|
475
578
|
annotations: { destructiveHint: true },
|
|
476
|
-
}, async (p) => {
|
|
579
|
+
}, safeToolHandler(async (p) => {
|
|
580
|
+
invalidateCache();
|
|
477
581
|
const conn = getConnector(bridge);
|
|
478
582
|
const result = await conn.instantiateComponent(p.componentKey, p.options || {});
|
|
479
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
480
|
-
});
|
|
583
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
584
|
+
}));
|
|
481
585
|
server.registerTool("figma_refresh_variables", {
|
|
482
586
|
description: "Refresh variables from the file.",
|
|
483
587
|
inputSchema: {},
|
|
484
588
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
485
|
-
}, async () => {
|
|
589
|
+
}, safeToolHandler(async () => {
|
|
486
590
|
const conn = getConnector(bridge);
|
|
487
591
|
const result = await conn.refreshVariables();
|
|
488
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
489
|
-
});
|
|
592
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
593
|
+
}));
|
|
490
594
|
// ---- Console (plugin buffer, no CDP) ----
|
|
491
595
|
server.registerTool("figma_get_console_logs", {
|
|
492
596
|
description: "Get plugin console logs (log/warn/error) from the F-MCP plugin buffer. No CDP. Limit default 50.",
|
|
493
597
|
inputSchema: { limit: z.number().min(1).max(200).optional().default(50) },
|
|
494
598
|
annotations: { readOnlyHint: true },
|
|
495
|
-
}, async ({ limit }) => {
|
|
599
|
+
}, safeToolHandler(async ({ limit }) => {
|
|
496
600
|
const conn = getConnector(bridge);
|
|
497
601
|
const data = await conn.getConsoleLogs(limit);
|
|
498
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }
|
|
499
|
-
});
|
|
602
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }) }] };
|
|
603
|
+
}));
|
|
500
604
|
server.registerTool("figma_watch_console", {
|
|
501
605
|
description: "Stream new plugin console logs until timeout. Polls the plugin buffer. Timeout default 30s.",
|
|
502
606
|
inputSchema: { timeoutSeconds: z.number().min(1).max(120).optional().default(30) },
|
|
503
607
|
annotations: { readOnlyHint: true },
|
|
504
|
-
}, async ({ timeoutSeconds }) => {
|
|
608
|
+
}, safeToolHandler(async ({ timeoutSeconds }) => {
|
|
505
609
|
const conn = getConnector(bridge);
|
|
506
610
|
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
507
|
-
|
|
611
|
+
let lastSeenTime = 0;
|
|
508
612
|
const stream = [];
|
|
613
|
+
let pollIntervalMs = 1000;
|
|
614
|
+
let consecutiveEmptyPolls = 0;
|
|
509
615
|
while (Date.now() < deadline) {
|
|
510
616
|
const { logs } = await conn.getConsoleLogs(200);
|
|
617
|
+
let newCount = 0;
|
|
511
618
|
for (const entry of logs) {
|
|
512
|
-
|
|
513
|
-
if (!seen.has(key)) {
|
|
514
|
-
seen.add(key);
|
|
619
|
+
if (entry.time > lastSeenTime) {
|
|
515
620
|
stream.push(entry);
|
|
621
|
+
newCount++;
|
|
622
|
+
if (entry.time > lastSeenTime)
|
|
623
|
+
lastSeenTime = entry.time;
|
|
516
624
|
}
|
|
517
625
|
}
|
|
518
|
-
|
|
626
|
+
if (newCount > 0) {
|
|
627
|
+
consecutiveEmptyPolls = 0;
|
|
628
|
+
pollIntervalMs = 1000;
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
consecutiveEmptyPolls++;
|
|
632
|
+
if (consecutiveEmptyPolls >= 10)
|
|
633
|
+
break;
|
|
634
|
+
if (consecutiveEmptyPolls >= 3) {
|
|
635
|
+
pollIntervalMs = Math.min(pollIntervalMs * 2, 5000);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
519
639
|
}
|
|
520
640
|
return {
|
|
521
|
-
content: [{ type: "text", text: JSON.stringify({ success: true, stream, count: stream.length }
|
|
641
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, stream, count: stream.length }) }],
|
|
522
642
|
};
|
|
523
|
-
});
|
|
643
|
+
}));
|
|
524
644
|
server.registerTool("figma_clear_console", {
|
|
525
645
|
description: "Clear the plugin console log buffer.",
|
|
526
646
|
inputSchema: {},
|
|
527
647
|
annotations: { destructiveHint: true },
|
|
528
|
-
}, async () => {
|
|
648
|
+
}, safeToolHandler(async () => {
|
|
529
649
|
const conn = getConnector(bridge);
|
|
530
650
|
await conn.clearConsole();
|
|
531
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: true, message: "Console cleared" }
|
|
532
|
-
});
|
|
651
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, message: "Console cleared" }) }] };
|
|
652
|
+
}));
|
|
533
653
|
// ---- set_description, get_component_image, get_component_for_development ----
|
|
534
654
|
server.registerTool("figma_set_description", {
|
|
535
655
|
description: "Set description on a component, component set, or style node. Supports markdown (descriptionMarkdown).",
|
|
@@ -539,11 +659,12 @@ export async function main() {
|
|
|
539
659
|
descriptionMarkdown: z.string().optional(),
|
|
540
660
|
},
|
|
541
661
|
annotations: { destructiveHint: true },
|
|
542
|
-
}, async (p) => {
|
|
662
|
+
}, safeToolHandler(async (p) => {
|
|
663
|
+
invalidateCache();
|
|
543
664
|
const conn = getConnector(bridge);
|
|
544
665
|
const result = await conn.setNodeDescription(p.nodeId, p.description, p.descriptionMarkdown);
|
|
545
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
546
|
-
});
|
|
666
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
667
|
+
}));
|
|
547
668
|
server.registerTool("figma_get_component_image", {
|
|
548
669
|
description: "Get screenshot of a node (component/frame). Returns base64 image. No REST API.",
|
|
549
670
|
inputSchema: {
|
|
@@ -552,11 +673,11 @@ export async function main() {
|
|
|
552
673
|
format: z.enum(["PNG", "JPG"]).optional().default("PNG"),
|
|
553
674
|
},
|
|
554
675
|
annotations: { readOnlyHint: true },
|
|
555
|
-
}, async ({ nodeId, scale, format }) => {
|
|
676
|
+
}, safeToolHandler(async ({ nodeId, scale, format }) => {
|
|
556
677
|
const conn = getConnector(bridge);
|
|
557
678
|
const result = await conn.captureScreenshot(nodeId, { scale, format });
|
|
558
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
559
|
-
});
|
|
679
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
680
|
+
}));
|
|
560
681
|
server.registerTool("figma_get_component_for_development", {
|
|
561
682
|
description: "Get component metadata plus base64 screenshot in one call. For design-to-code workflows.",
|
|
562
683
|
inputSchema: {
|
|
@@ -565,7 +686,7 @@ export async function main() {
|
|
|
565
686
|
format: z.enum(["PNG", "JPG"]).optional().default("PNG"),
|
|
566
687
|
},
|
|
567
688
|
annotations: { readOnlyHint: true },
|
|
568
|
-
}, async ({ nodeId, scale, format }) => {
|
|
689
|
+
}, safeToolHandler(async ({ nodeId, scale, format }) => {
|
|
569
690
|
const conn = getConnector(bridge);
|
|
570
691
|
const [component, screenshot] = await Promise.all([
|
|
571
692
|
conn.getComponentFromPluginUI(nodeId),
|
|
@@ -573,8 +694,8 @@ export async function main() {
|
|
|
573
694
|
]);
|
|
574
695
|
const comp = component?.component ?? component;
|
|
575
696
|
const out = { success: true, component: comp, image: screenshot?.image ?? screenshot?.data };
|
|
576
|
-
return { content: [{ type: "text", text: JSON.stringify(out
|
|
577
|
-
});
|
|
697
|
+
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
698
|
+
}));
|
|
578
699
|
// ---- Batch variables & setup_design_tokens & arrange_component_set ----
|
|
579
700
|
server.registerTool("figma_batch_create_variables", {
|
|
580
701
|
description: "Create up to 100 variables in one call. Each item: collectionId, name, resolvedType (COLOR/FLOAT/STRING/BOOLEAN), value, modeId. Returns created and failed lists.",
|
|
@@ -589,11 +710,12 @@ export async function main() {
|
|
|
589
710
|
})).max(100),
|
|
590
711
|
},
|
|
591
712
|
annotations: { destructiveHint: true },
|
|
592
|
-
}, async ({ items }) => {
|
|
713
|
+
}, safeToolHandler(async ({ items }) => {
|
|
714
|
+
invalidateCache();
|
|
593
715
|
const conn = getConnector(bridge);
|
|
594
716
|
const result = await conn.batchCreateVariables(items);
|
|
595
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }
|
|
596
|
-
});
|
|
717
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
|
|
718
|
+
}));
|
|
597
719
|
server.registerTool("figma_batch_update_variables", {
|
|
598
720
|
description: "Update up to 100 variables. Each item: variableId, modeId, value. Returns updated and failed lists.",
|
|
599
721
|
inputSchema: {
|
|
@@ -604,11 +726,12 @@ export async function main() {
|
|
|
604
726
|
})).max(100),
|
|
605
727
|
},
|
|
606
728
|
annotations: { destructiveHint: true },
|
|
607
|
-
}, async ({ items }) => {
|
|
729
|
+
}, safeToolHandler(async ({ items }) => {
|
|
730
|
+
invalidateCache();
|
|
608
731
|
const conn = getConnector(bridge);
|
|
609
732
|
const result = await conn.batchUpdateVariables(items);
|
|
610
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }
|
|
611
|
-
});
|
|
733
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
|
|
734
|
+
}));
|
|
612
735
|
server.registerTool("figma_setup_design_tokens", {
|
|
613
736
|
description: "Atomically create a variable collection + modes + variables. Rollback on any error. Params: collectionName, modes (array), tokens (array of { name, type?, value? or values? }).",
|
|
614
737
|
inputSchema: {
|
|
@@ -622,20 +745,22 @@ export async function main() {
|
|
|
622
745
|
})),
|
|
623
746
|
},
|
|
624
747
|
annotations: { destructiveHint: true },
|
|
625
|
-
}, async (p) => {
|
|
748
|
+
}, safeToolHandler(async (p) => {
|
|
749
|
+
invalidateCache();
|
|
626
750
|
const conn = getConnector(bridge);
|
|
627
751
|
const result = await conn.setupDesignTokens(p);
|
|
628
|
-
return { content: [{ type: "text", text: JSON.stringify(result
|
|
629
|
-
});
|
|
752
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
753
|
+
}));
|
|
630
754
|
server.registerTool("figma_arrange_component_set", {
|
|
631
755
|
description: "Combine multiple component nodes into one Figma component set (combineAsVariants). Params: nodeIds (array of at least 2 component node IDs). Returns new component set nodeId.",
|
|
632
756
|
inputSchema: { nodeIds: z.array(z.string()).min(2) },
|
|
633
757
|
annotations: { destructiveHint: true },
|
|
634
|
-
}, async ({ nodeIds }) => {
|
|
758
|
+
}, safeToolHandler(async ({ nodeIds }) => {
|
|
759
|
+
invalidateCache();
|
|
635
760
|
const conn = getConnector(bridge);
|
|
636
761
|
const result = await conn.arrangeComponentSet(nodeIds);
|
|
637
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }
|
|
638
|
-
});
|
|
762
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
|
|
763
|
+
}));
|
|
639
764
|
// ---- figma_check_design_parity (design–code gap analysis) ----
|
|
640
765
|
server.registerTool("figma_check_design_parity", {
|
|
641
766
|
description: "Compare Figma design tokens (variables + styles) with code-side tokens. Critical for design-code gap analysis. Returns matching, inFigmaOnly, inCodeOnly, and divergent (same name, different value). Optional codeTokens: JSON string of expected tokens, e.g. {\"primary\": \"#0066cc\", \"spacing.md\": 16} or {\"primary\": {\"value\": \"#0066cc\"}}.",
|
|
@@ -714,7 +839,7 @@ export async function main() {
|
|
|
714
839
|
content: [
|
|
715
840
|
{
|
|
716
841
|
type: "text",
|
|
717
|
-
text: JSON.stringify({ success: false, error: "codeTokens must be valid JSON" }
|
|
842
|
+
text: JSON.stringify({ success: false, error: "codeTokens must be valid JSON" }),
|
|
718
843
|
},
|
|
719
844
|
],
|
|
720
845
|
isError: true,
|
|
@@ -729,16 +854,18 @@ export async function main() {
|
|
|
729
854
|
if (codeVal === undefined) {
|
|
730
855
|
inFigmaOnly.push({ name, value: figVal });
|
|
731
856
|
}
|
|
732
|
-
else if (normalizeForCompare(figVal) === normalizeForCompare(codeVal)) {
|
|
733
|
-
matching.push({ name, value: figVal });
|
|
734
|
-
}
|
|
735
857
|
else {
|
|
736
|
-
|
|
858
|
+
codeMap.delete(name);
|
|
859
|
+
if (normalizeForCompare(figVal) === normalizeForCompare(codeVal)) {
|
|
860
|
+
matching.push({ name, value: figVal });
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
divergent.push({ name, figmaValue: figVal, codeValue: codeVal });
|
|
864
|
+
}
|
|
737
865
|
}
|
|
738
866
|
}
|
|
739
867
|
for (const [name, codeVal] of codeMap) {
|
|
740
|
-
|
|
741
|
-
inCodeOnly.push({ name, value: codeVal });
|
|
868
|
+
inCodeOnly.push({ name, value: codeVal });
|
|
742
869
|
}
|
|
743
870
|
const out = {
|
|
744
871
|
success: true,
|
|
@@ -753,12 +880,12 @@ export async function main() {
|
|
|
753
880
|
inFigmaOnly,
|
|
754
881
|
inCodeOnly,
|
|
755
882
|
};
|
|
756
|
-
return { content: [{ type: "text", text: JSON.stringify(out
|
|
883
|
+
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
757
884
|
}
|
|
758
885
|
catch (err) {
|
|
759
886
|
const msg = err instanceof Error ? err.message : String(err);
|
|
760
887
|
return {
|
|
761
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }
|
|
888
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }) }],
|
|
762
889
|
isError: true,
|
|
763
890
|
};
|
|
764
891
|
}
|
|
@@ -822,12 +949,12 @@ export async function main() {
|
|
|
822
949
|
: { id: s.id, name: s.name, fontSize: s.fontSize ?? s.style?.fontSize, fontName: s.fontName ?? s.style?.fontName }),
|
|
823
950
|
},
|
|
824
951
|
};
|
|
825
|
-
return { content: [{ type: "text", text: JSON.stringify(out
|
|
952
|
+
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
826
953
|
}
|
|
827
954
|
catch (err) {
|
|
828
955
|
const msg = err instanceof Error ? err.message : String(err);
|
|
829
956
|
return {
|
|
830
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }
|
|
957
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }) }],
|
|
831
958
|
isError: true,
|
|
832
959
|
};
|
|
833
960
|
}
|
|
@@ -871,7 +998,7 @@ export async function main() {
|
|
|
871
998
|
message: msg,
|
|
872
999
|
...(startError && { startError }),
|
|
873
1000
|
...(portHint && { portHint }),
|
|
874
|
-
}
|
|
1001
|
+
}),
|
|
875
1002
|
}],
|
|
876
1003
|
};
|
|
877
1004
|
});
|
|
@@ -913,10 +1040,10 @@ export async function main() {
|
|
|
913
1040
|
return { id: frame.id, name: frame.name, width: frame.width, height: frame.height, x: frame.x, y: frame.y };
|
|
914
1041
|
`;
|
|
915
1042
|
const result = await conn.executeCodeViaUI(code, 10000);
|
|
916
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }
|
|
1043
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
|
|
917
1044
|
}
|
|
918
1045
|
catch (err) {
|
|
919
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }
|
|
1046
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }) }], isError: true };
|
|
920
1047
|
}
|
|
921
1048
|
});
|
|
922
1049
|
server.registerTool("figma_create_text", {
|
|
@@ -947,10 +1074,10 @@ export async function main() {
|
|
|
947
1074
|
return { id: node.id, name: node.name, characters: node.characters };
|
|
948
1075
|
`;
|
|
949
1076
|
const result = await conn.executeCodeViaUI(code, 10000);
|
|
950
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }
|
|
1077
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
|
|
951
1078
|
}
|
|
952
1079
|
catch (err) {
|
|
953
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }
|
|
1080
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }) }], isError: true };
|
|
954
1081
|
}
|
|
955
1082
|
});
|
|
956
1083
|
server.registerTool("figma_create_rectangle", {
|
|
@@ -979,10 +1106,10 @@ export async function main() {
|
|
|
979
1106
|
return { id: rect.id, name: rect.name };
|
|
980
1107
|
`;
|
|
981
1108
|
const result = await conn.executeCodeViaUI(code, 10000);
|
|
982
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }
|
|
1109
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
|
|
983
1110
|
}
|
|
984
1111
|
catch (err) {
|
|
985
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }
|
|
1112
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }) }], isError: true };
|
|
986
1113
|
}
|
|
987
1114
|
});
|
|
988
1115
|
server.registerTool("figma_create_group", {
|
|
@@ -1006,10 +1133,10 @@ export async function main() {
|
|
|
1006
1133
|
return { id: group.id, name: group.name, childCount: group.children.length };
|
|
1007
1134
|
`;
|
|
1008
1135
|
const result = await conn.executeCodeViaUI(code, 10000);
|
|
1009
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }
|
|
1136
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
|
|
1010
1137
|
}
|
|
1011
1138
|
catch (err) {
|
|
1012
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }
|
|
1139
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }) }], isError: true };
|
|
1013
1140
|
}
|
|
1014
1141
|
});
|
|
1015
1142
|
// ---- figma_export_nodes (batch SVG/PNG/JPG/PDF export) ----
|
|
@@ -1062,13 +1189,13 @@ export async function main() {
|
|
|
1062
1189
|
...(r.base64 && { base64: r.base64 }),
|
|
1063
1190
|
...(r.error && { error: r.error }),
|
|
1064
1191
|
})),
|
|
1065
|
-
}
|
|
1192
|
+
}),
|
|
1066
1193
|
});
|
|
1067
1194
|
return { content: contentBlocks };
|
|
1068
1195
|
}
|
|
1069
1196
|
catch (err) {
|
|
1070
1197
|
return {
|
|
1071
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }
|
|
1198
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }) }],
|
|
1072
1199
|
isError: true,
|
|
1073
1200
|
};
|
|
1074
1201
|
}
|
|
@@ -1087,7 +1214,7 @@ export async function main() {
|
|
|
1087
1214
|
const code = `
|
|
1088
1215
|
if (!figma.teamLibrary) return { success: false, error: "teamLibrary API not available" };
|
|
1089
1216
|
const availableLibs = await figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync();
|
|
1090
|
-
const availableComps =
|
|
1217
|
+
const availableComps = typeof figma.teamLibrary.getAvailableLibraryComponentsAsync === 'function' ? await figma.teamLibrary.getAvailableLibraryComponentsAsync() : [];
|
|
1091
1218
|
return {
|
|
1092
1219
|
variableCollections: availableLibs.map(c => ({ name: c.name, key: c.key, libraryName: c.libraryName })),
|
|
1093
1220
|
note: "Use figma_search_components for file-local components. Team library component search requires REST API (figma_rest_api)."
|
|
@@ -1099,10 +1226,10 @@ export async function main() {
|
|
|
1099
1226
|
const q = query.toLowerCase();
|
|
1100
1227
|
data.variableCollections = data.variableCollections.filter((c) => (c.name || "").toLowerCase().includes(q) || (c.libraryName || "").toLowerCase().includes(q));
|
|
1101
1228
|
}
|
|
1102
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }
|
|
1229
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }) }] };
|
|
1103
1230
|
}
|
|
1104
1231
|
catch (err) {
|
|
1105
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }
|
|
1232
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }) }], isError: true };
|
|
1106
1233
|
}
|
|
1107
1234
|
});
|
|
1108
1235
|
// ---- figma_plugin_diagnostics ----
|
|
@@ -1132,7 +1259,7 @@ export async function main() {
|
|
|
1132
1259
|
hasRestToken: !!tokenInfo,
|
|
1133
1260
|
rateLimit: tokenInfo?.rateLimit || null,
|
|
1134
1261
|
nodeVersion: process.version,
|
|
1135
|
-
}
|
|
1262
|
+
}),
|
|
1136
1263
|
}],
|
|
1137
1264
|
};
|
|
1138
1265
|
});
|
|
@@ -1154,7 +1281,7 @@ export async function main() {
|
|
|
1154
1281
|
text: JSON.stringify({
|
|
1155
1282
|
success: false,
|
|
1156
1283
|
error: "Port değişikliği zaten devam ediyor. Lütfen tamamlanmasını bekleyin.",
|
|
1157
|
-
}
|
|
1284
|
+
}),
|
|
1158
1285
|
}],
|
|
1159
1286
|
isError: true,
|
|
1160
1287
|
};
|
|
@@ -1173,7 +1300,7 @@ export async function main() {
|
|
|
1173
1300
|
previousPort: oldPort,
|
|
1174
1301
|
newPort: result.port,
|
|
1175
1302
|
message: `Bridge restarted on port ${result.port}. Figma plugin'de Port: ${result.port} ayarlayın ve bağlanmasını bekleyin.`,
|
|
1176
|
-
}
|
|
1303
|
+
}),
|
|
1177
1304
|
}],
|
|
1178
1305
|
};
|
|
1179
1306
|
}
|
|
@@ -1187,7 +1314,7 @@ export async function main() {
|
|
|
1187
1314
|
attemptedPort: newPort,
|
|
1188
1315
|
error: result.error || "Port bind failed",
|
|
1189
1316
|
message: `Port ${newPort} bağlanamadı. Başka bir port deneyin (5454–5470).`,
|
|
1190
|
-
}
|
|
1317
|
+
}),
|
|
1191
1318
|
}],
|
|
1192
1319
|
isError: true,
|
|
1193
1320
|
};
|
|
@@ -1208,7 +1335,7 @@ export async function main() {
|
|
|
1208
1335
|
}, async ({ token }) => {
|
|
1209
1336
|
if (!token.startsWith("figd_")) {
|
|
1210
1337
|
return {
|
|
1211
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Token must start with 'figd_'" }
|
|
1338
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Token must start with 'figd_'" }) }],
|
|
1212
1339
|
isError: true,
|
|
1213
1340
|
};
|
|
1214
1341
|
}
|
|
@@ -1223,7 +1350,7 @@ export async function main() {
|
|
|
1223
1350
|
clearTimeout(timeout);
|
|
1224
1351
|
if (!res.ok) {
|
|
1225
1352
|
return {
|
|
1226
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: `Token validation failed: ${res.status} ${res.statusText}` }
|
|
1353
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: `Token validation failed: ${res.status} ${res.statusText}` }) }],
|
|
1227
1354
|
isError: true,
|
|
1228
1355
|
};
|
|
1229
1356
|
}
|
|
@@ -1241,12 +1368,12 @@ export async function main() {
|
|
|
1241
1368
|
user: me.handle || me.email || "unknown",
|
|
1242
1369
|
message: "Token set. REST API tools are now available.",
|
|
1243
1370
|
rateLimit: limit > 0 ? { remaining, limit, resetAt } : undefined,
|
|
1244
|
-
}
|
|
1371
|
+
}) }],
|
|
1245
1372
|
};
|
|
1246
1373
|
}
|
|
1247
1374
|
catch (err) {
|
|
1248
1375
|
return {
|
|
1249
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: `Token validation error: ${err instanceof Error ? err.message : String(err)}` }
|
|
1376
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: `Token validation error: ${err instanceof Error ? err.message : String(err)}` }) }],
|
|
1250
1377
|
isError: true,
|
|
1251
1378
|
};
|
|
1252
1379
|
}
|
|
@@ -1256,7 +1383,7 @@ export async function main() {
|
|
|
1256
1383
|
inputSchema: {},
|
|
1257
1384
|
}, async () => {
|
|
1258
1385
|
bridge.clearFigmaRestToken();
|
|
1259
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: true, message: "Token cleared." }
|
|
1386
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, message: "Token cleared." }) }] };
|
|
1260
1387
|
});
|
|
1261
1388
|
server.registerTool("figma_rest_api", {
|
|
1262
1389
|
description: "Call Figma REST API directly. Requires a token set via figma_set_rest_token. " +
|
|
@@ -1276,7 +1403,7 @@ export async function main() {
|
|
|
1276
1403
|
content: [{ type: "text", text: JSON.stringify({
|
|
1277
1404
|
success: false,
|
|
1278
1405
|
error: "No Figma REST API token set. Use figma_set_rest_token first. Or enter token in Figma plugin Advanced panel.",
|
|
1279
|
-
}
|
|
1406
|
+
}) }],
|
|
1280
1407
|
isError: true,
|
|
1281
1408
|
};
|
|
1282
1409
|
}
|
|
@@ -1289,7 +1416,7 @@ export async function main() {
|
|
|
1289
1416
|
success: false,
|
|
1290
1417
|
error: `API rate limit exhausted (0/${rl.limit}). Resets at ${resetDate.toISOString()}. Wait and retry.`,
|
|
1291
1418
|
rateLimit: rl,
|
|
1292
|
-
}
|
|
1419
|
+
}) }],
|
|
1293
1420
|
isError: true,
|
|
1294
1421
|
};
|
|
1295
1422
|
}
|
|
@@ -1332,7 +1459,7 @@ export async function main() {
|
|
|
1332
1459
|
success: false, status: 429,
|
|
1333
1460
|
error: `Rate limited. ${attempt + 1} attempts, total ${Math.round(elapsed / 1000)}s. Retry later.`,
|
|
1334
1461
|
rateLimit: limit > 0 ? { remaining, limit, resetAt } : undefined,
|
|
1335
|
-
}
|
|
1462
|
+
}) }],
|
|
1336
1463
|
isError: true,
|
|
1337
1464
|
};
|
|
1338
1465
|
}
|
|
@@ -1354,7 +1481,7 @@ export async function main() {
|
|
|
1354
1481
|
success: false, status: res.status, statusText: res.statusText,
|
|
1355
1482
|
error: responseData,
|
|
1356
1483
|
rateLimit: limit > 0 ? { remaining, limit, resetAt } : undefined,
|
|
1357
|
-
}
|
|
1484
|
+
}) }],
|
|
1358
1485
|
isError: true,
|
|
1359
1486
|
};
|
|
1360
1487
|
}
|
|
@@ -1381,7 +1508,7 @@ export async function main() {
|
|
|
1381
1508
|
data: result.data,
|
|
1382
1509
|
...(result.wasTruncated && { _responseGuard: { originalSizeKB: Math.round(result.originalSizeKB), truncatedSizeKB: Math.round(result.truncatedSizeKB) } }),
|
|
1383
1510
|
rateLimit: limit > 0 ? { remaining, limit, resetAt } : undefined,
|
|
1384
|
-
}
|
|
1511
|
+
}),
|
|
1385
1512
|
});
|
|
1386
1513
|
return { content: contentBlocks };
|
|
1387
1514
|
}
|
|
@@ -1396,14 +1523,14 @@ export async function main() {
|
|
|
1396
1523
|
content: [{ type: "text", text: JSON.stringify({
|
|
1397
1524
|
success: false,
|
|
1398
1525
|
error: `REST API call failed after ${attempt + 1} attempts: ${err instanceof Error ? err.message : String(err)}`,
|
|
1399
|
-
}
|
|
1526
|
+
}) }],
|
|
1400
1527
|
isError: true,
|
|
1401
1528
|
};
|
|
1402
1529
|
}
|
|
1403
1530
|
}
|
|
1404
1531
|
// Should not reach here
|
|
1405
1532
|
return {
|
|
1406
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Unexpected: all retries exhausted" }
|
|
1533
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: "Unexpected: all retries exhausted" }) }],
|
|
1407
1534
|
isError: true,
|
|
1408
1535
|
};
|
|
1409
1536
|
});
|
|
@@ -1418,7 +1545,7 @@ export async function main() {
|
|
|
1418
1545
|
content: [{ type: "text", text: JSON.stringify({
|
|
1419
1546
|
hasToken: false,
|
|
1420
1547
|
message: "No token set. Use figma_set_rest_token to add one.",
|
|
1421
|
-
}
|
|
1548
|
+
}) }],
|
|
1422
1549
|
};
|
|
1423
1550
|
}
|
|
1424
1551
|
const rl = tokenInfo.rateLimit;
|
|
@@ -1439,11 +1566,12 @@ export async function main() {
|
|
|
1439
1566
|
rateLimit: rl || null,
|
|
1440
1567
|
...(warning && { warning }),
|
|
1441
1568
|
message: warning || "Token is set. REST API tools are available.",
|
|
1442
|
-
}
|
|
1569
|
+
}) }],
|
|
1443
1570
|
};
|
|
1444
1571
|
});
|
|
1445
1572
|
const shutdown = () => {
|
|
1446
1573
|
logger.info("Shutting down plugin-only MCP server...");
|
|
1574
|
+
closeAuditLog();
|
|
1447
1575
|
try {
|
|
1448
1576
|
bridge.stop();
|
|
1449
1577
|
}
|