figma-console-mcp 1.38.0 → 1.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +16 -9
  2. package/dist/cloudflare/core/cloud-websocket-connector.js +12 -1
  3. package/dist/cloudflare/core/design-code-tools.js +6 -2
  4. package/dist/cloudflare/core/history/component-history.js +22 -5
  5. package/dist/cloudflare/core/history/history-formatter.js +4 -1
  6. package/dist/cloudflare/core/library-tools.js +56 -7
  7. package/dist/cloudflare/core/multi-file-tools.js +161 -0
  8. package/dist/cloudflare/core/port-discovery.js +101 -20
  9. package/dist/cloudflare/core/tokens-tools.js +1 -1
  10. package/dist/cloudflare/core/websocket-connector.js +5 -2
  11. package/dist/cloudflare/core/write-tools.js +9 -3
  12. package/dist/cloudflare/index.js +7 -7
  13. package/dist/core/figma-connector.d.ts +1 -1
  14. package/dist/core/figma-connector.d.ts.map +1 -1
  15. package/dist/core/library-tools.d.ts.map +1 -1
  16. package/dist/core/library-tools.js +56 -7
  17. package/dist/core/library-tools.js.map +1 -1
  18. package/dist/core/multi-file-tools.d.ts +9 -0
  19. package/dist/core/multi-file-tools.d.ts.map +1 -0
  20. package/dist/core/multi-file-tools.js +162 -0
  21. package/dist/core/multi-file-tools.js.map +1 -0
  22. package/dist/core/port-discovery.d.ts.map +1 -1
  23. package/dist/core/port-discovery.js +101 -20
  24. package/dist/core/port-discovery.js.map +1 -1
  25. package/dist/core/tokens-tools.js +1 -1
  26. package/dist/core/websocket-connector.d.ts +1 -1
  27. package/dist/core/websocket-connector.d.ts.map +1 -1
  28. package/dist/core/websocket-connector.js +5 -2
  29. package/dist/core/websocket-connector.js.map +1 -1
  30. package/dist/core/write-tools.d.ts.map +1 -1
  31. package/dist/core/write-tools.js +9 -3
  32. package/dist/core/write-tools.js.map +1 -1
  33. package/dist/local.d.ts.map +1 -1
  34. package/dist/local.js +6 -0
  35. package/dist/local.js.map +1 -1
  36. package/figma-desktop-bridge/code.js +1 -1
  37. package/figma-desktop-bridge/ui.html +7 -0
  38. package/package.json +1 -1
  39. package/dist/apps/design-system-dashboard/scoring/fix-generator.d.ts +0 -15
  40. package/dist/apps/design-system-dashboard/scoring/fix-generator.d.ts.map +0 -1
  41. package/dist/apps/design-system-dashboard/scoring/fix-generator.js +0 -270
  42. package/dist/apps/design-system-dashboard/scoring/fix-generator.js.map +0 -1
  43. package/dist/apps/design-system-dashboard/scoring/fix-types.d.ts +0 -32
  44. package/dist/apps/design-system-dashboard/scoring/fix-types.d.ts.map +0 -1
  45. package/dist/apps/design-system-dashboard/scoring/fix-types.js +0 -8
  46. package/dist/apps/design-system-dashboard/scoring/fix-types.js.map +0 -1
  47. package/dist/apps/token-browser.d.ts +0 -30
  48. package/dist/apps/token-browser.d.ts.map +0 -1
  49. package/dist/apps/token-browser.js +0 -680
  50. package/dist/apps/token-browser.js.map +0 -1
  51. package/dist/browser/base.d.ts +0 -58
  52. package/dist/browser/base.d.ts.map +0 -1
  53. package/dist/browser/base.js +0 -6
  54. package/dist/browser/base.js.map +0 -1
  55. package/dist/browser/local.d.ts +0 -87
  56. package/dist/browser/local.d.ts.map +0 -1
  57. package/dist/browser/local.js +0 -318
  58. package/dist/browser/local.js.map +0 -1
  59. package/dist/cloudflare/apps/design-system-dashboard/scoring/fix-generator.js +0 -269
  60. package/dist/cloudflare/apps/design-system-dashboard/scoring/fix-types.js +0 -7
  61. package/dist/cloudflare/apps/hello-world/server.js +0 -63
  62. package/dist/cloudflare/apps/token-browser.js +0 -679
  63. package/dist/cloudflare/browser/base.js +0 -5
  64. package/dist/cloudflare/browser/cloudflare.js +0 -156
  65. package/dist/cloudflare/core/code-to-spec.js +0 -1126
  66. package/dist/cloudflare/core/figma-desktop-connector.js +0 -1297
  67. package/dist/cloudflare/core/figma-mcp-client.js +0 -150
  68. package/dist/cloudflare/test-browser.js +0 -88
  69. package/dist/core/cdp-pipe-manager.d.ts +0 -96
  70. package/dist/core/cdp-pipe-manager.d.ts.map +0 -1
  71. package/dist/core/cdp-pipe-manager.js +0 -429
  72. package/dist/core/cdp-pipe-manager.js.map +0 -1
  73. package/dist/core/cloud-websocket-connector.d.ts +0 -58
  74. package/dist/core/cloud-websocket-connector.d.ts.map +0 -1
  75. package/dist/core/cloud-websocket-connector.js +0 -247
  76. package/dist/core/cloud-websocket-connector.js.map +0 -1
  77. package/dist/core/cloud-websocket-relay.d.ts +0 -51
  78. package/dist/core/cloud-websocket-relay.d.ts.map +0 -1
  79. package/dist/core/cloud-websocket-relay.js +0 -171
  80. package/dist/core/cloud-websocket-relay.js.map +0 -1
  81. package/dist/core/code-to-spec.d.ts +0 -103
  82. package/dist/core/code-to-spec.d.ts.map +0 -1
  83. package/dist/core/code-to-spec.js +0 -1127
  84. package/dist/core/code-to-spec.js.map +0 -1
  85. package/dist/core/figma-desktop-connector.d.ts +0 -312
  86. package/dist/core/figma-desktop-connector.d.ts.map +0 -1
  87. package/dist/core/figma-desktop-connector.js +0 -1298
  88. package/dist/core/figma-desktop-connector.js.map +0 -1
  89. package/dist/core/figma-mcp-client.d.ts +0 -61
  90. package/dist/core/figma-mcp-client.d.ts.map +0 -1
  91. package/dist/core/figma-mcp-client.js +0 -151
  92. package/dist/core/figma-mcp-client.js.map +0 -1
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  > **Your design system as an API.** Model Context Protocol server that bridges design and development—giving AI assistants complete access to Figma for **extraction**, **creation**, **debugging**, and **bidirectional token sync**.
10
10
 
11
- > **🆕 Ongoing Component Changelogs in Generated Docs (v1.38.0):** `figma_generate_component_doc` now pulls a *real* changelog instead of only echoing hand-written entries. Opt in with `history: { figma: true, git: true }` to get a `## History` section combining **design history** — Figma version history diffed per version and scoped to that component, so you see the rename, the property that was added, the tokens that got bound — with **code history** from `git log` on the component's source files, auto-derived from `codeInfo`. Prefers labeled versions and falls back to auto-saves when a file has none (most don't). Off by default, so existing calls are byte-identical. Server-only release — **no plugin re-import needed**. [See what's new →](CHANGELOG.md#1380---2026-07-25)
11
+ > **🗂 Multi-File Execution (v1.39.0):** Work across several Figma files at once. `figma_execute_across_files` runs one script against every (or a chosen subset of) connected file **concurrently** — for auditing a multi-file design system for the same problem, or applying one fix across a known set, instead of switching the active file and repeating yourself. Four files × 2s of work completes in ~3s, not ~8s. `figma_execute` also takes an optional `fileKey` to target a single file without moving the active file or releasing target lock. You must name `fileKeys` or pass `allFiles: true` — there's no fan-out-to-everything default, since this runs code in files you may be actively editing. From community PR [#107](https://github.com/southleft/figma-console-mcp/pull/107) by [@Wolfr](https://github.com/Wolfr). Also fixes a relay bug that silently dropped `resultAnalysis` and `fileContext` from every `figma_execute` response — **re-import `manifest.json`** for those two fields; everything else works without it. [See what's new →](CHANGELOG.md#1390---2026-08-02)
12
12
 
13
13
  ## What is this?
14
14
 
@@ -56,9 +56,9 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
56
56
  | Real-time monitoring (console, selection) | ✅ | ❌ | ❌ |
57
57
  | Desktop Bridge plugin | ✅ | ✅ | ❌ |
58
58
  | Requires Node.js | Yes | **No** | No |
59
- | **Total tools available** | **113** | **101** | **9** |
59
+ | **Total tools available** | **114** | **101** | **9** |
60
60
 
61
- > **Bottom line:** Remote SSE is **read-only** with 9 tools. **Cloud Mode** unlocks write access (101 tools) from web AI clients without Node.js. NPX/Local Git gives the full 113 tools with real-time monitoring.
61
+ > **Bottom line:** Remote SSE is **read-only** with 9 tools. **Cloud Mode** unlocks write access (101 tools) from web AI clients without Node.js. NPX/Local Git gives the full 114 tools with real-time monitoring.
62
62
 
63
63
  ---
64
64
 
@@ -66,7 +66,7 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
66
66
 
67
67
  **Best for:** Designers who want full AI-assisted design capabilities.
68
68
 
69
- **What you get:** All 113 tools including design creation, variable management, and component instantiation.
69
+ **What you get:** All 114 tools including design creation, variable management, and component instantiation.
70
70
 
71
71
  #### Prerequisites
72
72
 
@@ -163,7 +163,7 @@ Create a simple frame with a blue background
163
163
 
164
164
  **Best for:** Developers who want to modify source code or contribute to the project.
165
165
 
166
- **What you get:** Same 113 tools as NPX, plus full source code access.
166
+ **What you get:** Same 114 tools as NPX, plus full source code access.
167
167
 
168
168
  #### Quick Setup
169
169
 
@@ -309,7 +309,7 @@ AI Client → Cloud MCP Server → Durable Object Relay → Desktop Bridge Plugi
309
309
  | Feature | NPX (Recommended) | Cloud Mode | Local Git | Remote SSE |
310
310
  |---------|-------------------|------------|-----------|------------|
311
311
  | **Setup time** | ~10 minutes | ~5 minutes | ~15 minutes | ~2 minutes |
312
- | **Total tools** | **113** | **101** | **113** | **9** (read-only) |
312
+ | **Total tools** | **114** | **101** | **114** | **9** (read-only) |
313
313
  | **Design creation** | ✅ | ✅ | ✅ | ❌ |
314
314
  | **Variable management** | ✅ | ✅ | ✅ | ❌ |
315
315
  | **Component instantiation** | ✅ | ✅ | ✅ | ❌ |
@@ -324,7 +324,7 @@ AI Client → Cloud MCP Server → Durable Object Relay → Desktop Bridge Plugi
324
324
  | **Automatic updates** | ✅ (`@latest`) | ✅ | Manual (`git pull`) | ✅ |
325
325
  | **Source code access** | ❌ | ❌ | ✅ | ❌ |
326
326
 
327
- > **Key insight:** Remote SSE is read-only. Cloud Mode adds write access for web AI clients without Node.js. NPX/Local Git give the full 113 tools.
327
+ > **Key insight:** Remote SSE is read-only. Cloud Mode adds write access for web AI clients without Node.js. NPX/Local Git give the full 114 tools.
328
328
 
329
329
  **📖 [Complete Feature Comparison](docs/mode-comparison.md)**
330
330
 
@@ -417,6 +417,10 @@ When you first use design system tools:
417
417
  - Create frames, shapes, text, components
418
418
  - Apply auto-layout, styles, effects
419
419
  - Build complete UI mockups programmatically
420
+ - Optional `fileKey` targets one specific connected file directly (Local Mode only), without touching the active file or target lock
421
+ - `figma_execute_across_files` - **Local Mode only.** Run the same script in several Desktop Bridge-connected files at once, concurrently, returning a per-file result map — for cross-file consistency checks and fixes across a multi-file design system, replacing "open file, run plugin, repeat per file"
422
+ - Name the files with `fileKeys` (from `figma_list_open_files`), or pass `allFiles: true` to hit every connected file — one of the two is required, so a script never fans out to files you didn't mean to touch
423
+ - Per-file timeouts and error isolation: one slow or failing file doesn't delay or fail the others
420
424
  - `figma_create_component_set` - **Create a component set with variants in one declarative call**
421
425
  - Generate every variant combination from an axes matrix (e.g. `{ State: ["default", "hover", "disabled"], Size: ["sm", "lg"] }` → 6 variants) off a base component, or combine existing components
422
426
  - `Prop=Value` variant naming, `combineAsVariants` under the hood, optional auto-arranged labeled grid
@@ -682,7 +686,7 @@ The **Figma Desktop Bridge** plugin is the recommended way to connect Figma to t
682
686
  - The MCP server communicates via **WebSocket** through the Desktop Bridge plugin
683
687
  - The server tries port 9223 first, then automatically falls back through ports 9224–9232 if needed
684
688
  - The plugin scans all ports in the range and connects to every active server it finds
685
- - All 113 tools work through the WebSocket transport
689
+ - All 114 tools work through the WebSocket transport
686
690
 
687
691
  **Multiple files:** The WebSocket server supports multiple simultaneous plugin connections — one per open Figma file. Each connection is tracked by file key with independent state (selection, document changes, console logs).
688
692
 
@@ -823,9 +827,12 @@ The architecture supports adding new apps with minimal boilerplate — each app
823
827
 
824
828
  ## 🛤️ Roadmap
825
829
 
826
- **Current Status:** v1.38.0 (Stable) - Production-ready. Latest: Ongoing component changelogs in generated docs — `figma_generate_component_doc` takes an opt-in `history` parameter that pulls per-component **design history** from Figma version history (each version diffed and scoped to that component, so renames, added properties, and token bindings show up as rows) alongside **code history** from `git log` on the component's source files, rendered as a `## History` section with design, code, and release-note tables. Prefers labeled versions, falls back to auto-saves when a file has none, and is off by default so existing calls are unchanged (server-only, no plugin re-import). On top of v1.37.x's design-system health audit for every client — `figma_audit_design_system_report` runs the dashboard's deterministic six-category scoring engine and returns the report as data with per-finding remediation, live-first fileKey-verified data (source disclosed), chunked per-category drill-down, and a 5-minute cache (server-only, no plugin re-import). On top of v1.36.0's target lock for multi-file parallel work — — `figma_navigate` takes a `lock: true` flag that pins the active file so an AI agent can work in one file while you work in another without commands routing to the wrong file. On top of v1.35.0's Figma Slots write support — create, inspect, populate, and reset Slots (GA at Config 2026) via 5 new tools (`figma_create_slot`, `figma_get_slots`, `figma_append_to_slot`, `figma_reset_slot`, `figma_add_slot_property`), live-validated against the GA Plugin API and based on community PR #77. On top of v1.34.0's Bidirectional Token Sync v2 + DTCG 2025.10 — `figma_import_tokens` applies the complete diff plan (creates missing collections/variables, applies renames, writes real `VARIABLE_ALIAS` references, and deletes only under explicit `replace`), `figma_export_tokens` speaks the DTCG 2025.10 dialect on request (legacy default byte-identical), variable scopes/codeSyntax round-trip via `$extensions`, `figma_setup_design_tokens` accepts alias values via DTCG brace references, and `figma_create_component_set` builds a full variant set from an axes matrix in one call. On top of the v1.33.x line: version-handshake fix (re-import banner only fires when plugin files actually changed), security dependency sweep, and the v1.33.0 connection UX overhaul (honest status pill derived from live connection state, `/health` auto-discovery with self-healing reconnect) + a 33-fix full-codebase audit (lossless DTCG multi-mode round-trips, cross-collection alias resolution, branch-URL correctness across REST tools, cache-poisoning and CSWSH fixes, bridge-first screenshots). Built on WCAG-accurate accessibility auditing (line height below 1.5× is no longer mis-flagged as a failure; readability hints decoupled from conformance checks and scoped to multi-line text; code-side WCAG 1.4.12 check), a self-healing Desktop Bridge connection (zombie-process reaper + auto-reconnect watchdog — fixes the recurring "not connected until restart" bug), native variable binding on fills/strokes + typography control in the write tools, shared-library inspection (key-based component resolution + library variable read/import without Enterprise plan), 10-format token export pipeline (DTCG, CSS, Tailwind v4, Tailwind v3, SCSS, TS module, JSON flat/nested, Style Dictionary v3, Tokens Studio), bidirectional Figma↔code token sync, version history & time-series awareness, FigJam + Slides support, Cloud Write Relay, Design System Kit, WebSocket-only connectivity, smart multi-file tracking, **113 tools** (Local) / **101 tools** (Cloud) / **9 tools** (Remote read-only), Comments API, cross-MCP identity disambiguation, and MCP Apps.
830
+ **Current Status:** v1.39.0 (Stable) - Production-ready. Latest: multi-file execution — `figma_execute_across_files` runs one script against several Desktop Bridge-connected files concurrently (four files x 2s of work in ~3s, not ~8s), and `figma_execute` takes an optional `fileKey` to target a single file without moving the active file or releasing target lock; targeting must be explicit (`fileKeys` or `allFiles: true`), so nothing fans out to files you did not name. From community PR #107 by @Wolfr. Ships with a relay fix: `handleResult()` in the plugin's ui.html rebuilt each response field by field and silently dropped `resultAnalysis` and `fileContext`, so the `resultAnalysis.warning` check that `figma_execute`'s own description tells callers to perform has never been possible (re-import `manifest.json` for those two fields; everything else works without it). On top of v1.38.2's connection-stability fix — the server's own orphan reaper was terminating healthy MCP servers because its liveness probe used an IPv4 literal while the server binds the IPv6 loopback, which turned every kill-safety gate into a rubber stamp; this was the cause of recurring "Server disconnected" errors across all MCP clients. Alongside v1.38.1's fix for the shared-library variable tools, which had silently reported zero collections since v1.29.0 and reported failed imports as successes. On top of v1.38.0's ongoing component changelogs in generated docs — `figma_generate_component_doc` takes an opt-in `history` parameter that pulls per-component **design history** from Figma version history (each version diffed and scoped to that component, so renames, added properties, and token bindings show up as rows) alongside **code history** from `git log` on the component's source files, rendered as a `## History` section with design, code, and release-note tables. Prefers labeled versions, falls back to auto-saves when a file has none, and is off by default so existing calls are unchanged (server-only, no plugin re-import). On top of v1.37.x's design-system health audit for every client — `figma_audit_design_system_report` runs the dashboard's deterministic six-category scoring engine and returns the report as data with per-finding remediation, live-first fileKey-verified data (source disclosed), chunked per-category drill-down, and a 5-minute cache (server-only, no plugin re-import). On top of v1.36.0's target lock for multi-file parallel work — — `figma_navigate` takes a `lock: true` flag that pins the active file so an AI agent can work in one file while you work in another without commands routing to the wrong file. On top of v1.35.0's Figma Slots write support — create, inspect, populate, and reset Slots (GA at Config 2026) via 5 new tools (`figma_create_slot`, `figma_get_slots`, `figma_append_to_slot`, `figma_reset_slot`, `figma_add_slot_property`), live-validated against the GA Plugin API and based on community PR #77. On top of v1.34.0's Bidirectional Token Sync v2 + DTCG 2025.10 — `figma_import_tokens` applies the complete diff plan (creates missing collections/variables, applies renames, writes real `VARIABLE_ALIAS` references, and deletes only under explicit `replace`), `figma_export_tokens` speaks the DTCG 2025.10 dialect on request (legacy default byte-identical), variable scopes/codeSyntax round-trip via `$extensions`, `figma_setup_design_tokens` accepts alias values via DTCG brace references, and `figma_create_component_set` builds a full variant set from an axes matrix in one call. On top of the v1.33.x line: version-handshake fix (re-import banner only fires when plugin files actually changed), security dependency sweep, and the v1.33.0 connection UX overhaul (honest status pill derived from live connection state, `/health` auto-discovery with self-healing reconnect) + a 33-fix full-codebase audit (lossless DTCG multi-mode round-trips, cross-collection alias resolution, branch-URL correctness across REST tools, cache-poisoning and CSWSH fixes, bridge-first screenshots). Built on WCAG-accurate accessibility auditing (line height below 1.5× is no longer mis-flagged as a failure; readability hints decoupled from conformance checks and scoped to multi-line text; code-side WCAG 1.4.12 check), a self-healing Desktop Bridge connection (zombie-process reaper + auto-reconnect watchdog — fixes the recurring "not connected until restart" bug), native variable binding on fills/strokes + typography control in the write tools, shared-library inspection (key-based component resolution + library variable read/import without Enterprise plan), 10-format token export pipeline (DTCG, CSS, Tailwind v4, Tailwind v3, SCSS, TS module, JSON flat/nested, Style Dictionary v3, Tokens Studio), bidirectional Figma↔code token sync, version history & time-series awareness, FigJam + Slides support, Cloud Write Relay, Design System Kit, WebSocket-only connectivity, smart multi-file tracking, **114 tools** (Local) / **101 tools** (Cloud) / **9 tools** (Remote read-only), Comments API, cross-MCP identity disambiguation, and MCP Apps.
827
831
 
828
832
  **Recent Releases:**
833
+ - [x] **v1.39.0** - Multi-file execution. New `figma_execute_across_files` runs one script against several Desktop Bridge-connected files concurrently, with per-file error isolation and independent timeouts (verified live: four files × 2s of work in ~3.0s, all dispatches within 5ms — versus ~8s serialized). `figma_execute` gained an optional `fileKey` to target a single non-active file without moving the active file or releasing target lock. Targeting is deliberately explicit — `fileKeys` or `allFiles: true`, no fan-out-to-everything default — because this runs arbitrary code in files you may be actively editing, including one pinned by target lock. The transport was already concurrent (`sendCommand` has always taken a target file key, and every hop is request-id keyed); only the tool layer was missing. Community PR #107 by @Wolfr. Also fixes a Desktop Bridge relay bug that silently dropped `resultAnalysis` and `fileContext` from every `figma_execute` response since they were introduced — making the `resultAnalysis.warning` check the tool's own description tells callers to perform impossible — guarded now by a test that reads the real plugin files. **Re-import `manifest.json`** for those two fields; the rest works without it, and mixed plugin versions degrade cleanly. 52 suites / 1443 tests.
834
+ - [x] **v1.38.2** - Connection stability: the orphan reaper was terminating healthy MCP servers, the cause of recurring "Server disconnected" errors across every MCP client (reproduced in both Claude Code and Claude Desktop; unrelated to the Desktop Bridge plugin). The reaper probes a sibling's `/health` before deciding it is dead, but requested `127.0.0.1` while the WebSocket server binds `localhost` — resolved to the IPv6 loopback on dual-stack macOS, with nothing on IPv4 — so the probe reported "nothing responding" for healthy servers and every kill-safety gate built on it became a rubber stamp, including the one written to spare siblings after the machine sleeps. Three supporting defects: port files written non-atomically (a reader landing mid-rewrite got a parse error that both cleanup paths treated as "corrupt, delete it", stranding a healthy server), an orphan path that terminated fileless port-holders without probing at all, and a heartbeat that gave up permanently once its own file went missing. Fixes: probe `localhost`, atomic temp+rename writes, never delete on a parse failure, health-probe before the orphan kill in both paths, and self-healing re-advertisement guarded by in-process port ownership. **Fully restart MCP clients after upgrading** — older running builds keep the broken probe and still terminate healthy siblings. Server-only, no plugin re-import. 1422 tests (7 new regressions, each verified to fail pre-fix).
835
+ - [x] **v1.38.1** - Shared-library variable tools fixed. `figma_get_library_variables` returned `totalCollections: 0` for every file, on every plan, since it shipped in v1.29.0, and `figma_import_library_variable` reported failed imports as successes with `id: undefined`. Both read the Desktop Bridge's `{ success, result }` envelope as if it were the injected script's bare return value, so `Array.isArray()` was always false, the `__error` guard was dead code, and `.id` was always undefined. The failure presented as a *success* with a plausible hint attached ("subscribe a library via the Assets panel"), which is why it survived two months unreported. Both tools now unwrap through a shared helper that also maps a bridge-level `success: false` onto the error path, so a plugin timeout surfaces as a real error instead of being swallowed as "0 collections"; an import yielding no `id` is now an explicit error. All 26 `executeCodeViaUI` call sites audited — only these two were affected. Test mocks had encoded a wire contract that does not exist, which is why CI stayed green; they now reproduce the real envelope, plus six regression tests verified to fail against the pre-fix source. Reported by Isabella Minzly. Server-only — no plugin re-import needed.
829
836
  - [x] **v1.38.0** - Ongoing component changelogs in generated docs. `figma_generate_component_doc` gained an opt-in `history` parameter that produces a real changelog instead of only echoing hand-written `codeInfo.changelog` rows: `history.figma` walks Figma version history and diffs each consecutive pair scoped to the component (reusing the same engine as `figma_diff_versions`, so a rename, a newly added component property, or a token binding each become a row), while `history.git` runs `git log` over the component's source files auto-derived from `codeInfo.filePath` / `sourceFiles[]`. Labeled versions are preferred but auto-saves are used automatically when a file has none — verified against a mature system with 72 auto-saves and 0 labeled versions, where a labeled-only walk yields nothing. Detailed-mode bindings group by property (binding one token across a 24-variant set otherwise produced 44 near-identical bullets in a single table cell). Both sources default off, so existing callers get byte-identical output. No new tools; server-only, no plugin re-import.
830
837
  - [x] **v1.37.0** - Design-system health audit for every MCP client. New `figma_audit_design_system_report` runs the Design System Dashboard's deterministic scoring engine (naming, token architecture, component metadata, accessibility, consistency, coverage) and returns the scored report as data — no MCP Apps support or `ENABLE_MCP_APPS` needed (the previous registration gated everything, making the audit unreachable outside Claude Desktop). Component data is now live-first: a fileKey-verified per-page bridge crawl (30s/page, failures isolated per page) with REST published-library fallback, and the chosen source is disclosed in the report (`bridge-live` / `rest-published` / `none`) so stale-publish scores can't masquerade as live ones. Output is token-safe by design: bounded summary, per-category chunked drill-down, clamped full JSON, 5-minute raw-data cache. Every finding carries a remediation verdict — auto-fixable by this MCP's write tools, fixable after a design decision, or manual — with the exact tools named. Scoring accuracy pass: variant components no longer poison PascalCase/casing checks, Title Case accepted, component-set names count toward core-component coverage, `color/content/*` recognized in contrast pairing, state synonyms (`active`≈`pressed` etc.), and `.`/`_`-prefixed internals excluded. Server-only — no plugin re-import needed.
831
838
  - [x] **v1.36.0** - Target lock for multi-file parallel work. `figma_navigate` gains a `lock: true` flag that pins the active file — new connections, reconnects, and the user's own selection/page changes in other files no longer move the command target, so an agent can safely write to one file while the user works in another. Auto-releases when the pinned file disconnects or navigates away; `figma_list_open_files` reports a `targetLocked` flag for pre-write guards. Server-only — no plugin re-import needed.
@@ -56,7 +56,18 @@ export class CloudWebSocketConnector {
56
56
  `;
57
57
  return this.sendCommand('EXECUTE_CODE', { code, timeout: 30000 }, 32000);
58
58
  }
59
- async executeCodeViaUI(code, timeoutMs = 5000) {
59
+ async executeCodeViaUI(code, timeoutMs = 5000, fileKey) {
60
+ // fileKey exists for IFigmaConnector parity but cannot be honored here —
61
+ // the cloud relay pairs with exactly one plugin instance, so there is no
62
+ // multi-file target to route between (matches getVariables* above).
63
+ // Reject rather than silently running against the paired file: a caller
64
+ // that asked for file B and got a successful write to file A has no way
65
+ // to tell from the response that it was misrouted.
66
+ if (fileKey) {
67
+ throw new Error('Per-file targeting (fileKey) is Local Mode only. Cloud Mode pairs with a single ' +
68
+ 'plugin instance, so there is no second file to route to. Omit fileKey to run ' +
69
+ 'against the paired file, or use Local Mode (NPX/Git) for multi-file work.');
70
+ }
60
71
  return this.sendCommand('EXECUTE_CODE', { code, timeout: timeoutMs }, timeoutMs + 2000);
61
72
  }
62
73
  // ============================================================================
@@ -1476,11 +1476,13 @@ latestDesignVersion) {
1476
1476
  if (latestDesignVersion) {
1477
1477
  // Quote the label — it's user-authored and may contain YAML-significant
1478
1478
  // characters (a colon in "v2: buttons" would otherwise break parsing).
1479
+ // Unlabeled auto-saves fall back to the date rather than the raw 19-digit
1480
+ // version ID, which carries no meaning for a docs reader.
1481
+ const date = latestDesignVersion.created_at?.split("T")[0];
1479
1482
  const label = latestDesignVersion.label
1480
1483
  ? `"${latestDesignVersion.label.replace(/"/g, '\\"')}"`
1481
- : latestDesignVersion.version_id;
1484
+ : date || latestDesignVersion.version_id;
1482
1485
  lines.push(`figmaVersion: ${label}`);
1483
- const date = latestDesignVersion.created_at?.split("T")[0];
1484
1486
  if (date)
1485
1487
  lines.push(`figmaVersionDate: ${date}`);
1486
1488
  }
@@ -2919,6 +2921,8 @@ export function registerDesignCodeTools(server, getFigmaAPI, getCurrentUrl, vari
2919
2921
  entries: designHistory.entries.length,
2920
2922
  versionsScanned: designHistory._meta.versions_scanned,
2921
2923
  apiCalls: designHistory._meta.api_calls,
2924
+ usedAutosaveFallback: designHistory._meta.used_autosave_fallback === true,
2925
+ latestVersionId: latestDesignVersion?.version_id ?? null,
2922
2926
  notes: designHistory.notes,
2923
2927
  }
2924
2928
  : null,
@@ -73,20 +73,37 @@ export async function buildDesignHistory(api, fileKey, nodeIds, options = {}) {
73
73
  return result;
74
74
  }
75
75
  // N rows needs N+1 snapshots: each row is a diff between adjacent versions.
76
- const listed = await listVersions(api, fileKey, {
76
+ let listed = await listVersions(api, fileKey, {
77
77
  limit: versionsWanted + 1,
78
78
  includeAutosaves,
79
79
  });
80
80
  result._meta.api_calls += listed.apiCalls;
81
- result._meta.versions_scanned = listed.versions.length;
82
81
  if (listed.error) {
82
+ result._meta.versions_scanned = listed.versions.length;
83
83
  result.notes.push(listed.error);
84
84
  return result;
85
85
  }
86
+ // Labeled versions are better changelog material when a team curates them,
87
+ // but plenty of real design-system files have none at all (verified: a mature
88
+ // file with 72 auto-saves and 0 labeled versions). Rather than silently
89
+ // emitting nothing there, fall back to auto-saves and say so. Autosave noise
90
+ // is largely absorbed downstream anyway — a version only becomes a row if it
91
+ // actually changed the scoped component.
92
+ if (!includeAutosaves && listed.versions.length < 2) {
93
+ const fallback = await listVersions(api, fileKey, {
94
+ limit: versionsWanted + 1,
95
+ includeAutosaves: true,
96
+ });
97
+ result._meta.api_calls += fallback.apiCalls;
98
+ if (!fallback.error && fallback.versions.length >= 2) {
99
+ listed = fallback;
100
+ result._meta.used_autosave_fallback = true;
101
+ result.notes.push("This file has no labeled versions, so history was built from Figma auto-saves. Name versions in Figma (right-click a version → Add label) for a more curated changelog.");
102
+ }
103
+ }
104
+ result._meta.versions_scanned = listed.versions.length;
86
105
  if (listed.versions.length < 2) {
87
- result.notes.push(includeAutosaves
88
- ? "Not enough version history to build a changelog (need at least 2 saved versions)."
89
- : "Not enough labeled version history to build a changelog (need at least 2 named versions). Figma auto-saves are excluded by default — pass includeAutosaves to use them.");
106
+ result.notes.push("Not enough version history to build a changelog (need at least 2 saved versions).");
90
107
  return result;
91
108
  }
92
109
  // listVersions returns newest-first. Row i describes what changed when
@@ -42,7 +42,10 @@ function appendDesignHistory(lines, design) {
42
42
  lines.push("| Version | Date | Author | Changes |");
43
43
  lines.push("|---------|------|--------|---------|");
44
44
  for (const e of design.entries) {
45
- const version = e.label ? escapeCell(e.label) : `\`${e.version_id}\` _(autosave)_`;
45
+ // Auto-saves have no label and a 19-digit numeric ID that means nothing
46
+ // to a reader — the Date column already identifies the row. The raw ID
47
+ // stays available programmatically via historySummary / the entry object.
48
+ const version = e.label ? escapeCell(e.label) : "_(auto-save)_";
46
49
  const date = formatDate(e.created_at);
47
50
  const author = e.author ? escapeCell(e.author) : "—";
48
51
  const changes = e.changes.length > 0
@@ -317,6 +317,33 @@ export function registerLibraryTools(server, getFigmaAPI) {
317
317
  function safeKey(value) {
318
318
  return JSON.stringify(value);
319
319
  }
320
+ /**
321
+ * Unwrap the Desktop Bridge envelope around an executeCodeViaUI() result.
322
+ *
323
+ * The bridge resolves to `{ success, result }` (built by handleResult() in
324
+ * figma-desktop-bridge/ui.html) — NEVER the bare value the injected script
325
+ * returned. Reading the envelope as if it were the script's own return value
326
+ * silently yields the wrong type: `Array.isArray(envelope)` is always false,
327
+ * and a `{ __error }` sentinel lands at `.result.__error`, not `.__error`.
328
+ * That turns a hard failure into a confident-looking empty success, which is
329
+ * worse than a crash — see the regression tests in tests/library-tools.test.ts.
330
+ *
331
+ * Returns the script's value, or `{ __error }` when the bridge itself reported
332
+ * failure so callers can route both failure modes through one guard. The
333
+ * `?? raw` fallback keeps this tolerant of a bare value, matching the
334
+ * defensive `execResult.result ?? execResult` precedent in tokens-tools.ts.
335
+ */
336
+ function unwrapBridgeResult(raw) {
337
+ if (raw && typeof raw === "object" && raw.success === false) {
338
+ return {
339
+ __error: raw.error || "Desktop Bridge reported a failure with no error message.",
340
+ };
341
+ }
342
+ if (raw && typeof raw === "object" && "result" in raw) {
343
+ return raw.result ?? raw;
344
+ }
345
+ return raw;
346
+ }
320
347
  export function registerLibraryVariableTools(server, getDesktopConnector) {
321
348
  // --------------------------------------------------------------------
322
349
  // figma_get_library_variables — list available variables from
@@ -393,15 +420,16 @@ export function registerLibraryVariableTools(server, getDesktopConnector) {
393
420
  return out;
394
421
  `;
395
422
  const raw = await connector.executeCodeViaUI(script, 30000);
423
+ const payload = unwrapBridgeResult(raw);
396
424
  // Detect the wrapped-error sentinel
397
- if (raw && typeof raw === "object" && raw.__error) {
425
+ if (payload && typeof payload === "object" && payload.__error) {
398
426
  return {
399
427
  content: [
400
428
  {
401
429
  type: "text",
402
430
  text: JSON.stringify({
403
431
  _mcp: "figma-console-mcp",
404
- error: raw.__error,
432
+ error: payload.__error,
405
433
  hint: "Plugin sandbox could not access the team library API. Ensure Figma Desktop is recent and the Desktop Bridge is connected.",
406
434
  }),
407
435
  },
@@ -409,7 +437,7 @@ export function registerLibraryVariableTools(server, getDesktopConnector) {
409
437
  isError: true,
410
438
  };
411
439
  }
412
- let collections = Array.isArray(raw) ? raw : [];
440
+ let collections = Array.isArray(payload) ? payload : [];
413
441
  // Apply server-side filters (cheaper to filter here than in plugin)
414
442
  if (libraryName) {
415
443
  const needle = libraryName.toLowerCase();
@@ -520,8 +548,9 @@ After import, the variable becomes locally addressable by its returned 'id' and
520
548
  }
521
549
  `;
522
550
  const raw = await connector.executeCodeViaUI(script, 20000);
523
- if (raw && typeof raw === "object" && raw.__error) {
524
- const errMsg = raw.__error;
551
+ const imported = unwrapBridgeResult(raw);
552
+ if (imported && typeof imported === "object" && imported.__error) {
553
+ const errMsg = imported.__error;
525
554
  let hint = "Verify the variableKey was returned by figma_get_library_variables and that the source library is subscribed by the current file.";
526
555
  if (/not subscribed|not enabled|access/i.test(errMsg)) {
527
556
  hint =
@@ -542,15 +571,35 @@ After import, the variable becomes locally addressable by its returned 'id' and
542
571
  isError: true,
543
572
  };
544
573
  }
574
+ // An import with no id is a failure however it got here — never
575
+ // report it as a success with `id: undefined`, which is
576
+ // indistinguishable from a real import to a scripted caller.
577
+ if (!imported || typeof imported !== "object" || !imported.id) {
578
+ return {
579
+ content: [
580
+ {
581
+ type: "text",
582
+ text: JSON.stringify({
583
+ _mcp: "figma-console-mcp",
584
+ error: "Import returned no variable id — the variable was not imported.",
585
+ hint: "Verify the variableKey came from figma_get_library_variables and that the source library is subscribed by the current file.",
586
+ variableKey,
587
+ received: imported,
588
+ }),
589
+ },
590
+ ],
591
+ isError: true,
592
+ };
593
+ }
545
594
  return {
546
595
  content: [
547
596
  {
548
597
  type: "text",
549
598
  text: JSON.stringify({
550
599
  _mcp: "figma-console-mcp",
551
- imported: raw,
600
+ imported,
552
601
  usage: {
553
- bind: `Use the imported variable's id ('${raw?.id}') with figma_set_fills, figma_update_variable, or any other variable-binding tool to reference this token from nodes in the current file.`,
602
+ bind: `Use the imported variable's id ('${imported.id}') with figma_set_fills, figma_update_variable, or any other variable-binding tool to reference this token from nodes in the current file.`,
554
603
  note: "Variables imported from a library remain linked to their source — updates published from the library will propagate here automatically.",
555
604
  },
556
605
  }),
@@ -0,0 +1,161 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Register tools that operate across every Figma file connected via the
4
+ * Desktop Bridge plugin at once, rather than only the single active file.
5
+ * Local mode only — cloud/relay mode pairs with exactly one plugin instance.
6
+ */
7
+ export function registerMultiFileTools(server, getWsServer, getDesktopConnector) {
8
+ server.tool("figma_execute_across_files", "Run the same JavaScript against multiple Figma files connected via the Desktop Bridge plugin, IN PARALLEL, without touching the active file or target lock. Built for cross-file consistency work — e.g. running the same structural check or fix against every file in a multi-file design system and diffing the results — instead of switching the active file and running figma_execute once per file in sequence. Each file's code runs in that file's own plugin context (same 'figma' global as figma_execute). You must say which files to target: pass fileKeys (read them from figma_list_open_files first — strongly preferred for anything that writes), or pass allFiles: true to hit every connected file. Requires Desktop Bridge plugin open in each target file. Local Mode only.", {
9
+ code: z
10
+ .string()
11
+ .describe("JavaScript code to execute in each targeted file. Has access to the 'figma' global object, same as figma_execute. Example: 'return { pageCount: figma.root.children.length };'"),
12
+ fileKeys: z
13
+ .array(z.string())
14
+ .optional()
15
+ .describe("Which connected files to target, by fileKey. Get fileKeys from figma_list_open_files. Required unless allFiles is true."),
16
+ allFiles: z
17
+ .boolean()
18
+ .optional()
19
+ .default(false)
20
+ .describe("Run against EVERY currently connected file. Opt-in on purpose: this executes the code in files the user may be actively working in, including any file pinned by target lock. Prefer naming fileKeys explicitly for anything that writes."),
21
+ timeout: z
22
+ .number()
23
+ .optional()
24
+ .default(10000)
25
+ .describe("Per-file execution timeout in milliseconds (default: 10000, max: 30000). Applies independently to each file — one slow/unresponsive file does not delay the others."),
26
+ }, async ({ code, fileKeys, allFiles, timeout }) => {
27
+ try {
28
+ const wsServer = getWsServer();
29
+ if (!wsServer?.isClientConnected()) {
30
+ return {
31
+ content: [{
32
+ type: "text",
33
+ text: JSON.stringify({
34
+ error: "No files connected. Open the Desktop Bridge plugin in Figma to connect files.",
35
+ results: {},
36
+ }),
37
+ }],
38
+ isError: true,
39
+ };
40
+ }
41
+ const connectedFiles = wsServer.getConnectedFiles();
42
+ const cappedTimeout = Math.min(timeout, 30000);
43
+ // Fanning out to every connected file has to be asked for. This
44
+ // runs arbitrary code in files the user may be actively editing,
45
+ // including one pinned by target lock — the exact misrouting that
46
+ // target lock exists to prevent. "All" is a decision, not a default.
47
+ const hasExplicitTargets = Boolean(fileKeys && fileKeys.length > 0);
48
+ if (!hasExplicitTargets && !allFiles) {
49
+ return {
50
+ content: [{
51
+ type: "text",
52
+ text: JSON.stringify({
53
+ error: "No target files specified. Pass fileKeys to name the files to run against, or allFiles: true to run against every connected file.",
54
+ hint: "Call figma_list_open_files to see connected fileKeys. Naming files explicitly is strongly preferred for code that writes.",
55
+ connectedFiles: connectedFiles.map((f) => ({
56
+ fileKey: f.fileKey,
57
+ fileName: f.fileName,
58
+ isActive: f.isActive,
59
+ })),
60
+ }),
61
+ }],
62
+ isError: true,
63
+ };
64
+ }
65
+ // A client is only ever registered under a real fileKey (see
66
+ // handleFileInfo), so this filter is belt-and-braces — but an
67
+ // entry without one must never fall through to sendCommand's
68
+ // active-file default, which would silently run the code against
69
+ // the active file instead and report it as a success.
70
+ const targetable = connectedFiles.filter((f) => Boolean(f.fileKey));
71
+ const unidentifiedFiles = connectedFiles
72
+ .filter((f) => !f.fileKey)
73
+ .map((f) => f.fileName);
74
+ let targets = targetable;
75
+ let missingFileKeys = [];
76
+ if (hasExplicitTargets) {
77
+ const connectedKeySet = new Set(targetable.map((f) => f.fileKey));
78
+ missingFileKeys = fileKeys.filter((k) => !connectedKeySet.has(k));
79
+ targets = targetable.filter((f) => fileKeys.includes(f.fileKey));
80
+ }
81
+ if (targets.length === 0) {
82
+ return {
83
+ content: [{
84
+ type: "text",
85
+ text: JSON.stringify({
86
+ error: "No matching connected files to target.",
87
+ requestedFileKeys: fileKeys,
88
+ connectedFileKeys: connectedFiles.map((f) => f.fileKey),
89
+ }),
90
+ }],
91
+ isError: true,
92
+ };
93
+ }
94
+ const connector = await getDesktopConnector();
95
+ const outcomes = await Promise.allSettled(targets.map((file) => connector.executeCodeViaUI(code, cappedTimeout, file.fileKey)));
96
+ const results = {};
97
+ let succeeded = 0;
98
+ let failed = 0;
99
+ outcomes.forEach((outcome, i) => {
100
+ const file = targets[i];
101
+ const key = file.fileKey;
102
+ if (outcome.status === "fulfilled") {
103
+ const result = outcome.value;
104
+ const ok = result?.success !== false;
105
+ if (ok)
106
+ succeeded++;
107
+ else
108
+ failed++;
109
+ results[key] = {
110
+ fileName: file.fileName,
111
+ success: result?.success,
112
+ result: result?.result,
113
+ error: result?.error,
114
+ // Reported by the plugin itself, so the caller can confirm
115
+ // each result came from the file it was addressed to.
116
+ fileContext: result?.fileContext,
117
+ };
118
+ }
119
+ else {
120
+ failed++;
121
+ results[key] = {
122
+ fileName: file.fileName,
123
+ success: false,
124
+ error: outcome.reason instanceof Error
125
+ ? outcome.reason.message
126
+ : String(outcome.reason),
127
+ };
128
+ }
129
+ });
130
+ return {
131
+ content: [{
132
+ type: "text",
133
+ text: JSON.stringify({
134
+ results,
135
+ totalTargeted: targets.length,
136
+ totalSucceeded: succeeded,
137
+ totalFailed: failed,
138
+ missingFileKeys: missingFileKeys.length > 0 ? missingFileKeys : undefined,
139
+ // Connected but unaddressable (no fileKey reported) — skipped
140
+ // rather than silently redirected to the active file.
141
+ skippedUnidentifiedFiles: unidentifiedFiles.length > 0 ? unidentifiedFiles : undefined,
142
+ timestamp: Date.now(),
143
+ }),
144
+ }],
145
+ isError: failed > 0 && succeeded === 0,
146
+ };
147
+ }
148
+ catch (error) {
149
+ return {
150
+ content: [{
151
+ type: "text",
152
+ text: JSON.stringify({
153
+ error: error instanceof Error ? error.message : String(error),
154
+ message: "Failed to execute across files",
155
+ }),
156
+ }],
157
+ isError: true,
158
+ };
159
+ }
160
+ });
161
+ }