@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/agents/ds-auditor.md +6 -0
  3. package/agents/screen-builder.md +6 -0
  4. package/agents/token-syncer.md +6 -0
  5. package/dist/core/audit-log.d.ts +4 -0
  6. package/dist/core/audit-log.d.ts.map +1 -1
  7. package/dist/core/audit-log.js +12 -0
  8. package/dist/core/audit-log.js.map +1 -1
  9. package/dist/core/config.d.ts +1 -1
  10. package/dist/core/config.d.ts.map +1 -1
  11. package/dist/core/config.js +6 -2
  12. package/dist/core/config.js.map +1 -1
  13. package/dist/core/plugin-bridge-connector.d.ts +16 -16
  14. package/dist/core/plugin-bridge-connector.d.ts.map +1 -1
  15. package/dist/core/plugin-bridge-connector.js +24 -3
  16. package/dist/core/plugin-bridge-connector.js.map +1 -1
  17. package/dist/core/plugin-bridge-server.d.ts +5 -3
  18. package/dist/core/plugin-bridge-server.d.ts.map +1 -1
  19. package/dist/core/plugin-bridge-server.js +34 -22
  20. package/dist/core/plugin-bridge-server.js.map +1 -1
  21. package/dist/core/response-cache.d.ts +16 -0
  22. package/dist/core/response-cache.d.ts.map +1 -0
  23. package/dist/core/response-cache.js +46 -0
  24. package/dist/core/response-cache.js.map +1 -0
  25. package/dist/core/response-guard.d.ts.map +1 -1
  26. package/dist/core/response-guard.js +19 -13
  27. package/dist/core/response-guard.js.map +1 -1
  28. package/dist/core/types/figma.d.ts +20 -0
  29. package/dist/core/types/figma.d.ts.map +1 -1
  30. package/dist/core/version.d.ts +1 -1
  31. package/dist/core/version.js +1 -1
  32. package/dist/local-plugin-only.d.ts.map +1 -1
  33. package/dist/local-plugin-only.js +271 -143
  34. package/dist/local-plugin-only.js.map +1 -1
  35. package/f-mcp-plugin/code.js +40 -3
  36. package/f-mcp-plugin/manifest.json +1 -1
  37. package/f-mcp-plugin/ui.html +45 -9
  38. package/hooks/hooks.json +1 -1
  39. package/package.json +1 -1
  40. package/skills/ai-handoff-export/SKILL.md +8 -0
  41. package/skills/component-documentation/SKILL.md +8 -0
  42. package/skills/figjam-diagram-builder/SKILL.md +8 -0
  43. package/skills/figma-screen-analyzer/SKILL.md +8 -0
  44. package/skills/ux-copy-guidance/SKILL.md +8 -0
  45. 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: "1.7.24",
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
- }, null, 0),
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." }, null, 0) }],
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, null, 0);
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 }, null, 0) }],
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." }, null, 0) }],
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, null, 0);
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 }, null, 0) }],
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, null, 0) }] };
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, null, 0) }] };
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 || {}, null, 0) }] };
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(5000),
336
+ timeout: z.number().optional().default(15000),
288
337
  },
289
338
  annotations: { destructiveHint: true },
290
- }, async ({ figmaUrl, fileKey, code, timeout }) => {
291
- const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
292
- const result = await conn.executeCodeViaUI(code, timeout);
293
- return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
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, null, 0) }] };
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, null, 0) }] };
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, null, 0) }] };
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, null, 0) }] };
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, null, 0) }] };
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, null, 0) }] };
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, null, 0) }] };
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, null, 0) }] };
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, null, 0) }] };
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, null, 0) }] };
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, null, 0) }] };
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 }, null, 0) }] };
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, null, 0) }] };
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, null, 0) }] };
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 }, null, 0) }] };
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
- const seen = new Set();
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
- const key = `${entry.time}-${JSON.stringify(entry.args)}`;
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
- await new Promise((r) => setTimeout(r, 1000));
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 }, null, 0) }],
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" }, null, 0) }] };
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, null, 0) }] };
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, null, 0) }] };
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, null, 0) }] };
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 }, null, 0) }] };
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 }, null, 0) }] };
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, null, 0) }] };
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 }, null, 0) }] };
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" }, null, 0),
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
- divergent.push({ name, figmaValue: figVal, codeValue: codeVal });
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
- if (!figmaMap.has(name))
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, null, 0) }] };
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 }, null, 0) }],
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, null, 0) }] };
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 }, null, 0) }],
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
- }, null, 0),
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 }, null, 0) }] };
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) }, null, 0) }], isError: true };
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 }, null, 0) }] };
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) }, null, 0) }], isError: true };
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 }, null, 0) }] };
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) }, null, 0) }], isError: true };
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 }, null, 0) }] };
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) }, null, 0) }], isError: true };
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
- }, null, 0),
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) }, null, 0) }],
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 = await figma.teamLibrary.getAvailableLibraryComponentsAsync ? [] : [];
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 }, null, 0) }] };
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) }, null, 0) }], isError: true };
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
- }, null, 0),
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
- }, null, 0),
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
- }, null, 0),
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
- }, null, 0),
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_'" }, null, 0) }],
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}` }, null, 0) }],
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
- }, null, 0) }],
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)}` }, null, 0) }],
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." }, null, 0) }] };
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
- }, null, 0) }],
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
- }, null, 0) }],
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
- }, null, 0) }],
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
- }, null, 0) }],
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
- }, null, 0),
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
- }, null, 0) }],
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" }, null, 0) }],
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
- }, null, 0) }],
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
- }, null, 0) }],
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
  }