@mp3wizard/figma-console-mcp 1.32.2 → 1.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +26 -17
  2. package/dist/cloudflare/core/cloud-websocket-connector.js +18 -0
  3. package/dist/cloudflare/core/design-code-tools.js +60 -17
  4. package/dist/cloudflare/core/design-system-manifest.js +19 -14
  5. package/dist/cloudflare/core/design-system-tools.js +43 -34
  6. package/dist/cloudflare/core/diagnose-tool.js +4 -0
  7. package/dist/cloudflare/core/enrichment/enrichment-service.js +11 -5
  8. package/dist/cloudflare/core/enrichment/style-resolver.js +38 -18
  9. package/dist/cloudflare/core/figma-api.js +118 -54
  10. package/dist/cloudflare/core/figma-tools.js +179 -63
  11. package/dist/cloudflare/core/port-discovery.js +404 -31
  12. package/dist/cloudflare/core/tokens/alias-resolver.js +75 -5
  13. package/dist/cloudflare/core/tokens/config.js +10 -0
  14. package/dist/cloudflare/core/tokens/dialect.js +232 -0
  15. package/dist/cloudflare/core/tokens/figma-converter.js +144 -16
  16. package/dist/cloudflare/core/tokens/formatters/css-vars.js +21 -12
  17. package/dist/cloudflare/core/tokens/formatters/dtcg.js +106 -30
  18. package/dist/cloudflare/core/tokens/formatters/json.js +28 -10
  19. package/dist/cloudflare/core/tokens/formatters/scss.js +19 -13
  20. package/dist/cloudflare/core/tokens/formatters/style-dictionary-v3.js +15 -9
  21. package/dist/cloudflare/core/tokens/formatters/tailwind-v4.js +14 -9
  22. package/dist/cloudflare/core/tokens/formatters/tokens-studio.js +11 -5
  23. package/dist/cloudflare/core/tokens/index.js +2 -1
  24. package/dist/cloudflare/core/tokens/parsers/dtcg.js +32 -5
  25. package/dist/cloudflare/core/tokens/schemas.js +4 -0
  26. package/dist/cloudflare/core/tokens-tools.js +1017 -88
  27. package/dist/cloudflare/core/version-tools.js +44 -3
  28. package/dist/cloudflare/core/websocket-connector.js +42 -0
  29. package/dist/cloudflare/core/websocket-server.js +99 -8
  30. package/dist/cloudflare/core/write-tools.js +355 -86
  31. package/dist/cloudflare/index.js +7 -7
  32. package/dist/core/design-code-tools.d.ts.map +1 -1
  33. package/dist/core/design-code-tools.js +60 -17
  34. package/dist/core/design-code-tools.js.map +1 -1
  35. package/dist/core/design-system-manifest.d.ts +1 -0
  36. package/dist/core/design-system-manifest.d.ts.map +1 -1
  37. package/dist/core/design-system-manifest.js +19 -14
  38. package/dist/core/design-system-manifest.js.map +1 -1
  39. package/dist/core/design-system-tools.d.ts.map +1 -1
  40. package/dist/core/design-system-tools.js +43 -34
  41. package/dist/core/design-system-tools.js.map +1 -1
  42. package/dist/core/diagnose-tool.d.ts +8 -0
  43. package/dist/core/diagnose-tool.d.ts.map +1 -1
  44. package/dist/core/diagnose-tool.js +4 -0
  45. package/dist/core/diagnose-tool.js.map +1 -1
  46. package/dist/core/enrichment/enrichment-service.d.ts.map +1 -1
  47. package/dist/core/enrichment/enrichment-service.js +11 -5
  48. package/dist/core/enrichment/enrichment-service.js.map +1 -1
  49. package/dist/core/enrichment/style-resolver.d.ts +7 -2
  50. package/dist/core/enrichment/style-resolver.d.ts.map +1 -1
  51. package/dist/core/enrichment/style-resolver.js +38 -18
  52. package/dist/core/enrichment/style-resolver.js.map +1 -1
  53. package/dist/core/figma-api.d.ts +18 -9
  54. package/dist/core/figma-api.d.ts.map +1 -1
  55. package/dist/core/figma-api.js +118 -54
  56. package/dist/core/figma-api.js.map +1 -1
  57. package/dist/core/figma-connector.d.ts +12 -0
  58. package/dist/core/figma-connector.d.ts.map +1 -1
  59. package/dist/core/figma-tools.d.ts.map +1 -1
  60. package/dist/core/figma-tools.js +179 -63
  61. package/dist/core/figma-tools.js.map +1 -1
  62. package/dist/core/port-discovery.d.ts +40 -0
  63. package/dist/core/port-discovery.d.ts.map +1 -1
  64. package/dist/core/port-discovery.js +404 -31
  65. package/dist/core/port-discovery.js.map +1 -1
  66. package/dist/core/tokens/alias-resolver.d.ts +45 -3
  67. package/dist/core/tokens/alias-resolver.d.ts.map +1 -1
  68. package/dist/core/tokens/alias-resolver.js +75 -5
  69. package/dist/core/tokens/alias-resolver.js.map +1 -1
  70. package/dist/core/tokens/config.d.ts +28 -0
  71. package/dist/core/tokens/config.d.ts.map +1 -1
  72. package/dist/core/tokens/config.js +10 -0
  73. package/dist/core/tokens/config.js.map +1 -1
  74. package/dist/core/tokens/dialect.d.ts +107 -0
  75. package/dist/core/tokens/dialect.d.ts.map +1 -0
  76. package/dist/core/tokens/dialect.js +233 -0
  77. package/dist/core/tokens/dialect.js.map +1 -0
  78. package/dist/core/tokens/figma-converter.d.ts +23 -2
  79. package/dist/core/tokens/figma-converter.d.ts.map +1 -1
  80. package/dist/core/tokens/figma-converter.js +144 -16
  81. package/dist/core/tokens/figma-converter.js.map +1 -1
  82. package/dist/core/tokens/formatters/css-vars.d.ts.map +1 -1
  83. package/dist/core/tokens/formatters/css-vars.js +21 -12
  84. package/dist/core/tokens/formatters/css-vars.js.map +1 -1
  85. package/dist/core/tokens/formatters/dtcg.d.ts +2 -2
  86. package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -1
  87. package/dist/core/tokens/formatters/dtcg.js +106 -30
  88. package/dist/core/tokens/formatters/dtcg.js.map +1 -1
  89. package/dist/core/tokens/formatters/json.d.ts.map +1 -1
  90. package/dist/core/tokens/formatters/json.js +28 -10
  91. package/dist/core/tokens/formatters/json.js.map +1 -1
  92. package/dist/core/tokens/formatters/scss.d.ts.map +1 -1
  93. package/dist/core/tokens/formatters/scss.js +19 -13
  94. package/dist/core/tokens/formatters/scss.js.map +1 -1
  95. package/dist/core/tokens/formatters/style-dictionary-v3.d.ts.map +1 -1
  96. package/dist/core/tokens/formatters/style-dictionary-v3.js +15 -9
  97. package/dist/core/tokens/formatters/style-dictionary-v3.js.map +1 -1
  98. package/dist/core/tokens/formatters/tailwind-v4.d.ts.map +1 -1
  99. package/dist/core/tokens/formatters/tailwind-v4.js +14 -9
  100. package/dist/core/tokens/formatters/tailwind-v4.js.map +1 -1
  101. package/dist/core/tokens/formatters/tokens-studio.d.ts.map +1 -1
  102. package/dist/core/tokens/formatters/tokens-studio.js +11 -5
  103. package/dist/core/tokens/formatters/tokens-studio.js.map +1 -1
  104. package/dist/core/tokens/index.d.ts +2 -1
  105. package/dist/core/tokens/index.d.ts.map +1 -1
  106. package/dist/core/tokens/index.js +2 -1
  107. package/dist/core/tokens/index.js.map +1 -1
  108. package/dist/core/tokens/parsers/dtcg.js +32 -5
  109. package/dist/core/tokens/parsers/dtcg.js.map +1 -1
  110. package/dist/core/tokens/schemas.d.ts +3 -0
  111. package/dist/core/tokens/schemas.d.ts.map +1 -1
  112. package/dist/core/tokens/schemas.js +4 -0
  113. package/dist/core/tokens/schemas.js.map +1 -1
  114. package/dist/core/tokens/types.d.ts +57 -1
  115. package/dist/core/tokens/types.d.ts.map +1 -1
  116. package/dist/core/tokens/types.js.map +1 -1
  117. package/dist/core/tokens-tools.d.ts +250 -7
  118. package/dist/core/tokens-tools.d.ts.map +1 -1
  119. package/dist/core/tokens-tools.js +1017 -88
  120. package/dist/core/tokens-tools.js.map +1 -1
  121. package/dist/core/version-tools.d.ts.map +1 -1
  122. package/dist/core/version-tools.js +44 -3
  123. package/dist/core/version-tools.js.map +1 -1
  124. package/dist/core/websocket-connector.d.ts +38 -0
  125. package/dist/core/websocket-connector.d.ts.map +1 -1
  126. package/dist/core/websocket-connector.js +42 -0
  127. package/dist/core/websocket-connector.js.map +1 -1
  128. package/dist/core/websocket-server.d.ts +23 -0
  129. package/dist/core/websocket-server.d.ts.map +1 -1
  130. package/dist/core/websocket-server.js +99 -8
  131. package/dist/core/websocket-server.js.map +1 -1
  132. package/dist/core/write-tools.d.ts.map +1 -1
  133. package/dist/core/write-tools.js +355 -86
  134. package/dist/core/write-tools.js.map +1 -1
  135. package/dist/local.d.ts +0 -1
  136. package/dist/local.d.ts.map +1 -1
  137. package/dist/local.js +253 -63
  138. package/dist/local.js.map +1 -1
  139. package/figma-desktop-bridge/code.js +382 -28
  140. package/figma-desktop-bridge/ui.html +578 -292
  141. package/package.json +2 -2
package/dist/local.js CHANGED
@@ -36,7 +36,7 @@ import { registerWriteTools } from "./core/write-tools.js";
36
36
  import { registerTokensTools } from "./core/tokens-tools.js";
37
37
  import { wrapServerForIdentity } from "./core/identity.js";
38
38
  import { PACKAGE_ROOT } from "./core/resolve-package-root.js";
39
- import { FigmaWebSocketServer } from "./core/websocket-server.js";
39
+ import { FigmaWebSocketServer, getBundledPluginVersion } from "./core/websocket-server.js";
40
40
  import { WebSocketConnector } from "./core/websocket-connector.js";
41
41
  import { DEFAULT_WS_PORT, getPortRange, advertisePort, unadvertisePort, registerPortCleanup, startPeriodicReaper, discoverActiveInstances, cleanupStalePortFiles, cleanupOrphanedProcesses, evictOldestInstance, refreshPortAdvertisement, HEARTBEAT_INTERVAL_MS, } from "./core/port-discovery.js";
42
42
  import { registerTokenBrowserApp } from "./apps/token-browser/server.js";
@@ -97,7 +97,6 @@ class LocalFigmaConsoleMCP {
97
97
  }
98
98
  constructor() {
99
99
  this.figmaAPI = null;
100
- this.desktopConnector = null;
101
100
  this.wsServer = null;
102
101
  this.wsStartupError = null;
103
102
  /** The port the WebSocket server actually bound to (may differ from preferred if fallback occurred) */
@@ -211,9 +210,8 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
211
210
  try {
212
211
  const wsConnector = new WebSocketConnector(this.wsServer);
213
212
  await wsConnector.initialize();
214
- this.desktopConnector = wsConnector;
215
213
  logger.debug("Desktop connector initialized via WebSocket bridge");
216
- return this.desktopConnector;
214
+ return wsConnector;
217
215
  }
218
216
  catch (wsError) {
219
217
  const errorMsg = wsError instanceof Error ? wsError.message : String(wsError);
@@ -448,9 +446,11 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
448
446
  };
449
447
  }
450
448
  });
451
- // Tool 2: Take Screenshot (using Figma REST API)
449
+ // Tool 2: Take Screenshot (Desktop Bridge first, REST API fallback)
450
+ // Bridge-first: the plugin's exportAsync works on any Figma plan with no REST
451
+ // token, and reflects the current runtime state (no cloud-sync lag).
452
452
  // Note: For screenshots of specific components, use figma_get_component_image instead
453
- this.server.tool("figma_take_screenshot", `Export an image of the current Figma page or specific node via REST API. Returns an image URL (valid 30 days). Use for visual validation after design changes — check alignment, spacing, proportions. Pass nodeId to target specific elements. For components, prefer figma_get_component_image.`, {
453
+ this.server.tool("figma_take_screenshot", `Export an image of the current Figma page or specific node. Uses the Desktop Bridge plugin (exportAsync) when connected — works on any plan, no REST token needed, reflects current runtime state. Falls back to the Figma REST API when the bridge is unavailable or for PDF format. Use for visual validation after design changes — check alignment, spacing, proportions. Pass nodeId to target specific elements. For components, prefer figma_get_component_image.`, {
454
454
  nodeId: z
455
455
  .string()
456
456
  .optional()
@@ -468,6 +468,73 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
468
468
  .default("png")
469
469
  .describe("Image format (default: png)"),
470
470
  }, async ({ nodeId, scale, format }) => {
471
+ // Callers routinely paste URL-format ids (123-456); both the Plugin
472
+ // API and REST response maps use colon format (123:456).
473
+ nodeId = nodeId?.replace(/-/g, ":");
474
+ // Bridge-first: exportAsync via the Desktop Bridge works on any plan
475
+ // (no REST token) and reflects the current runtime state. PDF is the
476
+ // only format exportAsync can't produce, so it goes straight to REST.
477
+ if (format !== "pdf" && this.wsServer?.isClientConnected()) {
478
+ try {
479
+ const connector = await this.getDesktopConnector();
480
+ // Resolve the target node the same way the REST path does;
481
+ // empty string means the plugin captures the current page.
482
+ let bridgeNodeId = nodeId || "";
483
+ if (!bridgeNodeId) {
484
+ const currentUrl = this.getCurrentFileUrl();
485
+ const nodeIdParam = currentUrl
486
+ ? new URL(currentUrl).searchParams.get("node-id")
487
+ : null;
488
+ if (nodeIdParam) {
489
+ bridgeNodeId = nodeIdParam.replace(/-/g, ":");
490
+ }
491
+ }
492
+ const bridgeFormat = format === "jpg" ? "JPG" : format === "svg" ? "SVG" : "PNG";
493
+ // exportAsync scale floor is 0.5 (REST allows 0.01)
494
+ const bridgeScale = Math.min(Math.max(scale, 0.5), 4);
495
+ logger.info({ nodeId: bridgeNodeId, format: bridgeFormat, scale: bridgeScale }, "Capturing screenshot via Desktop Bridge (bridge-first)");
496
+ let result = await connector.captureScreenshot(bridgeNodeId, {
497
+ format: bridgeFormat,
498
+ scale: bridgeScale,
499
+ });
500
+ if (result &&
501
+ typeof result.success === "undefined" &&
502
+ result.image) {
503
+ result = { success: true, image: result };
504
+ }
505
+ if (!result?.success || !result.image?.base64) {
506
+ throw new Error(result?.error || "Bridge screenshot returned no image");
507
+ }
508
+ const bridgeMimeType = bridgeFormat === "JPG"
509
+ ? "image/jpeg"
510
+ : bridgeFormat === "SVG"
511
+ ? "image/svg+xml"
512
+ : "image/png";
513
+ return {
514
+ content: [
515
+ {
516
+ type: "text",
517
+ text: JSON.stringify({
518
+ nodeId: result.image.node?.id || bridgeNodeId || null,
519
+ scale: result.image.scale,
520
+ format,
521
+ byteLength: result.image.byteLength,
522
+ source: "desktop_bridge",
523
+ note: "Screenshot captured via the Desktop Bridge plugin (current runtime state, no REST token required). The image is included below for visual analysis.",
524
+ }),
525
+ },
526
+ {
527
+ type: "image",
528
+ data: result.image.base64,
529
+ mimeType: bridgeMimeType,
530
+ },
531
+ ],
532
+ };
533
+ }
534
+ catch (bridgeError) {
535
+ logger.warn({ error: bridgeError }, "Desktop Bridge screenshot failed, falling back to REST API");
536
+ }
537
+ }
471
538
  try {
472
539
  const api = await this.getFigmaAPI();
473
540
  // Get current URL to extract file key and node ID if not provided
@@ -544,6 +611,12 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
544
611
  catch (error) {
545
612
  logger.error({ error }, "Failed to capture screenshot");
546
613
  const errorMessage = error instanceof Error ? error.message : String(error);
614
+ // FigmaAPI.request() tags real token failures with isAuthError —
615
+ // a bare "403" substring also matches node IDs like "403:12"
616
+ // and permission-denied responses, which are not token problems.
617
+ const isAuthError = error?.isAuthError === true ||
618
+ errorMessage.toLowerCase().includes("token expired") ||
619
+ errorMessage.toLowerCase().includes("invalid token");
547
620
  return {
548
621
  content: [
549
622
  {
@@ -551,7 +624,9 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
551
624
  text: JSON.stringify({
552
625
  error: errorMessage,
553
626
  message: "Failed to capture screenshot via Figma API",
554
- hint: "Make sure you've called figma_navigate to open a file, or provide a valid nodeId parameter",
627
+ hint: isAuthError
628
+ ? "Your FIGMA_ACCESS_TOKEN is expired or invalid. Generate a new personal access token at figma.com → Settings → Security → Personal access tokens, then update FIGMA_ACCESS_TOKEN in your MCP config. Alternatively, open the Desktop Bridge plugin in Figma Desktop — screenshots work through the bridge without any REST token."
629
+ : "Make sure you've called figma_navigate to open a file, or provide a valid nodeId parameter. Tip: with the Desktop Bridge plugin open, screenshots don't need a REST token at all.",
555
630
  }),
556
631
  },
557
632
  ],
@@ -952,6 +1027,11 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
952
1027
  websocket: {
953
1028
  available: wsConnected,
954
1029
  serverRunning: this.wsServer?.isStarted() ?? false,
1030
+ // Version of the plugin files this server ships — what a
1031
+ // manifest re-import installs. pluginUpdateAvailable on
1032
+ // connected files compares against THIS, not the server
1033
+ // version (which can be newer on server-only releases).
1034
+ bundledPluginVersion: getBundledPluginVersion(),
955
1035
  port: this.wsActualPort ? String(this.wsActualPort) : null,
956
1036
  preferredPort: String(this.wsPreferredPort),
957
1037
  portFallbackUsed: this.wsActualPort !== null && this.wsActualPort !== this.wsPreferredPort,
@@ -981,6 +1061,8 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
981
1061
  fileKey: wsFileInfo.fileKey,
982
1062
  currentPage: wsFileInfo.currentPage,
983
1063
  connectedAt: new Date(wsFileInfo.connectedAt).toISOString(),
1064
+ pluginVersion: wsFileInfo.pluginVersion ?? undefined,
1065
+ pluginUpdateAvailable: wsFileInfo.pluginUpdateAvailable || undefined,
984
1066
  } : undefined,
985
1067
  connectedFiles: (() => {
986
1068
  const files = this.wsServer?.getConnectedFiles();
@@ -993,6 +1075,8 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
993
1075
  editorType: f.editorType || 'figma',
994
1076
  isActive: f.isActive,
995
1077
  connectedAt: new Date(f.connectedAt).toISOString(),
1078
+ pluginVersion: f.pluginVersion ?? undefined,
1079
+ pluginUpdateAvailable: f.pluginUpdateAvailable || undefined,
996
1080
  }));
997
1081
  })(),
998
1082
  currentSelection: (() => {
@@ -1073,46 +1157,60 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
1073
1157
  // Tool: Force reconnect to Figma Desktop
1074
1158
  this.server.tool("figma_reconnect", "Force a complete reconnection to Figma Desktop. Use when connection seems stale or after switching files.", {}, async () => {
1075
1159
  try {
1076
- // Clear cached desktop connector to force fresh detection
1077
- this.desktopConnector = null;
1078
- let transport = "none";
1079
- let currentUrl = null;
1080
- let fileName = null;
1081
1160
  // figma_reconnect is informational in WebSocket-only mode — the
1082
- // plugin handles its own reconnect logic. We just report whether
1083
- // the bridge is currently connected.
1084
- if (this.wsServer?.isClientConnected()) {
1085
- transport = "websocket";
1086
- }
1087
- if (transport === "none") {
1161
+ // plugin handles its own reconnect logic. A TCP-open socket is not
1162
+ // proof of health (the plugin sandbox can be dead while ui.html
1163
+ // still pongs), so a live roundtrip through the sandbox is the
1164
+ // success criterion here.
1165
+ if (!this.wsServer?.isClientConnected()) {
1088
1166
  throw new Error("Cannot connect to Figma Desktop.\n\n" +
1089
1167
  "Open the Desktop Bridge plugin in Figma (Plugins → Development → Figma Desktop Bridge).");
1090
1168
  }
1091
- // Try to get the file name via whichever transport connected
1092
- try {
1093
- const connector = await this.getDesktopConnector();
1094
- const fileInfo = await connector.executeCodeViaUI("return { fileName: figma.root.name, fileKey: figma.fileKey }", 5000);
1095
- if (fileInfo.success && fileInfo.result) {
1096
- fileName = fileInfo.result.fileName;
1169
+ const connector = await this.getDesktopConnector();
1170
+ const probe = async () => connector.executeCodeViaUI("return { fileName: figma.root.name, fileKey: figma.fileKey }", 5000);
1171
+ let fileInfo = await probe().catch(() => ({ success: false, result: null }));
1172
+ let selfHealed = false;
1173
+ if (!fileInfo.success) {
1174
+ // Sandbox-dead / wedged-relay state: attempt self-healing by
1175
+ // reloading the plugin iframe (RELOAD_UI re-runs figma.showUI,
1176
+ // which triggers a fresh port scan and reconnection). This only
1177
+ // works when the message relay is still partially functional —
1178
+ // if code.js itself is dead, the command times out and we fall
1179
+ // through to the honest error below.
1180
+ try {
1181
+ logger.info("Reconnect probe failed — attempting RELOAD_UI self-heal");
1182
+ await this.wsServer.sendCommand("RELOAD_UI", {}, 5000);
1183
+ // Give the fresh iframe time to rescan and re-identify
1184
+ await new Promise((resolve) => setTimeout(resolve, 4000));
1185
+ fileInfo = await probe().catch(() => ({ success: false, result: null }));
1186
+ selfHealed = fileInfo.success;
1187
+ }
1188
+ catch {
1189
+ // RELOAD_UI itself failed — the sandbox is truly unreachable
1097
1190
  }
1098
1191
  }
1099
- catch {
1100
- // Non-critical - just for context
1192
+ if (!fileInfo.success) {
1193
+ const probeErr = new Error("Desktop Bridge socket is open but the plugin is not responding to commands " +
1194
+ "(automatic plugin reload was attempted and did not help). " +
1195
+ "Close and reopen the Figma Console MCP plugin in Figma Desktop (Plugins → Development → Figma Console MCP).");
1196
+ probeErr.connectionError = this.buildConnectionError(probeErr);
1197
+ throw probeErr;
1101
1198
  }
1199
+ const fileName = fileInfo.result?.fileName || null;
1102
1200
  return {
1103
1201
  content: [
1104
1202
  {
1105
1203
  type: "text",
1106
1204
  text: JSON.stringify({
1107
- status: "reconnected",
1108
- transport,
1109
- currentUrl,
1110
- fileName: fileName ||
1111
- "(unknown - Desktop Bridge may need to be restarted)",
1205
+ status: "connected",
1206
+ transport: "websocket",
1207
+ probeVerified: true,
1208
+ selfHealed: selfHealed || undefined,
1209
+ fileName,
1112
1210
  timestamp: Date.now(),
1113
- message: fileName
1114
- ? `Successfully reconnected via ${transport.toUpperCase()}. Now connected to: "${fileName}"`
1115
- : `Successfully reconnected to Figma Desktop via ${transport.toUpperCase()}.`,
1211
+ message: selfHealed
1212
+ ? `Plugin was unresponsive; automatically reloaded it and verified the connection. Connected to: "${fileName || "(unnamed file)"}"`
1213
+ : `Connection verified via live roundtrip. Connected to: "${fileName || "(unnamed file)"}"`,
1116
1214
  }),
1117
1215
  },
1118
1216
  ],
@@ -1375,8 +1473,11 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
1375
1473
  const { DesignSystemManifestCache, createEmptyManifest, figmaColorToHex, } = await import("./core/design-system-manifest.js");
1376
1474
  const cache = DesignSystemManifestCache.getInstance();
1377
1475
  const currentUrl = this.getCurrentFileUrl();
1378
- const fileKeyMatch = currentUrl?.match(/\/(file|design)\/([a-zA-Z0-9]+)/);
1379
- const fileKey = fileKeyMatch ? fileKeyMatch[2] : "unknown";
1476
+ // extractFileKey is branch-aware: on /design/KEY/branch/BRANCH_KEY/…
1477
+ // URLs the branch key is the effective file key — an ad-hoc regex
1478
+ // here returned the main key and cached the wrong manifest for
1479
+ // branch files.
1480
+ const fileKey = (currentUrl ? extractFileKey(currentUrl) : null) ?? "unknown";
1380
1481
  // Check cache first
1381
1482
  let cacheEntry = cache.get(fileKey);
1382
1483
  if (cacheEntry) {
@@ -1431,7 +1532,10 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
1431
1532
  logger.warn({ error }, "Could not fetch variables during auto-load");
1432
1533
  }
1433
1534
  // Get components
1535
+ // Maps are keyed by component key (unique) rather than name — same-named
1536
+ // components on different pages would silently overwrite each other.
1434
1537
  let rawComponents;
1538
+ let componentsFetchError = null;
1435
1539
  try {
1436
1540
  const componentsResult = await connector.getLocalComponents();
1437
1541
  if (componentsResult.success && componentsResult.data) {
@@ -1440,7 +1544,7 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
1440
1544
  componentSets: componentsResult.data.componentSets || [],
1441
1545
  };
1442
1546
  for (const comp of rawComponents.components) {
1443
- manifest.components[comp.name] = {
1547
+ manifest.components[comp.key || comp.nodeId] = {
1444
1548
  key: comp.key,
1445
1549
  nodeId: comp.nodeId,
1446
1550
  name: comp.name,
@@ -1449,7 +1553,7 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
1449
1553
  };
1450
1554
  }
1451
1555
  for (const compSet of rawComponents.componentSets) {
1452
- manifest.componentSets[compSet.name] = {
1556
+ manifest.componentSets[compSet.key || compSet.nodeId] = {
1453
1557
  key: compSet.key,
1454
1558
  nodeId: compSet.nodeId,
1455
1559
  name: compSet.name,
@@ -1466,8 +1570,14 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
1466
1570
  };
1467
1571
  }
1468
1572
  }
1573
+ else {
1574
+ componentsFetchError =
1575
+ componentsResult?.error || "getLocalComponents returned no data";
1576
+ }
1469
1577
  }
1470
1578
  catch (error) {
1579
+ componentsFetchError =
1580
+ error instanceof Error ? error.message : String(error);
1471
1581
  logger.warn({ error }, "Could not fetch components during auto-load");
1472
1582
  }
1473
1583
  // Update summary
@@ -1484,10 +1594,29 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
1484
1594
  typographyScale: [],
1485
1595
  componentCategories: [],
1486
1596
  };
1487
- // Cache the result
1488
- cache.set(fileKey, manifest, rawComponents);
1489
- cacheEntry = cache.get(fileKey);
1490
- return { cacheEntry, fileKey, wasLoaded: true };
1597
+ // Cache the result — but never cache a manifest built from a FAILED
1598
+ // components fetch: serving an empty manifest as "cached" for the full
1599
+ // TTL is exactly the "search returns 0 components" poisoning bug.
1600
+ if (!componentsFetchError) {
1601
+ cache.set(fileKey, manifest, rawComponents);
1602
+ cacheEntry = cache.get(fileKey);
1603
+ }
1604
+ else {
1605
+ cacheEntry = {
1606
+ manifest,
1607
+ timestamp: Date.now(),
1608
+ fileKey,
1609
+ rawComponents,
1610
+ };
1611
+ }
1612
+ return {
1613
+ cacheEntry,
1614
+ fileKey,
1615
+ wasLoaded: true,
1616
+ warning: componentsFetchError
1617
+ ? `Components fetch failed (${componentsFetchError}) — results may be incomplete and were NOT cached; retry after checking the Desktop Bridge plugin.`
1618
+ : undefined,
1619
+ };
1491
1620
  };
1492
1621
  // ============================================================================
1493
1622
  // READ-SIDE LIBRARY / DESIGN-SYSTEM TOOLS
@@ -1505,7 +1634,7 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
1505
1634
  const { DesignSystemManifestCache, createEmptyManifest, figmaColorToHex, getCategories, getTokenSummary, } = await import("./core/design-system-manifest.js");
1506
1635
  const cache = DesignSystemManifestCache.getInstance();
1507
1636
  const currentUrl = this.getCurrentFileUrl();
1508
- const fileKeyMatch = currentUrl?.match(/\/(file|design)\/([a-zA-Z0-9]+)/);
1637
+ const fileKeyMatch = currentUrl?.match(/\/(file|design|board|slides)\/([a-zA-Z0-9]+)/);
1509
1638
  const fileKey = fileKeyMatch ? fileKeyMatch[2] : "unknown";
1510
1639
  // Check cache first
1511
1640
  let cacheEntry = cache.get(fileKey);
@@ -1582,7 +1711,10 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
1582
1711
  logger.warn({ error }, "Could not fetch variables");
1583
1712
  }
1584
1713
  // Get components (can be slow for large files)
1714
+ // Keyed by component key (unique) — name keys drop same-named
1715
+ // components on other pages.
1585
1716
  let rawComponents;
1717
+ let componentsFetchError = null;
1586
1718
  try {
1587
1719
  const componentsResult = await connector.getLocalComponents();
1588
1720
  if (componentsResult.success && componentsResult.data) {
@@ -1591,7 +1723,7 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
1591
1723
  componentSets: componentsResult.data.componentSets || [],
1592
1724
  };
1593
1725
  for (const comp of rawComponents.components) {
1594
- manifest.components[comp.name] = {
1726
+ manifest.components[comp.key || comp.nodeId] = {
1595
1727
  key: comp.key,
1596
1728
  nodeId: comp.nodeId,
1597
1729
  name: comp.name,
@@ -1600,7 +1732,7 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
1600
1732
  };
1601
1733
  }
1602
1734
  for (const compSet of rawComponents.componentSets) {
1603
- manifest.componentSets[compSet.name] = {
1735
+ manifest.componentSets[compSet.key || compSet.nodeId] = {
1604
1736
  key: compSet.key,
1605
1737
  nodeId: compSet.nodeId,
1606
1738
  name: compSet.name,
@@ -1617,8 +1749,14 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
1617
1749
  };
1618
1750
  }
1619
1751
  }
1752
+ else {
1753
+ componentsFetchError =
1754
+ componentsResult?.error || "getLocalComponents returned no data";
1755
+ }
1620
1756
  }
1621
1757
  catch (error) {
1758
+ componentsFetchError =
1759
+ error instanceof Error ? error.message : String(error);
1622
1760
  logger.warn({ error }, "Could not fetch components");
1623
1761
  }
1624
1762
  // Update summary
@@ -1635,8 +1773,11 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
1635
1773
  typographyScale: [],
1636
1774
  componentCategories: [],
1637
1775
  };
1638
- // Cache the result
1639
- cache.set(fileKey, manifest, rawComponents);
1776
+ // Cache the result — never cache a manifest built from a failed
1777
+ // components fetch (would serve empty results for the full TTL).
1778
+ if (!componentsFetchError) {
1779
+ cache.set(fileKey, manifest, rawComponents);
1780
+ }
1640
1781
  const categories = getCategories(manifest);
1641
1782
  const tokenSummary = getTokenSummary(manifest);
1642
1783
  return {
@@ -1654,6 +1795,11 @@ If Design Systems Assistant MCP is not available, install it from: https://githu
1654
1795
  componentSets: manifest.summary.totalComponentSets,
1655
1796
  tokens: manifest.summary.totalTokens,
1656
1797
  },
1798
+ warnings: componentsFetchError
1799
+ ? [
1800
+ `Components fetch failed (${componentsFetchError}) — component data is incomplete and was NOT cached. Check the Desktop Bridge plugin and retry.`,
1801
+ ]
1802
+ : undefined,
1657
1803
  hint: "Use figma_search_components to find specific components by name or category.",
1658
1804
  }),
1659
1805
  },
@@ -1800,7 +1946,18 @@ Without libraryFileKey/libraryFileUrl, searches the currently open file (local c
1800
1946
  const effectiveLimit = Math.min(limit || 10, 25);
1801
1947
  const effectiveOffset = offset || 0;
1802
1948
  const total = results.length;
1803
- const paginatedResults = results.slice(effectiveOffset, effectiveOffset + effectiveLimit);
1949
+ const paginatedResults = results
1950
+ .slice(effectiveOffset, effectiveOffset + effectiveLimit)
1951
+ // Search hits only need a teaser — some design systems carry
1952
+ // multi-KB doc blocks per component, which multiplies across
1953
+ // a result page. figma_get_component_details returns full text.
1954
+ .map((item) => item.description && item.description.length > 200
1955
+ ? {
1956
+ ...item,
1957
+ description: `${item.description.slice(0, 200)}…`,
1958
+ descriptionTruncated: true,
1959
+ }
1960
+ : item);
1804
1961
  return {
1805
1962
  content: [
1806
1963
  {
@@ -1827,7 +1984,7 @@ Without libraryFileKey/libraryFileUrl, searches the currently open file (local c
1827
1984
  // LOCAL SEARCH PATH: Use cached design system manifest (existing behavior)
1828
1985
  const { searchComponents } = await import("./core/design-system-manifest.js");
1829
1986
  // Auto-load design system cache if needed (no error returned to user)
1830
- const { cacheEntry } = await ensureDesignSystemCache();
1987
+ const { cacheEntry, warning: cacheWarning } = await ensureDesignSystemCache();
1831
1988
  if (!cacheEntry) {
1832
1989
  return {
1833
1990
  content: [
@@ -1858,6 +2015,7 @@ Without libraryFileKey/libraryFileUrl, searches the currently open file (local c
1858
2015
  query: query || "(all)",
1859
2016
  category: category || "(all)",
1860
2017
  results: results.results,
2018
+ warnings: cacheWarning ? [cacheWarning] : undefined,
1861
2019
  pagination: {
1862
2020
  offset: offset || 0,
1863
2021
  limit: effectiveLimit,
@@ -1930,10 +2088,12 @@ Without libraryFileKey/libraryFileUrl, searches the currently open file (local c
1930
2088
  // Search for the component
1931
2089
  let component = null;
1932
2090
  let isComponentSet = false;
1933
- // Check component sets first (they have variants)
1934
- for (const [name, compSet] of Object.entries(cacheEntry.manifest.componentSets)) {
2091
+ // Check component sets first (they have variants).
2092
+ // Maps are keyed by component key; match names via the entry's
2093
+ // own name field, not the map key.
2094
+ for (const compSet of Object.values(cacheEntry.manifest.componentSets)) {
1935
2095
  if ((componentKey && compSet.key === componentKey) ||
1936
- (componentName && name === componentName)) {
2096
+ (componentName && compSet.name === componentName)) {
1937
2097
  component = compSet;
1938
2098
  isComponentSet = true;
1939
2099
  break;
@@ -1941,9 +2101,9 @@ Without libraryFileKey/libraryFileUrl, searches the currently open file (local c
1941
2101
  }
1942
2102
  // Check standalone components
1943
2103
  if (!component) {
1944
- for (const [name, comp] of Object.entries(cacheEntry.manifest.components)) {
2104
+ for (const comp of Object.values(cacheEntry.manifest.components)) {
1945
2105
  if ((componentKey && comp.key === componentKey) ||
1946
- (componentName && name === componentName)) {
2106
+ (componentName && comp.name === componentName)) {
1947
2107
  component = comp;
1948
2108
  break;
1949
2109
  }
@@ -2363,8 +2523,9 @@ Without libraryFileKey/libraryFileUrl, searches the currently open file (local c
2363
2523
  registerWriteTools(this.server, () => this.getDesktopConnector());
2364
2524
  // Register token sync tools — figma_export_tokens and figma_import_tokens.
2365
2525
  // Replace Style Dictionary and Tokens Studio's export pipeline for the
2366
- // popular styling methods (DTCG canonical, plus CSS/Tailwind/SCSS/etc.
2367
- // as Phase 2+ extensions to a single internal token model).
2526
+ // popular styling methods (DTCG canonical legacy + 2025.10 dialects —
2527
+ // plus CSS/Tailwind/SCSS/TS/JSON/Style Dictionary/Tokens Studio, all
2528
+ // derived from a single internal token model).
2368
2529
  registerTokensTools(this.server, () => this.getDesktopConnector());
2369
2530
  // Register Figma API tools (Tools 8-11)
2370
2531
  registerFigmaAPITools(this.server, () => this.getFigmaAPI(), () => this.getCurrentFileUrl(), this.variablesCache, // Pass cache for efficient variable queries
@@ -2414,6 +2575,7 @@ Without libraryFileKey/libraryFileUrl, searches the currently open file (local c
2414
2575
  return "0.0.0";
2415
2576
  }
2416
2577
  },
2578
+ getBundledPluginVersion,
2417
2579
  getPluginState: () => {
2418
2580
  if (!this.wsServer)
2419
2581
  return null;
@@ -2427,6 +2589,8 @@ Without libraryFileKey/libraryFileUrl, searches the currently open file (local c
2427
2589
  editorType: fileInfo?.editorType,
2428
2590
  port: this.wsActualPort ?? undefined,
2429
2591
  portFallbackFrom: this.wsPreferredPort,
2592
+ pluginVersion: fileInfo?.pluginVersion,
2593
+ pluginUpdateAvailable: fileInfo?.pluginUpdateAvailable,
2430
2594
  };
2431
2595
  },
2432
2596
  getTokenState: () => {
@@ -2765,13 +2929,17 @@ Without libraryFileKey/libraryFileUrl, searches the currently open file (local c
2765
2929
  const wsHost = process.env.FIGMA_WS_HOST || 'localhost';
2766
2930
  this.wsPreferredPort = parseInt(process.env.FIGMA_WS_PORT || String(DEFAULT_WS_PORT), 10);
2767
2931
  // Clean up stale/orphaned MCP server instances before trying to bind.
2768
- // Phase 1: Remove stale port files and terminate zombie processes that have port files
2932
+ // Step 1: Remove stale port files and terminate zombie processes that have port files
2769
2933
  cleanupStalePortFiles();
2770
- // Phase 2: Deep scan for orphaned processes holding ports WITHOUT port files
2934
+ // Step 2: Deep scan for orphaned processes holding ports WITHOUT port files
2771
2935
  // (e.g., old instances from before port file tracking, or files already cleaned up)
2772
2936
  cleanupOrphanedProcesses(this.wsPreferredPort);
2773
2937
  const portsToTry = getPortRange(this.wsPreferredPort);
2774
2938
  let boundPort = null;
2939
+ // Distinguish "every port was in use" from "bind failed for another
2940
+ // reason" — eviction must only fire for genuine port exhaustion, and
2941
+ // figma_get_status must report the real error code.
2942
+ let lastNonPortError = null;
2775
2943
  for (const port of portsToTry) {
2776
2944
  try {
2777
2945
  this.wsServer = new FigmaWebSocketServer({ port, host: wsHost });
@@ -2806,12 +2974,18 @@ Without libraryFileKey/libraryFileUrl, searches the currently open file (local c
2806
2974
  }
2807
2975
  // Non-port-conflict error — don't try more ports
2808
2976
  logger.warn({ error: errorMsg, port }, "Failed to start WebSocket bridge server");
2977
+ lastNonPortError = {
2978
+ code: errorCode || "UNKNOWN",
2979
+ message: errorMsg,
2980
+ };
2809
2981
  this.wsServer = null;
2810
2982
  break;
2811
2983
  }
2812
2984
  }
2813
- // Phase 3: If all ports exhausted, try evicting the oldest instance and retry ONCE
2814
- if (!boundPort && evictOldestInstance(this.wsPreferredPort)) {
2985
+ // Phase 3: If all ports exhausted, try evicting the oldest instance and
2986
+ // retry ONCE. Only for genuine EADDRINUSE exhaustion — killing a healthy
2987
+ // sibling can't fix an EACCES/EADDRNOTAVAIL bind failure.
2988
+ if (!boundPort && !lastNonPortError && evictOldestInstance(this.wsPreferredPort)) {
2815
2989
  for (const port of portsToTry) {
2816
2990
  try {
2817
2991
  this.wsServer = new FigmaWebSocketServer({ port, host: wsHost });
@@ -2840,11 +3014,15 @@ Without libraryFileKey/libraryFileUrl, searches the currently open file (local c
2840
3014
  }
2841
3015
  if (!boundPort) {
2842
3016
  this.wsStartupError = {
2843
- code: "EADDRINUSE",
3017
+ code: lastNonPortError?.code ?? "EADDRINUSE",
2844
3018
  port: this.wsPreferredPort,
2845
3019
  };
2846
3020
  const rangeEnd = this.wsPreferredPort + portsToTry.length - 1;
2847
- logger.warn({ portRange: `${this.wsPreferredPort}-${rangeEnd}` }, "All WebSocket ports in range are in use — running without WebSocket transport");
3021
+ logger.warn(lastNonPortError
3022
+ ? { error: lastNonPortError }
3023
+ : { portRange: `${this.wsPreferredPort}-${rangeEnd}` }, lastNonPortError
3024
+ ? "WebSocket bridge failed to start (non-port error) — running without WebSocket transport"
3025
+ : "All WebSocket ports in range are in use — running without WebSocket transport");
2848
3026
  }
2849
3027
  if (this.wsServer) {
2850
3028
  // Log when plugin files connect/disconnect (with file identity)
@@ -2859,6 +3037,11 @@ Without libraryFileKey/libraryFileUrl, searches the currently open file (local c
2859
3037
  logger.info({ fileKey: data.fileKey, fileName: data.fileName }, "Desktop Bridge plugin disconnected from WebSocket");
2860
3038
  if (data.fileKey) {
2861
3039
  this.variablesCache.delete(data.fileKey);
3040
+ // design-system-tools.ts stores token data under a prefixed key
3041
+ this.variablesCache.delete(`vars:${data.fileKey}`);
3042
+ void import("./core/design-system-manifest.js").then(({ DesignSystemManifestCache }) => {
3043
+ DesignSystemManifestCache.getInstance().invalidate(data.fileKey);
3044
+ }).catch(() => { });
2862
3045
  }
2863
3046
  });
2864
3047
  // Invalidate variable cache when document changes are reported.
@@ -2868,8 +3051,15 @@ Without libraryFileKey/libraryFileUrl, searches the currently open file (local c
2868
3051
  this.wsServer.on("documentChange", (data) => {
2869
3052
  if (data.hasStyleChanges || data.hasNodeChanges) {
2870
3053
  if (data.fileKey) {
2871
- // Per-file cache invalidation — only clear the affected file's cache
3054
+ // Per-file cache invalidation — only clear the affected file's cache.
3055
+ // Also clear the design-system-tools token entry (prefixed key)
3056
+ // and the component manifest, so searches see new components
3057
+ // and the design-system kit sees edited variables.
2872
3058
  this.variablesCache.delete(data.fileKey);
3059
+ this.variablesCache.delete(`vars:${data.fileKey}`);
3060
+ void import("./core/design-system-manifest.js").then(({ DesignSystemManifestCache }) => {
3061
+ DesignSystemManifestCache.getInstance().invalidate(data.fileKey);
3062
+ }).catch(() => { });
2873
3063
  logger.debug({ fileKey: data.fileKey, changeCount: data.changeCount, hasStyleChanges: data.hasStyleChanges, hasNodeChanges: data.hasNodeChanges }, "Variable cache invalidated due to document changes");
2874
3064
  }
2875
3065
  else {