figma-console-mcp 1.33.2 β†’ 1.34.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 (67) hide show
  1. package/README.md +22 -16
  2. package/dist/cloudflare/core/cloud-websocket-connector.js +18 -0
  3. package/dist/cloudflare/core/tokens/alias-resolver.js +15 -2
  4. package/dist/cloudflare/core/tokens/config.js +10 -0
  5. package/dist/cloudflare/core/tokens/dialect.js +232 -0
  6. package/dist/cloudflare/core/tokens/figma-converter.js +37 -3
  7. package/dist/cloudflare/core/tokens/formatters/dtcg.js +35 -14
  8. package/dist/cloudflare/core/tokens/formatters/json.js +28 -10
  9. package/dist/cloudflare/core/tokens/index.js +2 -1
  10. package/dist/cloudflare/core/tokens/schemas.js +4 -0
  11. package/dist/cloudflare/core/tokens-tools.js +942 -81
  12. package/dist/cloudflare/core/websocket-connector.js +42 -0
  13. package/dist/cloudflare/core/write-tools.js +308 -49
  14. package/dist/cloudflare/index.js +7 -7
  15. package/dist/core/figma-connector.d.ts +12 -0
  16. package/dist/core/figma-connector.d.ts.map +1 -1
  17. package/dist/core/tokens/alias-resolver.d.ts +16 -0
  18. package/dist/core/tokens/alias-resolver.d.ts.map +1 -1
  19. package/dist/core/tokens/alias-resolver.js +15 -2
  20. package/dist/core/tokens/alias-resolver.js.map +1 -1
  21. package/dist/core/tokens/config.d.ts +28 -0
  22. package/dist/core/tokens/config.d.ts.map +1 -1
  23. package/dist/core/tokens/config.js +10 -0
  24. package/dist/core/tokens/config.js.map +1 -1
  25. package/dist/core/tokens/dialect.d.ts +107 -0
  26. package/dist/core/tokens/dialect.d.ts.map +1 -0
  27. package/dist/core/tokens/dialect.js +233 -0
  28. package/dist/core/tokens/dialect.js.map +1 -0
  29. package/dist/core/tokens/figma-converter.d.ts +2 -0
  30. package/dist/core/tokens/figma-converter.d.ts.map +1 -1
  31. package/dist/core/tokens/figma-converter.js +37 -3
  32. package/dist/core/tokens/figma-converter.js.map +1 -1
  33. package/dist/core/tokens/formatters/dtcg.d.ts +2 -2
  34. package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -1
  35. package/dist/core/tokens/formatters/dtcg.js +35 -14
  36. package/dist/core/tokens/formatters/dtcg.js.map +1 -1
  37. package/dist/core/tokens/formatters/json.d.ts.map +1 -1
  38. package/dist/core/tokens/formatters/json.js +28 -10
  39. package/dist/core/tokens/formatters/json.js.map +1 -1
  40. package/dist/core/tokens/index.d.ts +2 -1
  41. package/dist/core/tokens/index.d.ts.map +1 -1
  42. package/dist/core/tokens/index.js +2 -1
  43. package/dist/core/tokens/index.js.map +1 -1
  44. package/dist/core/tokens/schemas.d.ts +3 -0
  45. package/dist/core/tokens/schemas.d.ts.map +1 -1
  46. package/dist/core/tokens/schemas.js +4 -0
  47. package/dist/core/tokens/schemas.js.map +1 -1
  48. package/dist/core/tokens/types.d.ts +29 -0
  49. package/dist/core/tokens/types.d.ts.map +1 -1
  50. package/dist/core/tokens/types.js.map +1 -1
  51. package/dist/core/tokens-tools.d.ts +211 -12
  52. package/dist/core/tokens-tools.d.ts.map +1 -1
  53. package/dist/core/tokens-tools.js +942 -81
  54. package/dist/core/tokens-tools.js.map +1 -1
  55. package/dist/core/websocket-connector.d.ts +38 -0
  56. package/dist/core/websocket-connector.d.ts.map +1 -1
  57. package/dist/core/websocket-connector.js +42 -0
  58. package/dist/core/websocket-connector.js.map +1 -1
  59. package/dist/core/write-tools.d.ts.map +1 -1
  60. package/dist/core/write-tools.js +308 -49
  61. package/dist/core/write-tools.js.map +1 -1
  62. package/dist/local.d.ts.map +1 -1
  63. package/dist/local.js +5 -4
  64. package/dist/local.js.map +1 -1
  65. package/figma-desktop-bridge/code.js +288 -1
  66. package/figma-desktop-bridge/ui.html +27 -0
  67. package/package.json +1 -1
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
- > **πŸ†• Self-healing connection + 33-fix audit (v1.33.0, patched v1.33.1 β€” security dependency sweep):** The Desktop Bridge now tells the truth and heals itself β€” the status pill derives from live connection state (it used to glow green with zero MCP servers connected), `/health` auto-discovery reconnects restarted servers without the manual ritual, and a version handshake banners the plugin when it needs a re-import. Underneath: 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. **Re-import the plugin manifest one more time** β€” the new handshake makes it the last one you have to discover on your own. [See what's new β†’](CHANGELOG.md#1330---2026-07-02)
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 deletes β€” for 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,10 @@ The architecture supports adding new apps with minimal boilerplate β€” each app
808
812
 
809
813
  ## πŸ›€οΈ Roadmap
810
814
 
811
- **Current Status:** v1.33.2 (Stable) - Production-ready. Latest: version-handshake fix β€” the plugin's "update available" banner now compares against the plugin files the server actually ships, so server-only releases no longer nag you to re-import an already-current plugin. On top of the v1.33.1 security dependency sweep (all runtime + critical alerts resolved via in-range bumps; zero API changes) and the v1.33.0 connection UX overhaul (honest status pill derived from live connection state, `/health` auto-discovery with self-healing reconnect, version handshake with in-plugin re-import banner) + 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, **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.
814
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).
815
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.
816
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).
@@ -839,7 +844,8 @@ The architecture supports adding new apps with minimal boilerplate β€” each app
839
844
  - [x] **v1.7.0** - MCP Apps (Token Browser, Design System Dashboard), batch variable operations, design-code parity tools.
840
845
 
841
846
  **Coming Next:**
842
- - [ ] **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.
843
849
  - [ ] **Component template library** - Common UI pattern generation
844
850
  - [ ] **Visual regression testing** - Screenshot diff capabilities
845
851
 
@@ -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
  // ============================================================================
@@ -37,6 +37,19 @@ export function slugifySetName(name) {
37
37
  * a `warnings` array get one warning per ambiguous bare path.
38
38
  */
39
39
  export function buildTokenIndex(doc, warnings) {
40
+ const index = new Map();
41
+ for (const [key, entry] of buildTokenLookup(doc, warnings)) {
42
+ index.set(key, entry.token);
43
+ }
44
+ return index;
45
+ }
46
+ /**
47
+ * Set-aware variant of buildTokenIndex β€” identical key scheme and ambiguity
48
+ * rules (set-qualified keys always; bare-path fallback only when unambiguous
49
+ * across sets), but each entry carries the owning set's name alongside the
50
+ * token. buildTokenIndex delegates to this so the two can never drift.
51
+ */
52
+ export function buildTokenLookup(doc, warnings) {
40
53
  const index = new Map();
41
54
  // barePath β†’ owning entries, used to detect cross-set ambiguity.
42
55
  const bareOwners = new Map();
@@ -44,7 +57,7 @@ export function buildTokenIndex(doc, warnings) {
44
57
  const setKey = slugifySetName(set.name);
45
58
  for (const token of set.tokens) {
46
59
  const bare = token.path.join(".");
47
- index.set(`${setKey}.${bare}`, token);
60
+ index.set(`${setKey}.${bare}`, { setName: set.name, token });
48
61
  const owners = bareOwners.get(bare) ?? [];
49
62
  owners.push({ setName: set.name, token });
50
63
  bareOwners.set(bare, owners);
@@ -55,7 +68,7 @@ export function buildTokenIndex(doc, warnings) {
55
68
  for (const [bare, owners] of bareOwners) {
56
69
  if (owners.length === 1) {
57
70
  if (!index.has(bare))
58
- index.set(bare, owners[0].token);
71
+ index.set(bare, owners[0]);
59
72
  }
60
73
  else if (warnings) {
61
74
  warnings.push(`Token path "${bare}" exists in multiple collections (${owners
@@ -42,6 +42,16 @@ const OutputTargetSchema = z.object({
42
42
  * for CSS/SCSS/Tailwind/etc. since they can't natively express aliases).
43
43
  */
44
44
  resolveAliases: z.boolean().optional(),
45
+ /**
46
+ * DTCG dialect for dtcg/json outputs. 'legacy' (default): hex-string
47
+ * colors and bare-number dimensions, maximum compatibility (Style
48
+ * Dictionary v4, Tokens Studio). '2025': DTCG 2025.10 object colors
49
+ * ({ colorSpace, components, alpha?, hex }) and dimensions
50
+ * ({ value, unit }) for Style Dictionary v5+ and other 2025.10-aware
51
+ * tooling. Ignored by css/scss/tailwind/ts formatters, which render
52
+ * final code.
53
+ */
54
+ dtcgDialect: z.enum(["legacy", "2025"]).optional(),
45
55
  /** Per-target transform options. Override the global defaults. */
46
56
  transforms: z
47
57
  .object({
@@ -0,0 +1,232 @@
1
+ /**
2
+ * DTCG dialect helpers β€” legacy vs 2025.10 value encodings.
3
+ *
4
+ * The token pipeline historically speaks the "legacy" DTCG dialect: colors
5
+ * as hex strings ("#4085F2"), FLOAT dimensions as bare numbers. The DTCG
6
+ * 2025.10 spec (https://tr.designtokens.org/format/) switched to object
7
+ * forms:
8
+ *
9
+ * color = { colorSpace: "srgb", components: [r, g, b], alpha?, hex? }
10
+ * dimension = { value: number, unit: "px" | "rem" }
11
+ * duration = { value: number, unit: "ms" | "s" } (we already emit this)
12
+ *
13
+ * Export stays legacy by default (downstream consumers depend on it) and
14
+ * opts into 2025 via the `dtcgDialect` option. Import accepts BOTH dialects
15
+ * unconditionally. This module centralizes:
16
+ *
17
+ * - 2025 encoding helpers used by the dtcg/json formatters at render time
18
+ * - dialect-agnostic canonicalization used by the import diff so a 2025
19
+ * color object compares equal to the same color's legacy hex string
20
+ * (both are quantized to 1/255 per channel, tolerating the 8-bit
21
+ * precision loss inherent to hex)
22
+ * - stripping of the transient `rawColor` field the converter carries on
23
+ * COLOR TokenValues (full-precision floats for 2025 components) so it
24
+ * never leaks into serialized output
25
+ */
26
+ export function clamp01(n) {
27
+ if (Number.isNaN(n))
28
+ return 0;
29
+ return Math.max(0, Math.min(1, n));
30
+ }
31
+ function byteHex(f) {
32
+ return Math.round(clamp01(f) * 255)
33
+ .toString(16)
34
+ .padStart(2, "0");
35
+ }
36
+ /**
37
+ * Canonicalize any color-like literal to a lowercase 8-digit hex string
38
+ * (`#rrggbbaa`), or return null when the literal isn't recognizably a color.
39
+ *
40
+ * Accepts:
41
+ * - hex strings: #rgb, #rrggbb, #rrggbbaa
42
+ * - 2025.10 color objects with srgb (or unspecified) colorSpace +
43
+ * 3 numeric components (+ optional alpha)
44
+ * - color objects with a hex fallback field (any colorSpace) + optional alpha
45
+ *
46
+ * Both the components form and the hex form quantize to 1 / 255 per channel,
47
+ * so a full-precision components array compares equal to the hex string the
48
+ * legacy pipeline derived from the same Figma floats.
49
+ */
50
+ export function colorLiteralToCanonicalHex(literal) {
51
+ if (typeof literal === "string") {
52
+ const m = literal
53
+ .trim()
54
+ .match(/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i);
55
+ if (!m)
56
+ return null;
57
+ let digits = m[1].toLowerCase();
58
+ if (digits.length === 3) {
59
+ digits = digits
60
+ .split("")
61
+ .map((c) => c + c)
62
+ .join("");
63
+ }
64
+ if (digits.length === 6)
65
+ digits += "ff";
66
+ return `#${digits}`;
67
+ }
68
+ if (literal && typeof literal === "object" && !Array.isArray(literal)) {
69
+ const o = literal;
70
+ const colorSpace = typeof o.colorSpace === "string" ? o.colorSpace : undefined;
71
+ const comps = o.components;
72
+ if ((colorSpace === undefined || colorSpace === "srgb") &&
73
+ Array.isArray(comps) &&
74
+ comps.length === 3 &&
75
+ comps.every((c) => typeof c === "number")) {
76
+ const [r, g, b] = comps;
77
+ const a = typeof o.alpha === "number" ? o.alpha : 1;
78
+ return `#${byteHex(r)}${byteHex(g)}${byteHex(b)}${byteHex(a)}`;
79
+ }
80
+ if (typeof o.hex === "string") {
81
+ const base = colorLiteralToCanonicalHex(o.hex);
82
+ if (!base)
83
+ return null;
84
+ if (typeof o.alpha === "number") {
85
+ return base.slice(0, 7) + byteHex(o.alpha);
86
+ }
87
+ return base;
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+ /**
93
+ * Parse a hex color string to raw rgba floats (0–1). Returns null on
94
+ * anything that isn't a valid 3/6/8-digit hex string.
95
+ */
96
+ export function hexToRawRgba(hex) {
97
+ const canonical = colorLiteralToCanonicalHex(hex);
98
+ if (!canonical)
99
+ return null;
100
+ const d = canonical.slice(1);
101
+ return {
102
+ r: parseInt(d.slice(0, 2), 16) / 255,
103
+ g: parseInt(d.slice(2, 4), 16) / 255,
104
+ b: parseInt(d.slice(4, 6), 16) / 255,
105
+ a: parseInt(d.slice(6, 8), 16) / 255,
106
+ };
107
+ }
108
+ /**
109
+ * Encode a color TokenValue in the DTCG 2025.10 object form.
110
+ *
111
+ * Prefers the converter's transient `rawColor` floats (full precision β€” NOT
112
+ * round-tripped through 8-bit hex); falls back to parsing a hex-string
113
+ * literal. Literals already in object form pass through verbatim. Returns
114
+ * null when the value can't be encoded (caller keeps the legacy rendering).
115
+ *
116
+ * `alpha` is emitted only when < 1; `hex` is always included as the interop
117
+ * courtesy field (#RRGGBB, alpha carried separately).
118
+ */
119
+ export function colorValueTo2025(value) {
120
+ const lit = value.literal;
121
+ if (lit && typeof lit === "object" && !Array.isArray(lit)) {
122
+ // Already object-form (e.g. a re-exported 2025 document) β€” pass through.
123
+ const o = lit;
124
+ if ("colorSpace" in o || "components" in o || "hex" in o)
125
+ return o;
126
+ return null;
127
+ }
128
+ let floats = value.rawColor ?? null;
129
+ if (!floats && typeof lit === "string") {
130
+ floats = hexToRawRgba(lit);
131
+ }
132
+ if (!floats)
133
+ return null;
134
+ const { r, g, b, a } = floats;
135
+ return {
136
+ colorSpace: "srgb",
137
+ components: [r, g, b],
138
+ ...(a < 1 ? { alpha: a } : {}),
139
+ hex: `#${byteHex(r)}${byteHex(g)}${byteHex(b)}`.toUpperCase(),
140
+ };
141
+ }
142
+ /**
143
+ * Encode a dimension literal in the DTCG 2025.10 object form. The converter
144
+ * emits Figma FLOAT dimensions as bare unitless numbers conventionally
145
+ * interpreted as px (the same convention the css/json formatters use), so
146
+ * only bare finite numbers are converted; everything else (unit strings,
147
+ * pre-encoded objects) keeps its current rendering.
148
+ */
149
+ export function dimensionLiteralTo2025(literal) {
150
+ if (typeof literal === "number" && Number.isFinite(literal)) {
151
+ return { value: literal, unit: "px" };
152
+ }
153
+ return null;
154
+ }
155
+ /**
156
+ * Normalize a TokenValue to a dialect-agnostic comparable form for diffing:
157
+ *
158
+ * - the transient `rawColor` field is dropped (Figma-side values carry it;
159
+ * parsed code-side values never do)
160
+ * - color-like literals (hex strings OR 2025 color objects) normalize to a
161
+ * lowercase 8-digit hex string, quantized to 1/255 per channel
162
+ * - `{ value, unit }` objects with px/ms normalize to the bare number
163
+ * ("s" converts to ms first), so `{value: 16, unit: "px"}` equals 16 and
164
+ * `{value: 0.3, unit: "s"}` equals `{value: 300, unit: "ms"}`
165
+ * - "16px"-style strings normalize to the bare number
166
+ *
167
+ * Conservative by design: anything not confidently recognized is returned
168
+ * unchanged (minus rawColor), falling back to the existing deep comparison.
169
+ */
170
+ export function canonicalizeTokenValueForComparison(v) {
171
+ if (v === null || typeof v !== "object" || Array.isArray(v))
172
+ return v;
173
+ const obj = v;
174
+ if (!("literal" in obj) && !("reference" in obj))
175
+ return v;
176
+ const { rawColor: _rawColor, ...rest } = obj;
177
+ if (rest.literal !== undefined) {
178
+ const hex = colorLiteralToCanonicalHex(rest.literal);
179
+ if (hex !== null)
180
+ return { ...rest, literal: hex };
181
+ const num = numericLiteralToCanonical(rest.literal);
182
+ if (num !== null)
183
+ return { ...rest, literal: num };
184
+ }
185
+ return rest;
186
+ }
187
+ /**
188
+ * Normalize dimension/duration-shaped literals to a bare comparable number:
189
+ * - `{ value: n, unit: "px" | "ms" }` β†’ n
190
+ * - `{ value: n, unit: "s" }` β†’ n * 1000 (canonical ms)
191
+ * - `"16px"` strings β†’ 16
192
+ * Returns null for everything else (including rem/em/% and objects with
193
+ * extra fields β€” those keep structural comparison).
194
+ */
195
+ function numericLiteralToCanonical(literal) {
196
+ if (typeof literal === "string") {
197
+ const m = literal.trim().match(/^(-?(?:\d+\.?\d*|\.\d+))px$/i);
198
+ return m ? Number(m[1]) : null;
199
+ }
200
+ if (literal &&
201
+ typeof literal === "object" &&
202
+ !Array.isArray(literal) &&
203
+ Object.keys(literal).length === 2) {
204
+ const o = literal;
205
+ if (typeof o.value === "number" && typeof o.unit === "string") {
206
+ if (o.unit === "px" || o.unit === "ms")
207
+ return o.value;
208
+ if (o.unit === "s")
209
+ return o.value * 1000;
210
+ }
211
+ }
212
+ return null;
213
+ }
214
+ /**
215
+ * Return a copy of a mode-keyed TokenValue map with the transient `rawColor`
216
+ * field removed from every entry. Used wherever values get serialized
217
+ * (lastSyncedValue snapshots, diff-plan samples) so the transient field never
218
+ * appears in output β€” keeping legacy output byte-identical.
219
+ */
220
+ export function stripRawColorFromValues(values) {
221
+ const out = {};
222
+ for (const [k, v] of Object.entries(values)) {
223
+ if (v && typeof v === "object" && "rawColor" in v) {
224
+ const { rawColor: _rawColor, ...rest } = v;
225
+ out[k] = rest;
226
+ }
227
+ else {
228
+ out[k] = v;
229
+ }
230
+ }
231
+ return out;
232
+ }
@@ -12,6 +12,7 @@
12
12
  * $extensions["figma-console-mcp"] for round-trip non-destructiveness.
13
13
  */
14
14
  import { slugifySetName } from "./alias-resolver.js";
15
+ import { stripRawColorFromValues } from "./dialect.js";
15
16
  /**
16
17
  * Convert a Figma variables payload to our canonical TokenDocument.
17
18
  */
@@ -126,14 +127,35 @@ function convertVariable(variable, wantedModes, variableById, collectionNameById
126
127
  // reconstruct it.
127
128
  ...(strippedTypeSuffix ? { figmaName: variable.name } : {}),
128
129
  ...(Object.keys(springByMode).length > 0 ? { spring: springByMode } : {}),
130
+ // Scopes: stash only when NON-default. ["ALL_SCOPES"] (Figma's
131
+ // default) and empty/absent arrays are omitted so pre-existing
132
+ // exports stay byte-identical. codeSyntax: stash only when
133
+ // non-empty, same reasoning.
134
+ ...(hasNonDefaultScopes(variable.scopes)
135
+ ? { scopes: [...variable.scopes] }
136
+ : {}),
137
+ ...(variable.codeSyntax && Object.keys(variable.codeSyntax).length > 0
138
+ ? { codeSyntax: { ...variable.codeSyntax } }
139
+ : {}),
129
140
  lastSyncedAt: new Date().toISOString(),
130
141
  // We snapshot the synced value so future merge calls can detect
131
- // two-sided conflicts.
132
- lastSyncedValue: { ...values },
142
+ // two-sided conflicts. rawColor is transient render-time data and
143
+ // must not leak into serialized extensions (legacy output stays
144
+ // byte-identical).
145
+ lastSyncedValue: stripRawColorFromValues({ ...values }),
133
146
  },
134
147
  },
135
148
  };
136
149
  }
150
+ /**
151
+ * True when a variable's scopes array is meaningfully restrictive β€” i.e.
152
+ * present, non-empty, and not just the default ["ALL_SCOPES"].
153
+ */
154
+ function hasNonDefaultScopes(scopes) {
155
+ if (!Array.isArray(scopes) || scopes.length === 0)
156
+ return false;
157
+ return !(scopes.length === 1 && scopes[0] === "ALL_SCOPES");
158
+ }
137
159
  function mapResolvedType(resolvedType, variableName, warnings) {
138
160
  switch (resolvedType) {
139
161
  case "COLOR":
@@ -212,7 +234,19 @@ function convertValue(rawValue, variable, modeName, variableById, collectionName
212
234
  // Literal values per type.
213
235
  if (resolvedType === "COLOR") {
214
236
  if (typeof rawValue === "object" && rawValue !== null && "r" in rawValue) {
215
- return { literal: rgbaToHex(rawValue) };
237
+ // The hex string stays the literal (legacy dialect + back-compat), but
238
+ // we also carry the raw full-precision floats so the 2025.10 dialect
239
+ // can emit `components` without round-tripping through 8-bit hex.
240
+ // `rawColor` is transient β€” see TokenValue.rawColor in types.ts.
241
+ return {
242
+ literal: rgbaToHex(rawValue),
243
+ rawColor: {
244
+ r: rawValue.r,
245
+ g: rawValue.g,
246
+ b: rawValue.b,
247
+ a: rawValue.a ?? 1,
248
+ },
249
+ };
216
250
  }
217
251
  warnings.push(`COLOR value isn't an RGB object: ${JSON.stringify(rawValue)}`);
218
252
  return { literal: String(rawValue) };