@atezer/figma-mcp-bridge 1.7.25 → 1.7.26

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 (42) hide show
  1. package/CHANGELOG.md +37 -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 +2 -1
  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 +6 -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 +202 -140
  34. package/dist/local-plugin-only.js.map +1 -1
  35. package/f-mcp-plugin/manifest.json +1 -1
  36. package/package.json +1 -1
  37. package/skills/ai-handoff-export/SKILL.md +8 -0
  38. package/skills/component-documentation/SKILL.md +8 -0
  39. package/skills/figjam-diagram-builder/SKILL.md +8 -0
  40. package/skills/figma-screen-analyzer/SKILL.md +8 -0
  41. package/skills/ux-copy-guidance/SKILL.md +8 -0
  42. 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,21 @@ 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
+ /** Wrap a tool handler with try-catch to prevent unhandled rejections. */
74
+ function safeToolHandler(handler) {
75
+ return async (params) => {
76
+ try {
77
+ return await handler(params);
78
+ }
79
+ catch (err) {
80
+ const msg = err instanceof Error ? err.message : String(err);
81
+ return {
82
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }) }],
83
+ isError: true,
84
+ };
85
+ }
86
+ };
87
+ }
70
88
  function getConnector(bridge, fileKey) {
71
89
  if (!bridge.isConnected(fileKey)) {
72
90
  if (fileKey) {
@@ -87,9 +105,12 @@ export async function main() {
87
105
  const auditLogPath = config.local?.auditLogPath;
88
106
  const bridge = new PluginBridgeServer(port, { auditLogPath });
89
107
  bridge.start();
108
+ const cache = new ResponseCache();
109
+ /** Invalidate cache after any mutating operation. */
110
+ function invalidateCache() { cache.invalidate(); }
90
111
  const server = new McpServer({
91
112
  name: "F-MCP ATezer Bridge (Plugin-only)",
92
- version: "1.7.24",
113
+ version: FMCP_VERSION,
93
114
  });
94
115
  // ---- figma_list_connected_files (multi-client discovery) ----
95
116
  server.registerTool("figma_list_connected_files", {
@@ -108,7 +129,7 @@ export async function main() {
108
129
  message: files.length === 0
109
130
  ? "No plugins connected. Open Figma and run the F-MCP ATezer Bridge plugin."
110
131
  : `${files.length} plugin(s) connected. Use fileKey parameter in other tools to target a specific file.`,
111
- }, null, 0),
132
+ }),
112
133
  }],
113
134
  };
114
135
  });
@@ -132,7 +153,7 @@ export async function main() {
132
153
  const resolvedKey = resolveFileKey(figmaUrl, fileKey);
133
154
  if (figmaUrl && !resolvedKey) {
134
155
  return {
135
- content: [{ type: "text", text: JSON.stringify({ success: false, error: "Invalid Figma/FigJam URL: could not extract file key." }, null, 0) }],
156
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "Invalid Figma/FigJam URL: could not extract file key." }) }],
136
157
  isError: true,
137
158
  };
138
159
  }
@@ -149,13 +170,13 @@ export async function main() {
149
170
  ? JSON.stringify({ success: false, error: "No data from plugin" })
150
171
  : typeof data === "string"
151
172
  ? data
152
- : JSON.stringify(data, null, 0);
173
+ : JSON.stringify(data);
153
174
  return { content: [{ type: "text", text }] };
154
175
  }
155
176
  catch (err) {
156
177
  const msg = err instanceof Error ? err.message : String(err);
157
178
  return {
158
- content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }, null, 0) }],
179
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }) }],
159
180
  isError: true,
160
181
  };
161
182
  }
@@ -182,7 +203,7 @@ export async function main() {
182
203
  const { fileKey: resolvedKey, nodeId: resolvedNodeId } = resolveDesignContextParams({ figmaUrl, fileKey, nodeId });
183
204
  if (figmaUrl && !resolvedKey) {
184
205
  return {
185
- content: [{ type: "text", text: JSON.stringify({ success: false, error: "Invalid Figma/FigJam URL: could not extract file key." }, null, 0) }],
206
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "Invalid Figma/FigJam URL: could not extract file key." }) }],
186
207
  isError: true,
187
208
  };
188
209
  }
@@ -203,13 +224,13 @@ export async function main() {
203
224
  ? JSON.stringify({ success: false, error: "No data from plugin" })
204
225
  : typeof data === "string"
205
226
  ? data
206
- : JSON.stringify(data, null, 0);
227
+ : JSON.stringify(data);
207
228
  return { content: [{ type: "text", text }] };
208
229
  }
209
230
  catch (err) {
210
231
  const msg = err instanceof Error ? err.message : String(err);
211
232
  return {
212
- content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }, null, 0) }],
233
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }) }],
213
234
  isError: true,
214
235
  };
215
236
  }
@@ -223,7 +244,7 @@ export async function main() {
223
244
  verbosity: z.enum(["inventory", "summary", "standard", "full"]).optional().default("summary"),
224
245
  },
225
246
  annotations: { readOnlyHint: true },
226
- }, async ({ figmaUrl, fileKey, verbosity }) => {
247
+ }, safeToolHandler(async ({ figmaUrl, fileKey, verbosity }) => {
227
248
  const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
228
249
  const raw = await conn.getVariablesFromPluginUI();
229
250
  if (!raw || !raw.variables) {
@@ -247,8 +268,8 @@ export async function main() {
247
268
  valuesByMode: v.valuesByMode,
248
269
  }));
249
270
  }
250
- return { content: [{ type: "text", text: JSON.stringify(out, null, 0) }] };
251
- });
271
+ return { content: [{ type: "text", text: JSON.stringify(out) }] };
272
+ }));
252
273
  // ---- figma_get_component ----
253
274
  server.registerTool("figma_get_component", {
254
275
  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 +279,11 @@ export async function main() {
258
279
  nodeId: z.string(),
259
280
  },
260
281
  annotations: { readOnlyHint: true },
261
- }, async ({ figmaUrl, fileKey, nodeId }) => {
282
+ }, safeToolHandler(async ({ figmaUrl, fileKey, nodeId }) => {
262
283
  const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
263
284
  const result = await conn.getComponentFromPluginUI(nodeId);
264
- return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
265
- });
285
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
286
+ }));
266
287
  // ---- figma_get_styles (plugin only) ----
267
288
  server.registerTool("figma_get_styles", {
268
289
  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 +293,11 @@ export async function main() {
272
293
  verbosity: z.enum(["summary", "full"]).optional().default("summary"),
273
294
  },
274
295
  annotations: { readOnlyHint: true },
275
- }, async ({ figmaUrl, fileKey, verbosity }) => {
296
+ }, safeToolHandler(async ({ figmaUrl, fileKey, verbosity }) => {
276
297
  const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
277
298
  const data = await conn.getLocalStyles(verbosity);
278
- return { content: [{ type: "text", text: JSON.stringify(data || {}, null, 0) }] };
279
- });
299
+ return { content: [{ type: "text", text: JSON.stringify(data || {}) }] };
300
+ }));
280
301
  // ---- figma_execute ----
281
302
  server.registerTool("figma_execute", {
282
303
  description: "Run JavaScript in the Figma plugin context. Full Plugin API available. Use fileKey or figmaUrl to target a specific file.",
@@ -287,11 +308,18 @@ export async function main() {
287
308
  timeout: z.number().optional().default(5000),
288
309
  },
289
310
  annotations: { destructiveHint: true },
290
- }, async ({ figmaUrl, fileKey, code, timeout }) => {
311
+ }, safeToolHandler(async ({ figmaUrl, fileKey, code, timeout }) => {
312
+ if (code.length > 50000) {
313
+ return {
314
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "Code too long (max 50,000 characters). Break into smaller pieces." }) }],
315
+ isError: true,
316
+ };
317
+ }
318
+ invalidateCache();
291
319
  const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
292
320
  const result = await conn.executeCodeViaUI(code, timeout);
293
- return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
294
- });
321
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
322
+ }));
295
323
  // ---- figma_capture_screenshot ----
296
324
  server.registerTool("figma_capture_screenshot", {
297
325
  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 +331,11 @@ export async function main() {
303
331
  scale: z.number().optional().default(2),
304
332
  },
305
333
  annotations: { readOnlyHint: true },
306
- }, async ({ figmaUrl, fileKey, nodeId, format, scale }) => {
334
+ }, safeToolHandler(async ({ figmaUrl, fileKey, nodeId, format, scale }) => {
307
335
  const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
308
336
  const result = await conn.captureScreenshot(nodeId ?? null, { format, scale });
309
- return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
310
- });
337
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
338
+ }));
311
339
  // ---- figma_set_instance_properties ----
312
340
  server.registerTool("figma_set_instance_properties", {
313
341
  description: "Set component instance properties (TEXT, BOOLEAN, VARIANT, etc.). Use fileKey or figmaUrl to target a specific file.",
@@ -318,11 +346,12 @@ export async function main() {
318
346
  properties: z.record(z.union([z.string(), z.boolean()])),
319
347
  },
320
348
  annotations: { destructiveHint: true },
321
- }, async ({ figmaUrl, fileKey, nodeId, properties }) => {
349
+ }, safeToolHandler(async ({ figmaUrl, fileKey, nodeId, properties }) => {
350
+ invalidateCache();
322
351
  const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
323
352
  const result = await conn.setInstanceProperties(nodeId, properties);
324
- return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
325
- });
353
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
354
+ }));
326
355
  // ---- Variable CRUD ----
327
356
  server.registerTool("figma_update_variable", {
328
357
  description: "Update a variable value in a mode. Get IDs from figma_get_variables.",
@@ -332,11 +361,12 @@ export async function main() {
332
361
  value: z.union([z.string(), z.number(), z.boolean()]),
333
362
  },
334
363
  annotations: { destructiveHint: true },
335
- }, async (p) => {
364
+ }, safeToolHandler(async (p) => {
365
+ invalidateCache();
336
366
  const conn = getConnector(bridge);
337
367
  const result = await conn.updateVariable(p.variableId, p.modeId, p.value);
338
- return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
339
- });
368
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
369
+ }));
340
370
  server.registerTool("figma_create_variable", {
341
371
  description: "Create a variable in a collection. Get collectionId from figma_get_variables.",
342
372
  inputSchema: {
@@ -346,65 +376,72 @@ export async function main() {
346
376
  options: z.record(z.any()).optional(),
347
377
  },
348
378
  annotations: { destructiveHint: true },
349
- }, async (p) => {
379
+ }, safeToolHandler(async (p) => {
380
+ invalidateCache();
350
381
  const conn = getConnector(bridge);
351
382
  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
- });
383
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
384
+ }));
354
385
  server.registerTool("figma_create_variable_collection", {
355
386
  description: "Create a variable collection.",
356
387
  inputSchema: { name: z.string(), options: z.record(z.any()).optional() },
357
388
  annotations: { destructiveHint: true },
358
- }, async (p) => {
389
+ }, safeToolHandler(async (p) => {
390
+ invalidateCache();
359
391
  const conn = getConnector(bridge);
360
392
  const result = await conn.createVariableCollection(p.name, p.options);
361
- return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
362
- });
393
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
394
+ }));
363
395
  server.registerTool("figma_delete_variable", {
364
396
  description: "Delete a variable.",
365
397
  inputSchema: { variableId: z.string() },
366
398
  annotations: { destructiveHint: true },
367
- }, async (p) => {
399
+ }, safeToolHandler(async (p) => {
400
+ invalidateCache();
368
401
  const conn = getConnector(bridge);
369
402
  const result = await conn.deleteVariable(p.variableId);
370
- return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
371
- });
403
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
404
+ }));
372
405
  server.registerTool("figma_delete_variable_collection", {
373
406
  description: "Delete a variable collection.",
374
407
  inputSchema: { collectionId: z.string() },
375
408
  annotations: { destructiveHint: true },
376
- }, async (p) => {
409
+ }, safeToolHandler(async (p) => {
410
+ invalidateCache();
377
411
  const conn = getConnector(bridge);
378
412
  const result = await conn.deleteVariableCollection(p.collectionId);
379
- return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
380
- });
413
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
414
+ }));
381
415
  server.registerTool("figma_rename_variable", {
382
416
  description: "Rename a variable.",
383
417
  inputSchema: { variableId: z.string(), newName: z.string() },
384
418
  annotations: { destructiveHint: true },
385
- }, async (p) => {
419
+ }, safeToolHandler(async (p) => {
420
+ invalidateCache();
386
421
  const conn = getConnector(bridge);
387
422
  const result = await conn.renameVariable(p.variableId, p.newName);
388
- return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
389
- });
423
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
424
+ }));
390
425
  server.registerTool("figma_add_mode", {
391
426
  description: "Add a mode to a collection.",
392
427
  inputSchema: { collectionId: z.string(), modeName: z.string() },
393
428
  annotations: { destructiveHint: true },
394
- }, async (p) => {
429
+ }, safeToolHandler(async (p) => {
430
+ invalidateCache();
395
431
  const conn = getConnector(bridge);
396
432
  const result = await conn.addMode(p.collectionId, p.modeName);
397
- return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
398
- });
433
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
434
+ }));
399
435
  server.registerTool("figma_rename_mode", {
400
436
  description: "Rename a mode in a collection.",
401
437
  inputSchema: { collectionId: z.string(), modeId: z.string(), newName: z.string() },
402
438
  annotations: { destructiveHint: true },
403
- }, async (p) => {
439
+ }, safeToolHandler(async (p) => {
440
+ invalidateCache();
404
441
  const conn = getConnector(bridge);
405
442
  const result = await conn.renameMode(p.collectionId, p.modeId, p.newName);
406
- return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
407
- });
443
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
444
+ }));
408
445
  // ---- Design system summary (minimal tokens) ----
409
446
  server.registerTool("figma_get_design_system_summary", {
410
447
  description: "Get a compact overview: variable collection names and component counts. Minimal tokens. Use fileKey or figmaUrl to target a specific file.",
@@ -415,7 +452,7 @@ export async function main() {
415
452
  limit: z.number().min(0).optional(),
416
453
  },
417
454
  annotations: { readOnlyHint: true },
418
- }, async ({ figmaUrl, fileKey, currentPageOnly, limit }) => {
455
+ }, safeToolHandler(async ({ figmaUrl, fileKey, currentPageOnly, limit }) => {
419
456
  const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
420
457
  const [vars, components] = await Promise.all([
421
458
  conn.getVariablesFromPluginUI(),
@@ -430,8 +467,8 @@ export async function main() {
430
467
  components: compData?.totalComponents ?? 0,
431
468
  componentSets: compData?.totalComponentSets ?? 0,
432
469
  };
433
- return { content: [{ type: "text", text: JSON.stringify(out, null, 0) }] };
434
- });
470
+ return { content: [{ type: "text", text: JSON.stringify(out) }] };
471
+ }));
435
472
  // ---- figma_search_components ----
436
473
  server.registerTool("figma_search_components", {
437
474
  description: "Search local components by name. Returns nodeIds and names. No REST API. Use fileKey or figmaUrl to target a specific file.",
@@ -443,7 +480,7 @@ export async function main() {
443
480
  limit: z.number().min(0).optional(),
444
481
  },
445
482
  annotations: { readOnlyHint: true },
446
- }, async ({ figmaUrl, fileKey, query, currentPageOnly, limit }) => {
483
+ }, safeToolHandler(async ({ figmaUrl, fileKey, query, currentPageOnly, limit }) => {
447
484
  const conn = getConnector(bridge, resolveFileKey(figmaUrl, fileKey));
448
485
  const result = (await conn.getLocalComponents({ currentPageOnly, limit }));
449
486
  const data = result?.data;
@@ -456,8 +493,8 @@ export async function main() {
456
493
  list = list.filter((c) => (c.name || "").toLowerCase().includes(q));
457
494
  }
458
495
  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
- });
496
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, components: summary }) }] };
497
+ }));
461
498
  // ---- Node operations (short list) ----
462
499
  server.registerTool("figma_instantiate_component", {
463
500
  description: "Create a component instance. Use componentKey from figma_search_components or nodeId for local components.",
@@ -473,63 +510,80 @@ export async function main() {
473
510
  .optional(),
474
511
  },
475
512
  annotations: { destructiveHint: true },
476
- }, async (p) => {
513
+ }, safeToolHandler(async (p) => {
514
+ invalidateCache();
477
515
  const conn = getConnector(bridge);
478
516
  const result = await conn.instantiateComponent(p.componentKey, p.options || {});
479
- return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
480
- });
517
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
518
+ }));
481
519
  server.registerTool("figma_refresh_variables", {
482
520
  description: "Refresh variables from the file.",
483
521
  inputSchema: {},
484
522
  annotations: { readOnlyHint: false, destructiveHint: false },
485
- }, async () => {
523
+ }, safeToolHandler(async () => {
486
524
  const conn = getConnector(bridge);
487
525
  const result = await conn.refreshVariables();
488
- return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
489
- });
526
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
527
+ }));
490
528
  // ---- Console (plugin buffer, no CDP) ----
491
529
  server.registerTool("figma_get_console_logs", {
492
530
  description: "Get plugin console logs (log/warn/error) from the F-MCP plugin buffer. No CDP. Limit default 50.",
493
531
  inputSchema: { limit: z.number().min(1).max(200).optional().default(50) },
494
532
  annotations: { readOnlyHint: true },
495
- }, async ({ limit }) => {
533
+ }, safeToolHandler(async ({ limit }) => {
496
534
  const conn = getConnector(bridge);
497
535
  const data = await conn.getConsoleLogs(limit);
498
- return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }, null, 0) }] };
499
- });
536
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }) }] };
537
+ }));
500
538
  server.registerTool("figma_watch_console", {
501
539
  description: "Stream new plugin console logs until timeout. Polls the plugin buffer. Timeout default 30s.",
502
540
  inputSchema: { timeoutSeconds: z.number().min(1).max(120).optional().default(30) },
503
541
  annotations: { readOnlyHint: true },
504
- }, async ({ timeoutSeconds }) => {
542
+ }, safeToolHandler(async ({ timeoutSeconds }) => {
505
543
  const conn = getConnector(bridge);
506
544
  const deadline = Date.now() + timeoutSeconds * 1000;
507
- const seen = new Set();
545
+ let lastSeenTime = 0;
508
546
  const stream = [];
547
+ let pollIntervalMs = 1000;
548
+ let consecutiveEmptyPolls = 0;
509
549
  while (Date.now() < deadline) {
510
550
  const { logs } = await conn.getConsoleLogs(200);
551
+ let newCount = 0;
511
552
  for (const entry of logs) {
512
- const key = `${entry.time}-${JSON.stringify(entry.args)}`;
513
- if (!seen.has(key)) {
514
- seen.add(key);
553
+ if (entry.time > lastSeenTime) {
515
554
  stream.push(entry);
555
+ newCount++;
556
+ if (entry.time > lastSeenTime)
557
+ lastSeenTime = entry.time;
516
558
  }
517
559
  }
518
- await new Promise((r) => setTimeout(r, 1000));
560
+ if (newCount > 0) {
561
+ consecutiveEmptyPolls = 0;
562
+ pollIntervalMs = 1000;
563
+ }
564
+ else {
565
+ consecutiveEmptyPolls++;
566
+ if (consecutiveEmptyPolls >= 10)
567
+ break;
568
+ if (consecutiveEmptyPolls >= 3) {
569
+ pollIntervalMs = Math.min(pollIntervalMs * 2, 5000);
570
+ }
571
+ }
572
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
519
573
  }
520
574
  return {
521
- content: [{ type: "text", text: JSON.stringify({ success: true, stream, count: stream.length }, null, 0) }],
575
+ content: [{ type: "text", text: JSON.stringify({ success: true, stream, count: stream.length }) }],
522
576
  };
523
- });
577
+ }));
524
578
  server.registerTool("figma_clear_console", {
525
579
  description: "Clear the plugin console log buffer.",
526
580
  inputSchema: {},
527
581
  annotations: { destructiveHint: true },
528
- }, async () => {
582
+ }, safeToolHandler(async () => {
529
583
  const conn = getConnector(bridge);
530
584
  await conn.clearConsole();
531
- return { content: [{ type: "text", text: JSON.stringify({ success: true, message: "Console cleared" }, null, 0) }] };
532
- });
585
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, message: "Console cleared" }) }] };
586
+ }));
533
587
  // ---- set_description, get_component_image, get_component_for_development ----
534
588
  server.registerTool("figma_set_description", {
535
589
  description: "Set description on a component, component set, or style node. Supports markdown (descriptionMarkdown).",
@@ -539,11 +593,12 @@ export async function main() {
539
593
  descriptionMarkdown: z.string().optional(),
540
594
  },
541
595
  annotations: { destructiveHint: true },
542
- }, async (p) => {
596
+ }, safeToolHandler(async (p) => {
597
+ invalidateCache();
543
598
  const conn = getConnector(bridge);
544
599
  const result = await conn.setNodeDescription(p.nodeId, p.description, p.descriptionMarkdown);
545
- return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
546
- });
600
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
601
+ }));
547
602
  server.registerTool("figma_get_component_image", {
548
603
  description: "Get screenshot of a node (component/frame). Returns base64 image. No REST API.",
549
604
  inputSchema: {
@@ -552,11 +607,11 @@ export async function main() {
552
607
  format: z.enum(["PNG", "JPG"]).optional().default("PNG"),
553
608
  },
554
609
  annotations: { readOnlyHint: true },
555
- }, async ({ nodeId, scale, format }) => {
610
+ }, safeToolHandler(async ({ nodeId, scale, format }) => {
556
611
  const conn = getConnector(bridge);
557
612
  const result = await conn.captureScreenshot(nodeId, { scale, format });
558
- return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
559
- });
613
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
614
+ }));
560
615
  server.registerTool("figma_get_component_for_development", {
561
616
  description: "Get component metadata plus base64 screenshot in one call. For design-to-code workflows.",
562
617
  inputSchema: {
@@ -565,7 +620,7 @@ export async function main() {
565
620
  format: z.enum(["PNG", "JPG"]).optional().default("PNG"),
566
621
  },
567
622
  annotations: { readOnlyHint: true },
568
- }, async ({ nodeId, scale, format }) => {
623
+ }, safeToolHandler(async ({ nodeId, scale, format }) => {
569
624
  const conn = getConnector(bridge);
570
625
  const [component, screenshot] = await Promise.all([
571
626
  conn.getComponentFromPluginUI(nodeId),
@@ -573,8 +628,8 @@ export async function main() {
573
628
  ]);
574
629
  const comp = component?.component ?? component;
575
630
  const out = { success: true, component: comp, image: screenshot?.image ?? screenshot?.data };
576
- return { content: [{ type: "text", text: JSON.stringify(out, null, 0) }] };
577
- });
631
+ return { content: [{ type: "text", text: JSON.stringify(out) }] };
632
+ }));
578
633
  // ---- Batch variables & setup_design_tokens & arrange_component_set ----
579
634
  server.registerTool("figma_batch_create_variables", {
580
635
  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 +644,12 @@ export async function main() {
589
644
  })).max(100),
590
645
  },
591
646
  annotations: { destructiveHint: true },
592
- }, async ({ items }) => {
647
+ }, safeToolHandler(async ({ items }) => {
648
+ invalidateCache();
593
649
  const conn = getConnector(bridge);
594
650
  const result = await conn.batchCreateVariables(items);
595
- return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 0) }] };
596
- });
651
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
652
+ }));
597
653
  server.registerTool("figma_batch_update_variables", {
598
654
  description: "Update up to 100 variables. Each item: variableId, modeId, value. Returns updated and failed lists.",
599
655
  inputSchema: {
@@ -604,11 +660,12 @@ export async function main() {
604
660
  })).max(100),
605
661
  },
606
662
  annotations: { destructiveHint: true },
607
- }, async ({ items }) => {
663
+ }, safeToolHandler(async ({ items }) => {
664
+ invalidateCache();
608
665
  const conn = getConnector(bridge);
609
666
  const result = await conn.batchUpdateVariables(items);
610
- return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 0) }] };
611
- });
667
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
668
+ }));
612
669
  server.registerTool("figma_setup_design_tokens", {
613
670
  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
671
  inputSchema: {
@@ -622,20 +679,22 @@ export async function main() {
622
679
  })),
623
680
  },
624
681
  annotations: { destructiveHint: true },
625
- }, async (p) => {
682
+ }, safeToolHandler(async (p) => {
683
+ invalidateCache();
626
684
  const conn = getConnector(bridge);
627
685
  const result = await conn.setupDesignTokens(p);
628
- return { content: [{ type: "text", text: JSON.stringify(result, null, 0) }] };
629
- });
686
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
687
+ }));
630
688
  server.registerTool("figma_arrange_component_set", {
631
689
  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
690
  inputSchema: { nodeIds: z.array(z.string()).min(2) },
633
691
  annotations: { destructiveHint: true },
634
- }, async ({ nodeIds }) => {
692
+ }, safeToolHandler(async ({ nodeIds }) => {
693
+ invalidateCache();
635
694
  const conn = getConnector(bridge);
636
695
  const result = await conn.arrangeComponentSet(nodeIds);
637
- return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 0) }] };
638
- });
696
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
697
+ }));
639
698
  // ---- figma_check_design_parity (design–code gap analysis) ----
640
699
  server.registerTool("figma_check_design_parity", {
641
700
  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 +773,7 @@ export async function main() {
714
773
  content: [
715
774
  {
716
775
  type: "text",
717
- text: JSON.stringify({ success: false, error: "codeTokens must be valid JSON" }, null, 0),
776
+ text: JSON.stringify({ success: false, error: "codeTokens must be valid JSON" }),
718
777
  },
719
778
  ],
720
779
  isError: true,
@@ -729,16 +788,18 @@ export async function main() {
729
788
  if (codeVal === undefined) {
730
789
  inFigmaOnly.push({ name, value: figVal });
731
790
  }
732
- else if (normalizeForCompare(figVal) === normalizeForCompare(codeVal)) {
733
- matching.push({ name, value: figVal });
734
- }
735
791
  else {
736
- divergent.push({ name, figmaValue: figVal, codeValue: codeVal });
792
+ codeMap.delete(name);
793
+ if (normalizeForCompare(figVal) === normalizeForCompare(codeVal)) {
794
+ matching.push({ name, value: figVal });
795
+ }
796
+ else {
797
+ divergent.push({ name, figmaValue: figVal, codeValue: codeVal });
798
+ }
737
799
  }
738
800
  }
739
801
  for (const [name, codeVal] of codeMap) {
740
- if (!figmaMap.has(name))
741
- inCodeOnly.push({ name, value: codeVal });
802
+ inCodeOnly.push({ name, value: codeVal });
742
803
  }
743
804
  const out = {
744
805
  success: true,
@@ -753,12 +814,12 @@ export async function main() {
753
814
  inFigmaOnly,
754
815
  inCodeOnly,
755
816
  };
756
- return { content: [{ type: "text", text: JSON.stringify(out, null, 0) }] };
817
+ return { content: [{ type: "text", text: JSON.stringify(out) }] };
757
818
  }
758
819
  catch (err) {
759
820
  const msg = err instanceof Error ? err.message : String(err);
760
821
  return {
761
- content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }, null, 0) }],
822
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }) }],
762
823
  isError: true,
763
824
  };
764
825
  }
@@ -822,12 +883,12 @@ export async function main() {
822
883
  : { id: s.id, name: s.name, fontSize: s.fontSize ?? s.style?.fontSize, fontName: s.fontName ?? s.style?.fontName }),
823
884
  },
824
885
  };
825
- return { content: [{ type: "text", text: JSON.stringify(out, null, 0) }] };
886
+ return { content: [{ type: "text", text: JSON.stringify(out) }] };
826
887
  }
827
888
  catch (err) {
828
889
  const msg = err instanceof Error ? err.message : String(err);
829
890
  return {
830
- content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }, null, 0) }],
891
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: msg }) }],
831
892
  isError: true,
832
893
  };
833
894
  }
@@ -871,7 +932,7 @@ export async function main() {
871
932
  message: msg,
872
933
  ...(startError && { startError }),
873
934
  ...(portHint && { portHint }),
874
- }, null, 0),
935
+ }),
875
936
  }],
876
937
  };
877
938
  });
@@ -913,10 +974,10 @@ export async function main() {
913
974
  return { id: frame.id, name: frame.name, width: frame.width, height: frame.height, x: frame.x, y: frame.y };
914
975
  `;
915
976
  const result = await conn.executeCodeViaUI(code, 10000);
916
- return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 0) }] };
977
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
917
978
  }
918
979
  catch (err) {
919
- return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }, null, 0) }], isError: true };
980
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }) }], isError: true };
920
981
  }
921
982
  });
922
983
  server.registerTool("figma_create_text", {
@@ -947,10 +1008,10 @@ export async function main() {
947
1008
  return { id: node.id, name: node.name, characters: node.characters };
948
1009
  `;
949
1010
  const result = await conn.executeCodeViaUI(code, 10000);
950
- return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 0) }] };
1011
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
951
1012
  }
952
1013
  catch (err) {
953
- return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }, null, 0) }], isError: true };
1014
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }) }], isError: true };
954
1015
  }
955
1016
  });
956
1017
  server.registerTool("figma_create_rectangle", {
@@ -979,10 +1040,10 @@ export async function main() {
979
1040
  return { id: rect.id, name: rect.name };
980
1041
  `;
981
1042
  const result = await conn.executeCodeViaUI(code, 10000);
982
- return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 0) }] };
1043
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
983
1044
  }
984
1045
  catch (err) {
985
- 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 };
986
1047
  }
987
1048
  });
988
1049
  server.registerTool("figma_create_group", {
@@ -1006,10 +1067,10 @@ export async function main() {
1006
1067
  return { id: group.id, name: group.name, childCount: group.children.length };
1007
1068
  `;
1008
1069
  const result = await conn.executeCodeViaUI(code, 10000);
1009
- return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 0) }] };
1070
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }) }] };
1010
1071
  }
1011
1072
  catch (err) {
1012
- return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }, null, 0) }], isError: true };
1073
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }) }], isError: true };
1013
1074
  }
1014
1075
  });
1015
1076
  // ---- figma_export_nodes (batch SVG/PNG/JPG/PDF export) ----
@@ -1062,13 +1123,13 @@ export async function main() {
1062
1123
  ...(r.base64 && { base64: r.base64 }),
1063
1124
  ...(r.error && { error: r.error }),
1064
1125
  })),
1065
- }, null, 0),
1126
+ }),
1066
1127
  });
1067
1128
  return { content: contentBlocks };
1068
1129
  }
1069
1130
  catch (err) {
1070
1131
  return {
1071
- content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }, null, 0) }],
1132
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }) }],
1072
1133
  isError: true,
1073
1134
  };
1074
1135
  }
@@ -1087,7 +1148,7 @@ export async function main() {
1087
1148
  const code = `
1088
1149
  if (!figma.teamLibrary) return { success: false, error: "teamLibrary API not available" };
1089
1150
  const availableLibs = await figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync();
1090
- const availableComps = await figma.teamLibrary.getAvailableLibraryComponentsAsync ? [] : [];
1151
+ const availableComps = typeof figma.teamLibrary.getAvailableLibraryComponentsAsync === 'function' ? await figma.teamLibrary.getAvailableLibraryComponentsAsync() : [];
1091
1152
  return {
1092
1153
  variableCollections: availableLibs.map(c => ({ name: c.name, key: c.key, libraryName: c.libraryName })),
1093
1154
  note: "Use figma_search_components for file-local components. Team library component search requires REST API (figma_rest_api)."
@@ -1099,10 +1160,10 @@ export async function main() {
1099
1160
  const q = query.toLowerCase();
1100
1161
  data.variableCollections = data.variableCollections.filter((c) => (c.name || "").toLowerCase().includes(q) || (c.libraryName || "").toLowerCase().includes(q));
1101
1162
  }
1102
- return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }, null, 0) }] };
1163
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, ...data }) }] };
1103
1164
  }
1104
1165
  catch (err) {
1105
- return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }, null, 0) }], isError: true };
1166
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: err instanceof Error ? err.message : String(err) }) }], isError: true };
1106
1167
  }
1107
1168
  });
1108
1169
  // ---- figma_plugin_diagnostics ----
@@ -1132,7 +1193,7 @@ export async function main() {
1132
1193
  hasRestToken: !!tokenInfo,
1133
1194
  rateLimit: tokenInfo?.rateLimit || null,
1134
1195
  nodeVersion: process.version,
1135
- }, null, 0),
1196
+ }),
1136
1197
  }],
1137
1198
  };
1138
1199
  });
@@ -1154,7 +1215,7 @@ export async function main() {
1154
1215
  text: JSON.stringify({
1155
1216
  success: false,
1156
1217
  error: "Port değişikliği zaten devam ediyor. Lütfen tamamlanmasını bekleyin.",
1157
- }, null, 0),
1218
+ }),
1158
1219
  }],
1159
1220
  isError: true,
1160
1221
  };
@@ -1173,7 +1234,7 @@ export async function main() {
1173
1234
  previousPort: oldPort,
1174
1235
  newPort: result.port,
1175
1236
  message: `Bridge restarted on port ${result.port}. Figma plugin'de Port: ${result.port} ayarlayın ve bağlanmasını bekleyin.`,
1176
- }, null, 0),
1237
+ }),
1177
1238
  }],
1178
1239
  };
1179
1240
  }
@@ -1187,7 +1248,7 @@ export async function main() {
1187
1248
  attemptedPort: newPort,
1188
1249
  error: result.error || "Port bind failed",
1189
1250
  message: `Port ${newPort} bağlanamadı. Başka bir port deneyin (5454–5470).`,
1190
- }, null, 0),
1251
+ }),
1191
1252
  }],
1192
1253
  isError: true,
1193
1254
  };
@@ -1208,7 +1269,7 @@ export async function main() {
1208
1269
  }, async ({ token }) => {
1209
1270
  if (!token.startsWith("figd_")) {
1210
1271
  return {
1211
- content: [{ type: "text", text: JSON.stringify({ success: false, error: "Token must start with 'figd_'" }, null, 0) }],
1272
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "Token must start with 'figd_'" }) }],
1212
1273
  isError: true,
1213
1274
  };
1214
1275
  }
@@ -1223,7 +1284,7 @@ export async function main() {
1223
1284
  clearTimeout(timeout);
1224
1285
  if (!res.ok) {
1225
1286
  return {
1226
- content: [{ type: "text", text: JSON.stringify({ success: false, error: `Token validation failed: ${res.status} ${res.statusText}` }, null, 0) }],
1287
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: `Token validation failed: ${res.status} ${res.statusText}` }) }],
1227
1288
  isError: true,
1228
1289
  };
1229
1290
  }
@@ -1241,12 +1302,12 @@ export async function main() {
1241
1302
  user: me.handle || me.email || "unknown",
1242
1303
  message: "Token set. REST API tools are now available.",
1243
1304
  rateLimit: limit > 0 ? { remaining, limit, resetAt } : undefined,
1244
- }, null, 0) }],
1305
+ }) }],
1245
1306
  };
1246
1307
  }
1247
1308
  catch (err) {
1248
1309
  return {
1249
- content: [{ type: "text", text: JSON.stringify({ success: false, error: `Token validation error: ${err instanceof Error ? err.message : String(err)}` }, null, 0) }],
1310
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: `Token validation error: ${err instanceof Error ? err.message : String(err)}` }) }],
1250
1311
  isError: true,
1251
1312
  };
1252
1313
  }
@@ -1256,7 +1317,7 @@ export async function main() {
1256
1317
  inputSchema: {},
1257
1318
  }, async () => {
1258
1319
  bridge.clearFigmaRestToken();
1259
- return { content: [{ type: "text", text: JSON.stringify({ success: true, message: "Token cleared." }, null, 0) }] };
1320
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, message: "Token cleared." }) }] };
1260
1321
  });
1261
1322
  server.registerTool("figma_rest_api", {
1262
1323
  description: "Call Figma REST API directly. Requires a token set via figma_set_rest_token. " +
@@ -1276,7 +1337,7 @@ export async function main() {
1276
1337
  content: [{ type: "text", text: JSON.stringify({
1277
1338
  success: false,
1278
1339
  error: "No Figma REST API token set. Use figma_set_rest_token first. Or enter token in Figma plugin Advanced panel.",
1279
- }, null, 0) }],
1340
+ }) }],
1280
1341
  isError: true,
1281
1342
  };
1282
1343
  }
@@ -1289,7 +1350,7 @@ export async function main() {
1289
1350
  success: false,
1290
1351
  error: `API rate limit exhausted (0/${rl.limit}). Resets at ${resetDate.toISOString()}. Wait and retry.`,
1291
1352
  rateLimit: rl,
1292
- }, null, 0) }],
1353
+ }) }],
1293
1354
  isError: true,
1294
1355
  };
1295
1356
  }
@@ -1332,7 +1393,7 @@ export async function main() {
1332
1393
  success: false, status: 429,
1333
1394
  error: `Rate limited. ${attempt + 1} attempts, total ${Math.round(elapsed / 1000)}s. Retry later.`,
1334
1395
  rateLimit: limit > 0 ? { remaining, limit, resetAt } : undefined,
1335
- }, null, 0) }],
1396
+ }) }],
1336
1397
  isError: true,
1337
1398
  };
1338
1399
  }
@@ -1354,7 +1415,7 @@ export async function main() {
1354
1415
  success: false, status: res.status, statusText: res.statusText,
1355
1416
  error: responseData,
1356
1417
  rateLimit: limit > 0 ? { remaining, limit, resetAt } : undefined,
1357
- }, null, 0) }],
1418
+ }) }],
1358
1419
  isError: true,
1359
1420
  };
1360
1421
  }
@@ -1381,7 +1442,7 @@ export async function main() {
1381
1442
  data: result.data,
1382
1443
  ...(result.wasTruncated && { _responseGuard: { originalSizeKB: Math.round(result.originalSizeKB), truncatedSizeKB: Math.round(result.truncatedSizeKB) } }),
1383
1444
  rateLimit: limit > 0 ? { remaining, limit, resetAt } : undefined,
1384
- }, null, 0),
1445
+ }),
1385
1446
  });
1386
1447
  return { content: contentBlocks };
1387
1448
  }
@@ -1396,14 +1457,14 @@ export async function main() {
1396
1457
  content: [{ type: "text", text: JSON.stringify({
1397
1458
  success: false,
1398
1459
  error: `REST API call failed after ${attempt + 1} attempts: ${err instanceof Error ? err.message : String(err)}`,
1399
- }, null, 0) }],
1460
+ }) }],
1400
1461
  isError: true,
1401
1462
  };
1402
1463
  }
1403
1464
  }
1404
1465
  // Should not reach here
1405
1466
  return {
1406
- content: [{ type: "text", text: JSON.stringify({ success: false, error: "Unexpected: all retries exhausted" }, null, 0) }],
1467
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "Unexpected: all retries exhausted" }) }],
1407
1468
  isError: true,
1408
1469
  };
1409
1470
  });
@@ -1418,7 +1479,7 @@ export async function main() {
1418
1479
  content: [{ type: "text", text: JSON.stringify({
1419
1480
  hasToken: false,
1420
1481
  message: "No token set. Use figma_set_rest_token to add one.",
1421
- }, null, 0) }],
1482
+ }) }],
1422
1483
  };
1423
1484
  }
1424
1485
  const rl = tokenInfo.rateLimit;
@@ -1439,11 +1500,12 @@ export async function main() {
1439
1500
  rateLimit: rl || null,
1440
1501
  ...(warning && { warning }),
1441
1502
  message: warning || "Token is set. REST API tools are available.",
1442
- }, null, 0) }],
1503
+ }) }],
1443
1504
  };
1444
1505
  });
1445
1506
  const shutdown = () => {
1446
1507
  logger.info("Shutting down plugin-only MCP server...");
1508
+ closeAuditLog();
1447
1509
  try {
1448
1510
  bridge.stop();
1449
1511
  }