@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/README.md CHANGED
@@ -8,16 +8,16 @@
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
- > **🆕 Accessibility auditing now matches WCAG, not folklore (v1.32.0):** `figma_lint_design` no longer red-flags line height below 1.5× as an accessibility failure. **WCAG 1.4.12 Text Spacing** only requires that content *support* a user overriding text spacing without loss not that designs *ship* at 1.5× so a sub-1.5 line height is not a conformance failure. Line and paragraph spacing checks are now scoped to multi-line text (single-line labels, buttons, and headings are exempt), the readability hints are split into an opt-in `best-practice` group so `rules: ['wcag']` stays pure conformance, and the real 1.4.12 check now lives on the code side in `figma_scan_code_accessibility`. **Re-import the plugin once** to pick up the new audit behavior. [See what's new →](CHANGELOG.md#1320---2026-06-20)
11
+ > **🆕 Bidirectional Token Sync v2 + DTCG 2025.10 (v1.34.0):** `figma_import_tokens` now applies the *whole* diff plan creates, renames, alias re-targeting, and replace-gated deletesfor a true code→Figma round-trip; exports speak DTCG 2025.10 on request (opt-in, legacy default byte-identical); scopes/codeSyntax round-trip; `figma_setup_design_tokens` accepts alias values; and the new `figma_create_component_set` builds a full variant set from an axes matrix in one call. **Plugin re-import required** (`code.js` + `ui.html` changed). [See what's new →](CHANGELOG.md#1340---2026-07-03)
12
12
 
13
13
  ## What is this?
14
14
 
15
15
  Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
16
16
 
17
17
  - **🎨 Design system extraction** - Pull variables, components, and styles
18
- - **🔁 Bidirectional token sync** - Export Figma variables to DTCG JSON + CSS custom properties; push code-side edits back to Figma. Replaces Style Dictionary and Tokens Studio's export pipeline.
18
+ - **🔁 Bidirectional token sync** - Export Figma variables to DTCG JSON (legacy or 2025.10 dialect) + 9 more formats; push code-side edits back to Figma with full apply — creates, renames, alias re-targeting, and replace-gated deletes. Replaces Style Dictionary and Tokens Studio's export pipeline.
19
19
  - **📸 Visual debugging** - Take screenshots for context
20
- - **✏️ Design creation** - Create UI components, frames, and layouts directly in Figma
20
+ - **✏️ Design creation** - Create UI components, frames, and layouts directly in Figma — including one-call component-set creation from a variant axes matrix
21
21
  - **🔧 Variable management** - Create, update, rename, and delete design tokens
22
22
  - **🕰 Version history & time-series awareness** - List versions, diff snapshots, generate markdown changelogs, trace property/variant introduction via binary-search blame
23
23
  - **⚡ Real-time monitoring** - Watch console logs from the Desktop Bridge plugin
@@ -55,9 +55,9 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
55
55
  | Real-time monitoring (console, selection) | ✅ | ❌ | ❌ |
56
56
  | Desktop Bridge plugin | ✅ | ✅ | ❌ |
57
57
  | Requires Node.js | Yes | **No** | No |
58
- | **Total tools available** | **106** | **95** | **9** |
58
+ | **Total tools available** | **107** | **96** | **9** |
59
59
 
60
- > **Bottom line:** Remote SSE is **read-only** with ~38% of the tools. **Cloud Mode** unlocks write access from web AI clients without Node.js. NPX/Local Git gives the full 106 tools with real-time monitoring.
60
+ > **Bottom line:** Remote SSE is **read-only** with 9 tools. **Cloud Mode** unlocks write access (96 tools) from web AI clients without Node.js. NPX/Local Git gives the full 107 tools with real-time monitoring.
61
61
 
62
62
  ---
63
63
 
@@ -65,7 +65,7 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
65
65
 
66
66
  **Best for:** Designers who want full AI-assisted design capabilities.
67
67
 
68
- **What you get:** All 106 tools including design creation, variable management, and component instantiation.
68
+ **What you get:** All 107 tools including design creation, variable management, and component instantiation.
69
69
 
70
70
  #### Prerequisites
71
71
 
@@ -162,7 +162,7 @@ Create a simple frame with a blue background
162
162
 
163
163
  **Best for:** Developers who want to modify source code or contribute to the project.
164
164
 
165
- **What you get:** Same 106 tools as NPX, plus full source code access.
165
+ **What you get:** Same 107 tools as NPX, plus full source code access.
166
166
 
167
167
  #### Quick Setup
168
168
 
@@ -251,7 +251,7 @@ Ready for design creation? Follow the [NPX Setup](#-npx-setup-recommended) guide
251
251
 
252
252
  **Best for:** Using Claude.ai, v0, Replit, or Lovable to create and modify Figma designs — no Node.js required.
253
253
 
254
- **What you get:** 95 tools including full write access — design creation, variable management, component instantiation, and all REST API tools. Only real-time monitoring (console logs, selection tracking, document changes) requires Local Mode.
254
+ **What you get:** 96 tools including full write access — design creation, variable management, component instantiation, and all REST API tools. Only real-time monitoring (console logs, selection tracking, document changes) requires Local Mode.
255
255
 
256
256
  #### Prerequisites
257
257
 
@@ -308,7 +308,7 @@ AI Client → Cloud MCP Server → Durable Object Relay → Desktop Bridge Plugi
308
308
  | Feature | NPX (Recommended) | Cloud Mode | Local Git | Remote SSE |
309
309
  |---------|-------------------|------------|-----------|------------|
310
310
  | **Setup time** | ~10 minutes | ~5 minutes | ~15 minutes | ~2 minutes |
311
- | **Total tools** | **106** | **95** | **106** | **9** (read-only) |
311
+ | **Total tools** | **107** | **96** | **107** | **9** (read-only) |
312
312
  | **Design creation** | ✅ | ✅ | ✅ | ❌ |
313
313
  | **Variable management** | ✅ | ✅ | ✅ | ❌ |
314
314
  | **Component instantiation** | ✅ | ✅ | ✅ | ❌ |
@@ -323,7 +323,7 @@ AI Client → Cloud MCP Server → Durable Object Relay → Desktop Bridge Plugi
323
323
  | **Automatic updates** | ✅ (`@latest`) | ✅ | Manual (`git pull`) | ✅ |
324
324
  | **Source code access** | ❌ | ❌ | ✅ | ❌ |
325
325
 
326
- > **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 106 tools.
326
+ > **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 107 tools.
327
327
 
328
328
  **📖 [Complete Feature Comparison](docs/mode-comparison.md)**
329
329
 
@@ -415,6 +415,10 @@ When you first use design system tools:
415
415
  - Create frames, shapes, text, components
416
416
  - Apply auto-layout, styles, effects
417
417
  - Build complete UI mockups programmatically
418
+ - `figma_create_component_set` - **Create a component set with variants in one declarative call**
419
+ - 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
420
+ - `Prop=Value` variant naming, `combineAsVariants` under the hood, optional auto-arranged labeled grid
421
+ - Returns each variant's key, ready for `figma_instantiate_component`
418
422
  - `figma_arrange_component_set` - **Organize variants into professional component sets**
419
423
  - Convert multiple component variants into a proper Figma component set
420
424
  - Applies native purple dashed border visualization automatically
@@ -432,8 +436,8 @@ When you first use design system tools:
432
436
  - `figma_generate_component_doc` - Generate platform-agnostic markdown documentation by merging Figma design data with code-side info
433
437
 
434
438
  ### 🔁 Token Sync (Local Mode + Cloud Mode)
435
- - `figma_export_tokens` - **Export Figma variables to design token files in your codebase.** Canonical DTCG JSON + CSS custom properties out of the box. Diff-aware merge against existing source files (only writes what changed). `tokens.config.json` autodiscovery means zero-arg calls after first setup. Replaces Style Dictionary and Tokens Studio's export pipeline for popular styling methods.
436
- - `figma_import_tokens` - **Push code-side token edits back to Figma.** Diff against current Figma state, apply only the deltas. Round-trip safe — Figma variable IDs preserved in DTCG `$extensions["figma-console-mcp"]` so renames on either side don't create duplicates. Dry-run default for safety. In Cloud Mode, pass tokens inline via `payload` or `files` (no local filesystem access).
439
+ - `figma_export_tokens` - **Export Figma variables to design token files in your codebase.** Canonical DTCG JSON (legacy hex dialect by default, or DTCG 2025.10 object colors/dimensions via `dtcgDialect: "2025"`) plus CSS, Tailwind v4/v3, SCSS, TS, JSON, Style Dictionary, and Tokens Studio formats. Diff-aware merge against existing source files (only writes what changed). `tokens.config.json` autodiscovery means zero-arg calls after first setup. Scopes and codeSyntax metadata round-trip via `$extensions`. Replaces Style Dictionary and Tokens Studio's export pipeline for popular styling methods.
440
+ - `figma_import_tokens` - **Push code-side token edits back to Figma with a full apply phase.** Diffs against current Figma state, then applies value updates, **creates** missing collections/variables, applies **renames**, writes real **alias** (`VARIABLE_ALIAS`) references, and — only under `strategy: "replace"` — deletes Figma-only variables. Round-trip safe — Figma variable IDs preserved in DTCG `$extensions["figma-console-mcp"]` so renames on either side don't create duplicates. Accepts both DTCG dialects. Dry-run strategy for safe previews. In Cloud Mode, pass tokens inline via `payload` or `files` (no local filesystem access).
437
441
 
438
442
  ### 🔧 Variable Management (Local Mode + Cloud Mode)
439
443
  - `figma_create_variable_collection` - Create new variable collections with modes
@@ -446,7 +450,7 @@ When you first use design system tools:
446
450
  - `figma_rename_mode` - Rename existing modes
447
451
  - `figma_batch_create_variables` - Create up to 100 variables in one call (10-50x faster)
448
452
  - `figma_batch_update_variables` - Update up to 100 variable values in one call
449
- - `figma_setup_design_tokens` - Create complete token system (collection + modes + variables) atomically
453
+ - `figma_setup_design_tokens` - Create complete token system (collection + modes + variables) atomically — values accept DTCG brace references (`"{color.blue.600}"`) that resolve to real variable aliases
450
454
 
451
455
  ### 📌 FigJam Board Tools (Local Mode + Cloud Mode)
452
456
  - `figjam_create_sticky` - Create a sticky note with color options
@@ -669,7 +673,7 @@ The **Figma Desktop Bridge** plugin is the recommended way to connect Figma to t
669
673
  - The MCP server communicates via **WebSocket** through the Desktop Bridge plugin
670
674
  - The server tries port 9223 first, then automatically falls back through ports 9224–9232 if needed
671
675
  - The plugin scans all ports in the range and connects to every active server it finds
672
- - All 106 tools work through the WebSocket transport
676
+ - All 107 tools work through the WebSocket transport
673
677
 
674
678
  **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).
675
679
 
@@ -808,9 +812,14 @@ The architecture supports adding new apps with minimal boilerplate — each app
808
812
 
809
813
  ## 🛤️ Roadmap
810
814
 
811
- **Current Status:** v1.32.0 (Stable) - Production-ready with 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, **106 tools** (Local) / **95 tools** (Cloud) / **9 tools** (Remote read-only), Comments API, cross-MCP identity disambiguation, and MCP Apps.
815
+ **Current Status:** v1.34.0 (Stable) - Production-ready. Latest: Bidirectional Token Sync v2 + DTCG 2025.10 — `figma_import_tokens` now 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 the new `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, **107 tools** (Local) / **96 tools** (Cloud) / **9 tools** (Remote read-only), Comments API, cross-MCP identity disambiguation, and MCP Apps.
812
816
 
813
817
  **Recent Releases:**
818
+ - [x] **v1.34.0** - Bidirectional Token Sync v2 + DTCG 2025.10. `figma_import_tokens` now applies the *complete* diff plan: missing collections and variables are created (with modes, inferred/recorded types, and values set in dependency order — aliases in a second pass), token-path renames route to the update phase by round-trip variable ID (no more create+delete pairs that would permanently destroy the original under `replace`), reference values write real `{ type: "VARIABLE_ALIAS", id }` payloads via a four-tier resolver, and deletes are strictly gated behind `strategy: "replace"`. `figma_export_tokens` gains `dtcgDialect: "2025"` (object-form colors from full-precision floats, object dimensions) while the legacy default stays byte-identical; import accepts both dialects unconditionally with dialect-insensitive diff normalization. Variable `scopes` + `codeSyntax` round-trip through `$extensions["figma-console-mcp"]`. `figma_setup_design_tokens` accepts DTCG brace references (`"{color.blue.600}"`) that resolve to real aliases, including forward references. New tool `figma_create_component_set` builds a variant set from an axes matrix (or combines existing components) with `Prop=Value` naming, optional auto-arranged grid, and variant keys in the response — with count-scaled timeouts and rollback on failure. **Plugin re-import required** (`code.js` + `ui.html` changed — the component-set handler and relay). 183 tests across the token/write-tools suites.
819
+ - [x] **v1.33.2** - Version-handshake false-positive fix. The v1.33.0 handshake compared the plugin's reported version against the server's *package* version, so server-only releases (like the v1.33.1 dependency sweep) flagged every up-to-date plugin as stale and pushed the re-import banner for files that hadn't changed. The server now compares against the `PLUGIN_VERSION` embedded in the `figma-desktop-bridge/code.js` it ships — exactly what a re-import would install — and `PLUGIN_VERSION` itself now means "last release in which plugin files changed" (release tooling bumps it only when `figma-desktop-bridge/` actually changed since the last tag). `figma_get_status` gains `transport.websocket.bundledPluginVersion`; `figma_diagnose` blames the right version. No new tools, **no plugin re-import required** (one-time exception: if you re-imported at v1.33.1, the banner appears once more — clear it with one final re-import). 1245 tests passing (9 new).
820
+ - [x] **v1.33.1** - Security dependency sweep. All runtime and critical npm audit alerts resolved via in-range bumps (`ws` 8.21.0, `hono` 4.12.27, `undici` 7.28.0, `handlebars` 4.7.9 — the lone critical, dev-only — plus `lodash`, `path-to-regexp`, `basic-ftp`, `fast-uri`, `vite`). `wrangler` deliberately held at 4.72.0 because newer versions require Node ≥22; the only residual audit findings are inside wrangler/miniflare's dev-time toolchain, which never ships in the npm package or Worker bundle. Supersedes dependabot PRs #81/#82/#84. No code changes, no API changes, no plugin re-import. 1236 tests passing unchanged.
821
+ - [x] **v1.33.0** - Connection UX overhaul + full-codebase audit. The plugin's status pill now derives from live connection state instead of Figma's variables loading (it used to glow green with zero MCP servers connected); HTTP `/health` auto-discovery reconnects restarted servers automatically (including one-dead-among-live, previously a permanent dead end); a version handshake banners the plugin UI when a re-import is needed and surfaces the mismatch in `figma_get_status`/`figma_diagnose`; cloud pairing config survives plugin reopen and its status line is derived + labeled (no more orphaned "Disconnected" under a green pill); all plugin copy is designer-language. The audit fixed 33 verified issues: lossless DTCG multi-mode round-trips, set-qualified cross-collection aliases, TIMING/EASING mapped to DTCG `duration`/`cubicBezier`, two cache-poisoning bugs (the "search returns 0 components" reports), a CSWSH origin bypass (`startsWith` → exact match), post-sleep reaper kill-safety (plus a shell-free `/health` probe with `os.devNull` so Windows curl can't false-negative a healthy sibling), branch-URL correctness across REST tools, and bridge-first `figma_take_screenshot`. `figma_arrange_component_set` now rearranges variants in place so placed instances survive. No new tools; **plugin re-import required** (`code.js` + `ui.html` changed — and the new handshake makes this the last one you have to discover on your own). 1236 tests passing (33 new).
822
+ - [x] **v1.32.1** - Documentation-generator fix reported by Robin Di Capua: `figma_generate_component_doc` documented **colors** as raw hex (with `—` in the Figma Variable column) even when fills/strokes were bound to variables, while spacing tokens documented correctly. Two root causes — an id→name lookup that read the wrong keys (`.id`/`.name` instead of `variableId`/`variableName`), and variable names only ever being sourced from the Enterprise-only REST `/variables/local` endpoint (403 elsewhere). The generator now resolves names via the Desktop Bridge Plugin API (works on every plan) and threads them through the States, Color Tokens, and Spacing tables, so real token names like `color/content/default` and `spacing/1` appear. No new tools, no arg-shape changes, no plugin re-import required. 1203 tests passing.
814
823
  - [x] **v1.32.0** - Accessibility-audit correctness fix reported by Isabella (a11y collaborator): `figma_lint_design` was flagging line height below 1.5× as an accessibility failure on hundreds of components. That misreads **WCAG 1.4.12 Text Spacing**, which requires content to *support* user spacing overrides without loss — not that designs *ship* at 1.5× — so a sub-1.5 line height is not a conformance failure. Line/paragraph-spacing checks are now scoped to multi-line text (single-line labels and buttons exempt); readability hints (`text-size`, `line-height`, `letter-spacing`, `paragraph-spacing`) are decoupled from the `wcag` group into an opt-in `best-practice` group, so the default audit (`['wcag','design-system','layout']`) and `rules: ['wcag']` return genuine conformance only; and a new code-side `text-spacing-support` advisory in `figma_scan_code_accessibility` flags fixed-px typography — where 1.4.12/1.4.4 are actually verifiable. No new tools, no arg-shape changes; **plugin re-import required** to pick up the new audit behavior (bridge protocol unchanged, so an un-updated plugin stays compatible). 1196 tests passing.
815
824
  - [x] **v1.31.0** - Fixes the most-reported reliability bug: the Desktop Bridge connection dropping and staying down until you closed the plugin, restarted your MCP client, or killed ports by hand. Root cause was **zombie MCP server processes** squatting the WebSocket port range (9223–9232) after a bad shutdown. The reaper now escalates `SIGTERM` → `SIGKILL` (a hung server that ignores graceful shutdown can no longer survive), sweeps the range every 5 minutes via an `unref`'d periodic reaper, and a shutdown backstop prevents a server from zombifying in the first place. The redesigned Desktop Bridge plugin adds an auto-reconnect watchdog (re-probes every ~12s while disconnected), a context-aware **Pause / Resume / Reconnect** button, and a live server-count badge. No new tools; **plugin re-import required** (bridge `ui.html` + `code.js` changed). 1190 tests passing, including an integration test that spawns a real `SIGTERM`-ignoring process and asserts the reaper kills it.
816
825
  - [x] **v1.30.0** - Native variable binding + typography in the structured write tools, closing the Plugin API gaps that used to force raw `figma_execute`. `figma_set_fills` / `figma_set_strokes` accept a `variableId` to bind a fill/stroke to a color variable via `setBoundVariableForPaint` (any plan, via the bridge). `figma_set_text` gains `fontFamily` / `fontStyle` with space-insensitive normalization (`SemiBold` → `Semi Bold`) and graceful `Regular` fallback. `figma_instantiate_component` pre-loads instance text fonts before applying overrides (fixes silently-skipped text overrides on non-Regular weights) and returns a `warnings` array for failed overrides. Also fixes a mixed-font crash in `figma_set_text` and a `ui.html` relay that was dropping new message fields. No new tools; **plugin re-import required** (bridge `ui.html` + `code.js` changed). Validated live; 1185 tests passing.
@@ -835,10 +844,10 @@ The architecture supports adding new apps with minimal boilerplate — each app
835
844
  - [x] **v1.7.0** - MCP Apps (Token Browser, Design System Dashboard), batch variable operations, design-code parity tools.
836
845
 
837
846
  **Coming Next:**
838
- - [ ] **Token sync — parsers + import-side apply expansion** - Parsers for non-DTCG input (Tokens Studio, CSS vars, Tailwind v4, Tailwind v3 config, SCSS, Style Dictionary v3, JSON flat/nested). Plus `toCreate` apply orchestration, `toDelete` for `replace` strategy, alias-target updates, and cross-library variable resolution via `getVariableByIdAsync` so cross-library aliases render as real `var(--target)` references instead of comments.
847
+ - [ ] **Token sync — non-DTCG input parsers** - Parsers for non-DTCG input (Tokens Studio, CSS vars, Tailwind v4, Tailwind v3 config, SCSS, Style Dictionary v3, JSON flat/nested) so `figma_import_tokens` can ingest the same formats it exports. (The import-side apply expansion creates, replace-gated deletes, alias-target updates shipped in v1.34.0.)
848
+ - [ ] **Cross-library variable resolution** - Resolve cross-library aliases via `getVariableByIdAsync` so they render as real `var(--target)` references in exports instead of comments.
839
849
  - [ ] **Component template library** - Common UI pattern generation
840
850
  - [ ] **Visual regression testing** - Screenshot diff capabilities
841
- - [ ] **Design linting** - Automated compliance and accessibility checks
842
851
 
843
852
  **📖 [Full Roadmap](docs/ROADMAP.md)**
844
853
 
@@ -180,6 +180,24 @@ export class CloudWebSocketConnector {
180
180
  }
181
181
  return this.sendCommand('INSTANTIATE_COMPONENT', params);
182
182
  }
183
+ async createComponentSet(params) {
184
+ // Timeout scales with variant count — the plugin builds all variants in
185
+ // one uncancellable pass, so a fixed 30s ceiling on big matrices reports
186
+ // failure while the set still gets created (retry → duplicates).
187
+ // Mirrors componentSetTimeoutMs in websocket-connector.ts (not imported:
188
+ // that module pulls the Node 'ws' stack into the Workers bundle).
189
+ let variantCount = 1;
190
+ if (params.componentIds?.length) {
191
+ variantCount = params.componentIds.length;
192
+ }
193
+ else if (params.properties) {
194
+ for (const values of Object.values(params.properties)) {
195
+ variantCount *= Math.max(1, values?.length ?? 1);
196
+ }
197
+ }
198
+ const timeout = Math.min(120000, Math.max(30000, variantCount * 1200)) + 5000;
199
+ return this.sendCommand('CREATE_COMPONENT_SET', params, timeout);
200
+ }
183
201
  // ============================================================================
184
202
  // Node manipulation
185
203
  // ============================================================================
@@ -477,7 +477,7 @@ function buildAnatomyLines(node, lines, prefix, isLast, depth, maxDepth) {
477
477
  /**
478
478
  * Collect spacing tokens with their bound variable names.
479
479
  */
480
- function collectSpacingTokens(node) {
480
+ function collectSpacingTokens(node, varNameMap = new Map()) {
481
481
  const tokens = [];
482
482
  const boundVars = node.boundVariables || {};
483
483
  const spacingProps = [
@@ -493,7 +493,11 @@ function collectSpacingTokens(node) {
493
493
  const value = node[key];
494
494
  if (value !== undefined && value !== null) {
495
495
  const varBinding = boundVars[key];
496
- const varName = varBinding?.id || varBinding?.name;
496
+ // Bound spacing variables expose their id as boundVariables[key].id.
497
+ // Resolve it to a friendly token name (e.g. `spacing/1`) when the caller
498
+ // supplied a name map; fall back to the raw id so the binding stays visible.
499
+ const varId = typeof varBinding?.id === "string" ? varBinding.id : undefined;
500
+ const varName = varId ? varNameMap.get(varId) || varId : undefined;
497
501
  tokens.push({
498
502
  property: label,
499
503
  value,
@@ -843,20 +847,26 @@ function compareTokens(enrichedData, codeSpec, discrepancies) {
843
847
  });
844
848
  }
845
849
  }
846
- // Cross-reference design variables with code tokens
850
+ // Cross-reference design variables with code tokens.
851
+ // enrichment entries key off variableName (NOT name); reading `.name` here left
852
+ // every comparison undefined (and would throw on .toLowerCase()). Use variableName
853
+ // and skip any entries that never resolved to a real token name.
847
854
  if (enrichedData.variables_used && ct.usedTokens) {
848
- const designTokenNames = enrichedData.variables_used.map((v) => v.name.toLowerCase());
855
+ const designTokens = enrichedData.variables_used
856
+ .map((v) => v.variableName)
857
+ .filter((n) => typeof n === "string" && n.length > 0);
858
+ const designTokenNames = designTokens.map((n) => n.toLowerCase());
849
859
  const codeTokenNames = ct.usedTokens.map((t) => t.toLowerCase());
850
- for (const designToken of enrichedData.variables_used) {
851
- const normalizedName = designToken.name.toLowerCase();
860
+ for (const tokenName of designTokens) {
861
+ const normalizedName = tokenName.toLowerCase();
852
862
  if (!codeTokenNames.some((ct) => ct.includes(normalizedName) || normalizedName.includes(ct))) {
853
863
  discrepancies.push({
854
864
  category: "tokens",
855
- property: `token:${designToken.name}`,
865
+ property: `token:${tokenName}`,
856
866
  severity: "minor",
857
- designValue: designToken.name,
867
+ designValue: tokenName,
858
868
  codeValue: null,
859
- message: `Design uses token "${designToken.name}" but code doesn't reference it`,
869
+ message: `Design uses token "${tokenName}" but code doesn't reference it`,
860
870
  suggestion: `Add token reference in code`,
861
871
  });
862
872
  }
@@ -1680,13 +1690,16 @@ function deduplicateColors(colors) {
1680
1690
  }
1681
1691
  return Array.from(seen.values());
1682
1692
  }
1683
- function generateVisualSpecsSection(node, enrichedData, variantData) {
1693
+ function generateVisualSpecsSection(node, enrichedData, variantData, varNameMap = new Map()) {
1684
1694
  const lines = ["", "## Token Specification", ""];
1685
- // Build variable name lookup from enrichment data
1686
- const varNameMap = new Map();
1695
+ // Fill any gaps in the caller-supplied name map from enrichment data.
1696
+ // enrichment entries key off variableId/variableName (NOT id/name), and only
1697
+ // carry a useful name when it actually resolved (not the raw VariableID).
1687
1698
  if (enrichedData?.variables_used) {
1688
1699
  for (const v of enrichedData.variables_used) {
1689
- varNameMap.set(v.id, v.name);
1700
+ if (v.variableId && v.variableName && v.variableName !== v.variableId && !varNameMap.has(v.variableId)) {
1701
+ varNameMap.set(v.variableId, v.variableName);
1702
+ }
1690
1703
  }
1691
1704
  }
1692
1705
  // Per-variant color token table
@@ -1740,7 +1753,7 @@ function generateVisualSpecsSection(node, enrichedData, variantData) {
1740
1753
  }
1741
1754
  // Spacing tokens with variable names
1742
1755
  const visualNode = resolveVisualNode(node);
1743
- const spacingTokens = collectSpacingTokens(visualNode);
1756
+ const spacingTokens = collectSpacingTokens(visualNode, varNameMap);
1744
1757
  if (spacingTokens.length > 0) {
1745
1758
  lines.push("### Spacing Tokens");
1746
1759
  lines.push("");
@@ -2565,11 +2578,41 @@ export function registerDesignCodeTools(server, getFigmaAPI, getCurrentUrl, vari
2565
2578
  logger.warn("Enrichment failed, proceeding without token data");
2566
2579
  }
2567
2580
  }
2568
- // Build variable name lookup for per-variant color collection
2581
+ // Build variable name lookup (id → token name) for per-variant color
2582
+ // AND spacing collection. Two sources, in order of authority:
2583
+ // 1. Desktop Bridge local variables (Plugin API getLocalVariablesAsync) —
2584
+ // works on EVERY Figma plan and is the only reliable id→name source.
2585
+ // The REST /files/:key/variables/local endpoint is Enterprise-only
2586
+ // (returns 403 on all other plans), so enrichment's variable map is
2587
+ // almost always empty and bound colors would otherwise render as raw
2588
+ // hex / raw VariableIDs.
2589
+ // 2. Enrichment variables_used — fallback for non-bridge (Cloud/Remote)
2590
+ // paths. NOTE: entries key off variableId/variableName (NOT id/name),
2591
+ // and their name is only useful when it actually resolved to a token
2592
+ // name (not the raw VariableID).
2569
2593
  const varNameMap = new Map();
2570
2594
  if (enrichedData?.variables_used) {
2571
2595
  for (const v of enrichedData.variables_used) {
2572
- varNameMap.set(v.id, v.name);
2596
+ if (v.variableId && v.variableName && v.variableName !== v.variableId) {
2597
+ varNameMap.set(v.variableId, v.variableName);
2598
+ }
2599
+ }
2600
+ }
2601
+ if (getDesktopConnector) {
2602
+ try {
2603
+ const connector = await getDesktopConnector();
2604
+ const varsResult = await connector.getVariables();
2605
+ const varList = varsResult?.variables || varsResult?.result?.variables;
2606
+ if (Array.isArray(varList)) {
2607
+ for (const v of varList) {
2608
+ if (v?.id && v?.name)
2609
+ varNameMap.set(v.id, v.name);
2610
+ }
2611
+ logger.info({ count: varList.length }, "Resolved variable names via Desktop Bridge for docs");
2612
+ }
2613
+ }
2614
+ catch {
2615
+ logger.warn("Could not load bridge variables for doc token names — colors may fall back to hex");
2573
2616
  }
2574
2617
  }
2575
2618
  // Collect per-variant color/icon data
@@ -2657,7 +2700,7 @@ export function registerDesignCodeTools(server, getFigmaAPI, getCurrentUrl, vari
2657
2700
  }
2658
2701
  }
2659
2702
  if (s.visualSpecs) {
2660
- parts.push(generateVisualSpecsSection(nodeForVisual, enrichedData, variantData));
2703
+ parts.push(generateVisualSpecsSection(nodeForVisual, enrichedData, variantData, varNameMap));
2661
2704
  includedSections.push("visualSpecs");
2662
2705
  }
2663
2706
  if (s.typography) {
@@ -68,12 +68,13 @@ export function searchComponents(manifest, query, options) {
68
68
  const queryLower = query.toLowerCase();
69
69
  const categoryLower = options?.category?.toLowerCase();
70
70
  const allResults = [];
71
- // Search component sets first (they're typically the main design system components)
72
- for (const [name, compSet] of Object.entries(manifest.componentSets)) {
73
- const nameLower = name.toLowerCase();
71
+ // Search component sets first (they're typically the main design system components).
72
+ // Manifest maps are keyed by component key — always read names from the entry.
73
+ for (const compSet of Object.values(manifest.componentSets)) {
74
+ const nameLower = compSet.name.toLowerCase();
74
75
  const descLower = compSet.description?.toLowerCase() || '';
75
76
  const matchesQuery = !query || nameLower.includes(queryLower) || descLower.includes(queryLower);
76
- const matchesCategory = !categoryLower || inferCategory(name).toLowerCase().includes(categoryLower);
77
+ const matchesCategory = !categoryLower || inferCategory(compSet.name).toLowerCase().includes(categoryLower);
77
78
  if (matchesQuery && matchesCategory) {
78
79
  allResults.push({
79
80
  name: compSet.name,
@@ -81,17 +82,17 @@ export function searchComponents(manifest, query, options) {
81
82
  nodeId: compSet.nodeId,
82
83
  type: 'componentSet',
83
84
  description: compSet.description,
84
- category: inferCategory(name),
85
+ category: inferCategory(compSet.name),
85
86
  variantCount: compSet.variants?.length || 0,
86
87
  });
87
88
  }
88
89
  }
89
90
  // Then search standalone components
90
- for (const [name, comp] of Object.entries(manifest.components)) {
91
- const nameLower = name.toLowerCase();
91
+ for (const comp of Object.values(manifest.components)) {
92
+ const nameLower = comp.name.toLowerCase();
92
93
  const descLower = comp.description?.toLowerCase() || '';
93
94
  const matchesQuery = !query || nameLower.includes(queryLower) || descLower.includes(queryLower);
94
- const matchesCategory = !categoryLower || inferCategory(name).toLowerCase().includes(categoryLower);
95
+ const matchesCategory = !categoryLower || inferCategory(comp.name).toLowerCase().includes(categoryLower);
95
96
  if (matchesQuery && matchesCategory) {
96
97
  allResults.push({
97
98
  name: comp.name,
@@ -99,13 +100,17 @@ export function searchComponents(manifest, query, options) {
99
100
  nodeId: comp.nodeId,
100
101
  type: 'component',
101
102
  description: comp.description,
102
- category: inferCategory(name),
103
+ category: inferCategory(comp.name),
103
104
  defaultSize: comp.defaultSize,
104
105
  });
105
106
  }
106
107
  }
107
108
  const total = allResults.length;
108
- const paginatedResults = allResults.slice(offset, offset + limit);
109
+ // Truncate long doc blocks in search hits — full text is available via
110
+ // figma_get_component_details.
111
+ const paginatedResults = allResults.slice(offset, offset + limit).map((r) => r.description && r.description.length > 200
112
+ ? { ...r, description: `${r.description.slice(0, 200)}…`, descriptionTruncated: true }
113
+ : r);
109
114
  const hasMore = offset + limit < total;
110
115
  return { results: paginatedResults, total, hasMore };
111
116
  }
@@ -121,14 +126,14 @@ function inferCategory(name) {
121
126
  */
122
127
  export function getCategories(manifest) {
123
128
  const categories = new Map();
124
- for (const name of Object.keys(manifest.componentSets)) {
125
- const cat = inferCategory(name);
129
+ for (const compSet of Object.values(manifest.componentSets)) {
130
+ const cat = inferCategory(compSet.name);
126
131
  const existing = categories.get(cat) || { componentCount: 0, componentSetCount: 0 };
127
132
  existing.componentSetCount++;
128
133
  categories.set(cat, existing);
129
134
  }
130
- for (const name of Object.keys(manifest.components)) {
131
- const cat = inferCategory(name);
135
+ for (const comp of Object.values(manifest.components)) {
136
+ const cat = inferCategory(comp.name);
132
137
  const existing = categories.get(cat) || { componentCount: 0, componentSetCount: 0 };
133
138
  existing.componentCount++;
134
139
  categories.set(cat, existing);
@@ -155,35 +155,36 @@ export function extractVisualSpec(node) {
155
155
  return hasData ? spec : undefined;
156
156
  }
157
157
  /**
158
- * Extract visual specs from a component node and its first-level children.
159
- * Returns a compact representation of the component's visual appearance.
158
+ * Delta-encode variant visual specs against the first variant that has one.
159
+ * Variants in a set share most visual properties, so repeating the full spec
160
+ * on every variant inflates the payload ~30x for large sets. The base variant
161
+ * keeps its full visualSpec; every other variant gets a visualSpecDelta with
162
+ * only the top-level properties that differ from the base (null means the
163
+ * property exists on the base but not on this variant). Variants identical to
164
+ * the base carry neither field.
160
165
  */
161
- function extractComponentVisualData(node) {
162
- if (!node)
163
- return {};
164
- const result = {};
165
- const rootSpec = extractVisualSpec(node);
166
- if (rootSpec)
167
- result.visualSpec = rootSpec;
168
- // Extract first-level children specs (the structural elements)
169
- if (node.children && Array.isArray(node.children)) {
170
- const childSpecs = [];
171
- for (const child of node.children) {
172
- const childInfo = {
173
- name: child.name,
174
- type: child.type,
175
- };
176
- const childVisual = extractVisualSpec(child);
177
- if (childVisual)
178
- childInfo.visualSpec = childVisual;
179
- if (child.characters)
180
- childInfo.characters = child.characters;
181
- childSpecs.push(childInfo);
166
+ function deltaEncodeVariantSpecs(variants) {
167
+ const base = variants.find((v) => v.visualSpec);
168
+ if (!base)
169
+ return;
170
+ const baseSpec = base.visualSpec;
171
+ for (const variant of variants) {
172
+ if (variant === base || !variant.visualSpec)
173
+ continue;
174
+ const spec = variant.visualSpec;
175
+ const delta = {};
176
+ for (const key of new Set([...Object.keys(baseSpec), ...Object.keys(spec)])) {
177
+ if (!(key in spec)) {
178
+ delta[key] = null;
179
+ }
180
+ else if (!(key in baseSpec) || JSON.stringify(spec[key]) !== JSON.stringify(baseSpec[key])) {
181
+ delta[key] = spec[key];
182
+ }
182
183
  }
183
- if (childSpecs.length > 0)
184
- result.childSpecs = childSpecs;
184
+ delete variant.visualSpec;
185
+ if (Object.keys(delta).length > 0)
186
+ variant.visualSpecDelta = delta;
185
187
  }
186
- return result;
187
188
  }
188
189
  /**
189
190
  * Resolve style node IDs to their actual visual values.
@@ -428,6 +429,7 @@ export function registerDesignSystemTools(server, getFigmaAPI, getCurrentUrl, va
428
429
  "token values per mode (light/dark), and resolved style values. " +
429
430
  "Use this instead of calling individual tools to avoid context window overflow. " +
430
431
  "Ideal for AI code generation — use visualSpec for pixel-accurate reproduction. " +
432
+ "Variant specs are delta-encoded: the base variant carries the full visualSpec, siblings carry visualSpecDelta with only the properties that differ. " +
431
433
  "Tokens/variables are read through the connected Desktop Bridge or cloud relay and work on ANY Figma plan — no Enterprise required. " +
432
434
  "If a tokens fetch ever reports the Variables REST API is plan-limited (403), the bridge/relay is the plan-independent path: ensure it's connected and retry rather than abandoning variables.", {
433
435
  fileKey: z
@@ -600,6 +602,9 @@ export function registerDesignSystemTools(server, getFigmaAPI, getCurrentUrl, va
600
602
  return entry;
601
603
  });
602
604
  if (variants.length > 0) {
605
+ // Variants share most visual properties — keep the full spec on
606
+ // the base variant only and encode the rest as deltas
607
+ deltaEncodeVariantSpecs(variants);
603
608
  spec.variants = variants;
604
609
  }
605
610
  if (setNode?.componentPropertyDefinitions) {
@@ -611,11 +616,11 @@ export function registerDesignSystemTools(server, getFigmaAPI, getCurrentUrl, va
611
616
  height: setNode.absoluteBoundingBox.height,
612
617
  };
613
618
  }
614
- // Extract visual spec from the set node (root + children)
619
+ // Extract visual spec from the set node itself
615
620
  if (setNode) {
616
- const visualData = extractComponentVisualData(setNode);
617
- if (visualData.visualSpec) {
618
- spec.visualSpec = visualData.visualSpec;
621
+ const setSpec = extractVisualSpec(setNode);
622
+ if (setSpec) {
623
+ spec.visualSpec = setSpec;
619
624
  }
620
625
  }
621
626
  componentSpecs.push(spec);
@@ -638,11 +643,11 @@ export function registerDesignSystemTools(server, getFigmaAPI, getCurrentUrl, va
638
643
  height: node.absoluteBoundingBox.height,
639
644
  };
640
645
  }
641
- // Extract visual spec from the component node (root + children)
646
+ // Extract visual spec from the component node
642
647
  if (node) {
643
- const visualData = extractComponentVisualData(node);
644
- if (visualData.visualSpec) {
645
- spec.visualSpec = visualData.visualSpec;
648
+ const nodeSpec = extractVisualSpec(node);
649
+ if (nodeSpec) {
650
+ spec.visualSpec = nodeSpec;
646
651
  }
647
652
  }
648
653
  componentSpecs.push(spec);
@@ -762,6 +767,10 @@ export function registerDesignSystemTools(server, getFigmaAPI, getCurrentUrl, va
762
767
  " - layout.itemSpacing → gap\n" +
763
768
  " - layout.primaryAxisAlign → justify-content, counterAxisAlign → align-items\n" +
764
769
  " - typography → font-family, font-size, font-weight, line-height, letter-spacing\n" +
770
+ " - Variant specs are delta-encoded: one base variant carries the full visualSpec; " +
771
+ "sibling variants carry 'visualSpecDelta' with ONLY the properties that differ from the base " +
772
+ "(null = property absent on this variant). Merge base visualSpec + visualSpecDelta to get a " +
773
+ "variant's full spec. A variant with neither field is visually identical to the base.\n" +
765
774
  "3. Do NOT add decorative elements (colored borders, accents, dividers, gradients) " +
766
775
  "unless they appear in the visualSpec data.\n" +
767
776
  "4. Use 'imageUrl' screenshots as the visual ground truth. If the screenshot " +
@@ -34,6 +34,10 @@ function buildReport(opts) {
34
34
  if (plugin.editorType && plugin.editorType !== "figma") {
35
35
  lines.push(`- Editor type: ${plugin.editorType}.`);
36
36
  }
37
+ if (plugin.pluginUpdateAvailable) {
38
+ const bundledVersion = opts.getBundledPluginVersion?.() ?? opts.getServerVersion();
39
+ lines.push(`- ⚠️ **Plugin update available**: the imported plugin${plugin.pluginVersion ? ` (v${plugin.pluginVersion})` : " (version unknown — very old)"} differs from the plugin files this server ships (v${bundledVersion}). Figma caches plugin files, so re-import it: Figma Desktop → Plugins → Development → Import plugin from manifest → select manifest.json. Until then, recently added or fixed plugin features may silently misbehave.`);
40
+ }
37
41
  }
38
42
  else {
39
43
  lines.push(`- ⚠️ Desktop Bridge plugin not connected${plugin.port ? ` (server is listening on port ${plugin.port})` : ""}.`);
@@ -83,17 +83,23 @@ export class EnrichmentService {
83
83
  const enriched = {
84
84
  ...variable,
85
85
  };
86
- // Resolve values for all modes
86
+ // Resolve values for all modes — each mode must resolve ITS OWN
87
+ // value, so the modeId is threaded through the resolver (which
88
+ // also keys its cache per mode).
87
89
  if (options.include_exports !== false) {
88
90
  const resolved_values = {};
89
- for (const [modeId, value] of Object.entries(variable.valuesByMode || {})) {
90
- const resolvedValue = await this.styleResolver.resolveVariableValue(variable, variablesMap, options.max_depth);
91
+ for (const modeId of Object.keys(variable.valuesByMode || {})) {
92
+ const resolvedValue = await this.styleResolver.resolveVariableValue(variable, variablesMap, options.max_depth, 0, modeId);
91
93
  resolved_values[modeId] = resolvedValue;
92
94
  }
93
95
  enriched.resolved_values = resolved_values;
94
- // Generate export formats using first mode
96
+ // Generate export formats using first mode. Guard only against
97
+ // null/undefined — legitimate falsy values (0, false, "") must
98
+ // still produce export formats.
95
99
  const firstModeValue = Object.values(resolved_values)[0];
96
- if (firstModeValue && options.export_formats) {
100
+ if (firstModeValue !== null &&
101
+ firstModeValue !== undefined &&
102
+ options.export_formats) {
97
103
  enriched.export_formats = this.styleResolver.generateExportFormats(variable.name, firstModeValue, variable.resolvedType, options.export_formats);
98
104
  }
99
105
  }