figma-console-mcp 1.45.1 → 1.46.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.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  > **Your design system as an API.** A design-system-focused Model Context Protocol server for Figma. It works in both directions (Figma ⇄ code), **writes to Figma**, and runs **deterministic checks** for design-code parity, accessibility, and design-system health. It hands the AI exact tokens, variants, bindings, and states as structured data rather than prescribing a framework or house style, so generated code follows your team's own stack and conventions.
10
10
 
11
- > **🆕 Motion tools, motion tokens and code layers (v1.45.0, latest v1.45.1):** animate layers with Figma Motion presets, custom animation styles or keyframes, bind easing and duration to Timing and Easing tokens, sync motion tokens to and from code, export animations as MP4, GIF or WebM, and find code layers in a file. Recent releases added a shared local server over HTTP (`FIGMA_MCP_HTTP_PORT`, v1.44.0), MCP SDK and zod upgrades that clear seven security advisories (v1.43.0), and compact comment threads with filters and paging (v1.42.0). [See what's new →](CHANGELOG.md#1451---2026-10-29)
11
+ > **🆕 Shaders, grid and auto layout, OKLCH and P3 color, text wrap and variable fonts (latest v1.46.0):** apply shaders and set their properties by name (`figma_apply_shader`, `figma_list_shaders`), set auto layout or grid layout in one call with CSS-style grid tracks (`figma_set_layout`), use OKLCH, Display P3 and other CSS colors on fills and strokes, and set text wrap (balance, pretty) and variable-font axes with `figma_set_text` (v1.46.0). v1.45 added Motion: animate layers with presets, custom animation styles or keyframes bound to Timing and Easing tokens, export video, and find code layers. [See what's new →](CHANGELOG.md#1460---2026-10-29)
12
12
 
13
13
  ## What is this?
14
14
 
@@ -59,9 +59,9 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
59
59
  | Codebase → design system extraction (`figma_ds_*`) | ✅ | ❌ | ❌ |
60
60
  | Desktop Bridge plugin | ✅ | ✅ | ❌ |
61
61
  | Requires Node.js | Yes | **No** | No |
62
- | **Total tools available** | **127** | **102** after pairing | Read-only subset |
62
+ | **Total tools available** | **130** | **105** after pairing | Read-only subset |
63
63
 
64
- > **Bottom line:** Remote SSE is **read-only** until you pair the plugin. **Cloud Mode** unlocks write access (102 tools) from web AI clients without Node.js. NPX/Local Git gives the full 127 tools with real-time monitoring.
64
+ > **Bottom line:** Remote SSE is **read-only** until you pair the plugin. **Cloud Mode** unlocks write access (105 tools) from web AI clients without Node.js. NPX/Local Git gives the full 130 tools with real-time monitoring.
65
65
 
66
66
  ---
67
67
 
@@ -69,7 +69,7 @@ Figma Console MCP connects AI assistants (like Claude) to Figma, enabling:
69
69
 
70
70
  **Best for:** Designers who want full AI-assisted design capabilities.
71
71
 
72
- **What you get:** All 127 tools including design creation, variable management, and component instantiation.
72
+ **What you get:** All 130 tools including design creation, variable management, and component instantiation.
73
73
 
74
74
  #### Prerequisites
75
75
 
@@ -184,7 +184,7 @@ Create a simple frame with a blue background
184
184
 
185
185
  **Best for:** Developers who want to modify source code or contribute to the project.
186
186
 
187
- **What you get:** Same 127 tools as NPX, plus full source code access.
187
+ **What you get:** Same 130 tools as NPX, plus full source code access.
188
188
 
189
189
  #### Quick Setup
190
190
 
@@ -273,7 +273,7 @@ Ready for design creation? Follow the [NPX Setup](#-npx-setup-recommended) guide
273
273
 
274
274
  **Best for:** Using Claude.ai, v0, Replit, or Lovable to create and modify Figma designs — no Node.js required.
275
275
 
276
- **What you get:** 102 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.
276
+ **What you get:** 105 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.
277
277
 
278
278
  #### Prerequisites
279
279
 
@@ -330,7 +330,7 @@ AI Client → Cloud MCP Server → Durable Object Relay → Desktop Bridge Plugi
330
330
  | Feature | NPX (Recommended) | Cloud Mode | Local Git | Remote SSE |
331
331
  |---------|-------------------|------------|-----------|------------|
332
332
  | **Setup time** | ~10 minutes | ~5 minutes | ~15 minutes | ~2 minutes |
333
- | **Total tools** | **127** | **102** after pairing | **127** (Local Git) | Read-only subset |
333
+ | **Total tools** | **130** | **105** after pairing | **130** (Local Git) | Read-only subset |
334
334
  | **Design creation** | ✅ | ✅ | ✅ | ❌ |
335
335
  | **Variable management** | ✅ | ✅ | ✅ | ❌ |
336
336
  | **Component instantiation** | ✅ | ✅ | ✅ | ❌ |
@@ -345,7 +345,7 @@ AI Client → Cloud MCP Server → Durable Object Relay → Desktop Bridge Plugi
345
345
  | **Automatic updates** | ✅ (`@latest`) | ✅ | Manual (`git pull`) | ✅ |
346
346
  | **Source code access** | ❌ | ❌ | ✅ | ❌ |
347
347
 
348
- > **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 127 tools.
348
+ > **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 130 tools.
349
349
 
350
350
  **📖 [Complete Feature Comparison](docs/mode-comparison.md)**
351
351
 
@@ -483,6 +483,13 @@ Figma Motion (Plugin API beta). Animations play in Figma's preview and in video
483
483
  ### 🧱 Code Layers (Local Mode + Cloud Mode)
484
484
  - `figma_get_code_layers` - Find Figma code layers on a page or across the file, with position, size and linked dev resources. Figma's APIs don't expose code-layer source, so creating and editing them stays in Figma or Figma's own MCP (`send_code_to_figma`)
485
485
 
486
+ ### ✨ Shaders (Local Mode + Cloud Mode)
487
+ - `figma_list_shaders` - List shaders applied in the file (with each property's name, type and default, and the layers using them) and shaders from your libraries
488
+ - `figma_apply_shader` - **Apply a shader or change its properties by name** (`{turbulence: 40, cloudColor: '#E8F4FF'}`) on fills, strokes or effects, including variable bindings. Shaders themselves are created in Figma or with Figma's own MCP
489
+
490
+ ### 📐 Layout (Local Mode + Cloud Mode)
491
+ - `figma_set_layout` - **Auto layout and grid layout in one call**: direction, gaps, padding, alignment including `SPACE_EVENLY`/`SPACE_AROUND`, wrap, sizing, and grid tracks written CSS-style (`'240px 1fr 1fr'`, `repeat(3, 1fr)`) with child placement and spans
492
+
486
493
  ### 🔍 Design-Code Parity (All Modes)
487
494
  - `figma_check_design_parity` - Compare Figma component specs against code implementation, producing a scored diff report with actionable fix items
488
495
  - `figma_generate_component_doc` - Generate platform-agnostic markdown documentation by merging Figma design data with code-side info; the optional `history` parameter adds a changelog from Figma version history and `git log`
@@ -491,7 +498,7 @@ Figma Motion (Plugin API beta). Animations play in Figma's preview and in video
491
498
  - `figma_search_components` / `figma_get_component_details` - Find components (local + library) and get their variants, properties, and keys (Local Mode only)
492
499
  - `figma_instantiate_component` / `figma_set_instance_properties` - Place instances and set their TEXT, BOOLEAN, INSTANCE_SWAP, and VARIANT properties
493
500
  - `figma_add_component_property` / `figma_edit_component_property` / `figma_delete_component_property` - Manage component properties
494
- - `figma_create_child`, `figma_move_node`, `figma_resize_node`, `figma_clone_node`, `figma_rename_node`, `figma_delete_node`, `figma_set_text`, `figma_set_fills`, `figma_set_strokes`, `figma_set_image_fill` - Structured node edits, including variable binding on fills and strokes
501
+ - `figma_create_child`, `figma_move_node`, `figma_resize_node`, `figma_clone_node`, `figma_rename_node`, `figma_delete_node`, `figma_set_text`, `figma_set_fills`, `figma_set_strokes`, `figma_set_image_fill` - Structured node edits, including variable binding on fills and strokes, OKLCH/Display P3 and other CSS colors on fills and strokes, and text wrap style and variable-font axes on text
495
502
  - `figma_get_component_for_development_deep` - Full component tree with resolved token names and instance references
496
503
  - `figma_analyze_component_set` - A variant set as a state machine, with CSS pseudo-class mappings and per-variant diffs
497
504
 
@@ -761,7 +768,7 @@ The **Figma Desktop Bridge** plugin is the recommended way to connect Figma to t
761
768
  - The MCP server communicates via **WebSocket** through the Desktop Bridge plugin
762
769
  - The server tries port 9223 first, then automatically falls back through ports 9224–9232 if needed
763
770
  - The plugin scans all ports in the range and connects to every active server it finds
764
- - All 127 tools work through the WebSocket transport
771
+ - All 130 tools work through the WebSocket transport
765
772
 
766
773
  **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).
767
774
 
@@ -920,9 +927,10 @@ The two servers are complementary, and both can run in the same MCP client.
920
927
 
921
928
  ## 🛤️ Roadmap
922
929
 
923
- **Current Status:** v1.45.1 (Stable) - Production-ready. Latest: `figma_get_code_layers` no longer hangs on a file in a background window, and the tool docs were audited against every tool's real parameters (server-only). On top of v1.45.0's Motion tools — apply, read, remove and export Figma Motion animations, manage custom animation styles, write Timing and Easing variables from token files, and find code layers (server-only). On top of v1.44.2's Cloud sign-in requests `library_assets:read`, so component lookups by key work over OAuth (server-only). On top of v1.44.1's Cloud Mode accepting personal access tokens without the Current user scope, 403 errors name the missing scope, and setup guides for Codex, Gemini CLI and Antigravity (server-only). On top of v1.44.0's opt-in Streamable HTTP endpoint for Local Mode (`FIGMA_MCP_HTTP_PORT`, loopback only, optional bearer token) so several local clients share one server and its Figma connection (server-only). On top of v1.43.0's `@modelcontextprotocol/sdk` 1.32.1 and `zod` 4 clear all seven production security advisories, with tool behavior unchanged and Gemini-safe schemas (server-only). On top of v1.42.1's commands to a plugin stuck on a timed-out script fail fast with a busy message instead of queuing behind it, port-exhaustion eviction ends the least recently used server instead of the oldest, and `figma_execute` accepts up to a 300 s timeout (server-only). On top of v1.42.0's `figma_get_comments` returning compact threads (root + replies, newest activity first) with `node_id`, `thread_id` and `since` filters and `limit`/`offset` paging, 30 threads by default; summary counts are per thread (server-only, PR #139). On top of v1.41.5's component-set analysis reporting gradient/image/paint-opacity, corner radius, typography and label text, swapped spacing tokens, nested layer geometry and vector shape changes (re-import the plugin). On top of v1.41.4's variant background/stroke and auto-layout changes. On top of v1.41.3's component-set analysis reporting changes inside nested layers (instance swaps, nested token changes, opacity on the variant itself) and no longer reports a layer hidden in every variant as a change (re-import the plugin). On top of v1.41.2's design-system summary and `figma_get_token_values` report the file's tokens (they always reported 0) (server-only). On top of v1.41.1's component-set analysis reports real variant differences (Figma's default variant, size and layer changes), component loading no longer times out on large files (throttled plugin timers), relevance-ranked component search, and `figma_get_variables` paging that works with `format: "full"` (re-import the plugin for the component fixes). On top of v1.41.0's Cloud Mode sign-in hardening (per-session tokens on `/sse`, single-use authorization codes bound to client, redirect URI and PKCE, a consent page for unrecognized destinations), a script-execution fix in the accessibility scanner, `figma_import_tokens` refusing empty imports and scoping `replace` deletes to imported collections, `figma_arrange_component_set` no longer deleting other arranged sets, live variable reads for token export/import, Windows orphan-server exit (PR #140), and `fileKey` for the Slots tools (PR #112) (server-only, no plugin re-import). On top of v1.40.9's composed colors (a color variable set to another color at an opacity, "grey/900 at 50%") export and import with the link to the primitive intact — `color-mix()` in CSS/Tailwind v4, `rgba()` in SCSS, an alpha modifier in Tokens Studio, the resolved color plus the composition in DTCG — where the export previously failed or wrote `[object Object]` (server-only, no plugin re-import). On top of v1.40.8's min/max width and height survive every way of extracting a component as JSON — the design system kit, the reconstruction format and `figma_get_file_data` now carry them, design-code parity compares them, and the reconstruction format returns real geometry again instead of 50×50 placeholders when the plugin is connected (server-only, no plugin re-import). On top of v1.40.7's clearer Color Tokens table in generated docs — layers revealed by a boolean property are printed once under that property, and stroke/fill rows name the nested instance they belong to (server-only, no plugin re-import). On top of v1.40.6's overrides in extended variable collections are visible (previously reported as nonexistent), and a third round of generated-doc fidelity fixes — radius tokens read per corner, boolean props compared in Design-Code Parity, source links pinned to a commit, hidden layers linked to the property that shows them (server-only, no plugin re-import). On top of v1.40.5's clean dependency audit for the npm package — Worker-only dependencies (`@cloudflare/puppeteer`, which pulled in a high-severity `extract-zip` advisory with no upstream fix, and `agents`) no longer install for npm users, so `npm install figma-console-mcp` audits at 0 vulnerabilities and is 64 MB instead of 173 MB; the server also reports its real version to MCP clients instead of `0.1.0` (server-only, no plugin re-import). On top of v1.40.4's fixes found by live-testing v1.40.3 on hard production components — tools given a `fileUrl` now read variables, descriptions and annotations from THAT file rather than whichever file is active (previously every token name could vanish, or on an id collision come from the wrong file; `figma_get_component` could return a different component), a tool result over 16 MB no longer disconnects the server, anatomy labels sizing by the layout's real axes, and the icon/typography/anatomy output stays readable on large components (server-only, no plugin re-import). On top of v1.40.3's fidelity fixes for generated component docs on harder components — hidden layers are labeled instead of documented as rendered, one-sided borders report the weight that actually renders, typography and layer structure are compared across every variant instead of read from the first, gradients/shadows/opacity are no longer silently dropped, truncated trees say so, and `SLOT` properties are listed (server-only, no plugin re-import). On top of v1.40.2's data-loss fix for `figma_export_tokens` — with a different file active in Figma than intended, an export could replace a real token file with an empty one (or with another file's tokens) and report success; exports now refuse to write when a requested collection is missing, when the export is empty, when the target was generated from a different Figma file, or when overwriting would destroy tokens the export doesn't manage, and `strategy: "merge"` — documented as preserving code-only tokens but never implemented — is now a genuinely safe default (server-only, no plugin re-import). On top of v1.40.1's accuracy fixes for generated component docs across variants — a descendant's fill (typically an icon's) could be documented as a variant's Background, Color Token headings kept only a property literally named `Variant`, spacing was read from the first variant alone, and icons were only detected by layer name — plus `figma_export_tokens` accepting a file path as `outputPath` instead of failing with a raw `EEXIST` or silently creating a directory (server-only, no plugin re-import). On top of v1.40.0's design system extraction from production codebases — seven Local-Mode `figma_ds_*` tools that scan one or more apps (framework/styling/vendor detection, usage-ranked component inventory classified vendored/wrapped/pure-vendor/bespoke, variant inference from real call sites, duplicate detection, and an architecture pass separating UI-kit specializations from the missing generic primitives), mine the de-facto styling into DTCG tokens with per-token provenance (multi-mode CSS custom properties, SCSS, Tailwind config, shadcn HSL triples, utility-class frequency mining valued from the app's own theme), scaffold the design-system package with showcase docs pages, wire a fresh Storybook workshop to the app's real theme layers and fonts, deep-extract components for porting with CSF3 story scaffolds, verify with deterministic fidelity evals, and persist porting progress across sessions — the extracted tokens import straight into Figma variables via `figma_import_tokens`. On top of v1.39.1's fix for a plugin-update banner that could never be cleared — an older server instance nagging a newer plugin to re-import, which only ever installs the same or a newer plugin. On top of v1.39.0's multi-file execution — `figma_execute_across_files` runs one script against several Desktop Bridge-connected files concurrently (four files x 2s of work in ~3s, not ~8s), and `figma_execute` takes an optional `fileKey` to target a single file without moving the active file or releasing target lock; targeting must be explicit (`fileKeys` or `allFiles: true`), so nothing fans out to files you did not name. From community PR #107 by @Wolfr. Ships with a relay fix: `handleResult()` in the plugin's ui.html rebuilt each response field by field and silently dropped `resultAnalysis` and `fileContext`, so the `resultAnalysis.warning` check that `figma_execute`'s own description tells callers to perform has never been possible (re-import `manifest.json` for those two fields; everything else works without it). On top of v1.38.2's connection-stability fix — the server's own orphan reaper was terminating healthy MCP servers because its liveness probe used an IPv4 literal while the server binds the IPv6 loopback, which turned every kill-safety gate into a rubber stamp; this was the cause of recurring "Server disconnected" errors across all MCP clients. Alongside v1.38.1's fix for the shared-library variable tools, which had silently reported zero collections since v1.29.0 and reported failed imports as successes. On top of v1.38.0's ongoing component changelogs in generated docs — `figma_generate_component_doc` takes an opt-in `history` parameter that pulls per-component **design history** from Figma version history (each version diffed and scoped to that component, so renames, added properties, and token bindings show up as rows) alongside **code history** from `git log` on the component's source files, rendered as a `## History` section with design, code, and release-note tables. Prefers labeled versions, falls back to auto-saves when a file has none, and is off by default so existing calls are unchanged (server-only, no plugin re-import). On top of v1.37.x's design-system health audit for every client — `figma_audit_design_system_report` runs the dashboard's deterministic six-category scoring engine and returns the report as data with per-finding remediation, live-first fileKey-verified data (source disclosed), chunked per-category drill-down, and a 5-minute cache (server-only, no plugin re-import). On top of v1.36.0's target lock for multi-file parallel work — — `figma_navigate` takes a `lock: true` flag that pins the active file so an AI agent can work in one file while you work in another without commands routing to the wrong file. On top of v1.35.0's Figma Slots write support — create, inspect, populate, and reset Slots (GA at Config 2026) via 5 new tools (`figma_create_slot`, `figma_get_slots`, `figma_append_to_slot`, `figma_reset_slot`, `figma_add_slot_property`), live-validated against the GA Plugin API and based on community PR #77. On top of v1.34.0's Bidirectional Token Sync v2 + DTCG 2025.10 — `figma_import_tokens` applies the complete diff plan (creates missing collections/variables, applies renames, writes real `VARIABLE_ALIAS` references, and deletes only under explicit `replace`), `figma_export_tokens` speaks the DTCG 2025.10 dialect on request (legacy default byte-identical), variable scopes/codeSyntax round-trip via `$extensions`, `figma_setup_design_tokens` accepts alias values via DTCG brace references, and `figma_create_component_set` builds a full variant set from an axes matrix in one call. On top of the v1.33.x line: version-handshake fix (re-import banner only fires when plugin files actually changed), security dependency sweep, and the v1.33.0 connection UX overhaul (honest status pill derived from live connection state, `/health` auto-discovery with self-healing reconnect) + a 33-fix full-codebase audit (lossless DTCG multi-mode round-trips, cross-collection alias resolution, branch-URL correctness across REST tools, cache-poisoning and CSWSH fixes, bridge-first screenshots). Built on WCAG-accurate accessibility auditing (line height below 1.5× is no longer mis-flagged as a failure; readability hints decoupled from conformance checks and scoped to multi-line text; code-side WCAG 1.4.12 check), a self-healing Desktop Bridge connection (zombie-process reaper + auto-reconnect watchdog — fixes the recurring "not connected until restart" bug), native variable binding on fills/strokes + typography control in the write tools, shared-library inspection (key-based component resolution + library variable read/import without Enterprise plan), 10-format token export pipeline (DTCG, CSS, Tailwind v4, Tailwind v3, SCSS, TS module, JSON flat/nested, Style Dictionary v3, Tokens Studio), bidirectional Figma↔code token sync, version history & time-series awareness, FigJam + Slides support, Cloud Write Relay, Design System Kit, WebSocket-only connectivity, smart multi-file tracking, **127 tools** (Local) / **102 tools** (Cloud) / read-only subset (Remote), Comments API, cross-MCP identity disambiguation, and MCP Apps.
930
+ **Current Status:** v1.46.0 (Stable) - Production-ready. Latest: shaders applied with properties set by name, `figma_set_layout` for auto layout and grid layout, OKLCH/Display P3 and other CSS colors on fills and strokes, and text wrap style and variable-font axes in `figma_set_text` (server-only). On top of v1.45.1's `figma_get_code_layers` no longer hangs on a file in a background window, and the tool docs were audited against every tool's real parameters (server-only). On top of v1.45.0's Motion tools — apply, read, remove and export Figma Motion animations, manage custom animation styles, write Timing and Easing variables from token files, and find code layers (server-only). On top of v1.44.2's Cloud sign-in requests `library_assets:read`, so component lookups by key work over OAuth (server-only). On top of v1.44.1's Cloud Mode accepting personal access tokens without the Current user scope, 403 errors name the missing scope, and setup guides for Codex, Gemini CLI and Antigravity (server-only). On top of v1.44.0's opt-in Streamable HTTP endpoint for Local Mode (`FIGMA_MCP_HTTP_PORT`, loopback only, optional bearer token) so several local clients share one server and its Figma connection (server-only). On top of v1.43.0's `@modelcontextprotocol/sdk` 1.32.1 and `zod` 4 clear all seven production security advisories, with tool behavior unchanged and Gemini-safe schemas (server-only). On top of v1.42.1's commands to a plugin stuck on a timed-out script fail fast with a busy message instead of queuing behind it, port-exhaustion eviction ends the least recently used server instead of the oldest, and `figma_execute` accepts up to a 300 s timeout (server-only). On top of v1.42.0's `figma_get_comments` returning compact threads (root + replies, newest activity first) with `node_id`, `thread_id` and `since` filters and `limit`/`offset` paging, 30 threads by default; summary counts are per thread (server-only, PR #139). On top of v1.41.5's component-set analysis reporting gradient/image/paint-opacity, corner radius, typography and label text, swapped spacing tokens, nested layer geometry and vector shape changes (re-import the plugin). On top of v1.41.4's variant background/stroke and auto-layout changes. On top of v1.41.3's component-set analysis reporting changes inside nested layers (instance swaps, nested token changes, opacity on the variant itself) and no longer reports a layer hidden in every variant as a change (re-import the plugin). On top of v1.41.2's design-system summary and `figma_get_token_values` report the file's tokens (they always reported 0) (server-only). On top of v1.41.1's component-set analysis reports real variant differences (Figma's default variant, size and layer changes), component loading no longer times out on large files (throttled plugin timers), relevance-ranked component search, and `figma_get_variables` paging that works with `format: "full"` (re-import the plugin for the component fixes). On top of v1.41.0's Cloud Mode sign-in hardening (per-session tokens on `/sse`, single-use authorization codes bound to client, redirect URI and PKCE, a consent page for unrecognized destinations), a script-execution fix in the accessibility scanner, `figma_import_tokens` refusing empty imports and scoping `replace` deletes to imported collections, `figma_arrange_component_set` no longer deleting other arranged sets, live variable reads for token export/import, Windows orphan-server exit (PR #140), and `fileKey` for the Slots tools (PR #112) (server-only, no plugin re-import). On top of v1.40.9's composed colors (a color variable set to another color at an opacity, "grey/900 at 50%") export and import with the link to the primitive intact — `color-mix()` in CSS/Tailwind v4, `rgba()` in SCSS, an alpha modifier in Tokens Studio, the resolved color plus the composition in DTCG — where the export previously failed or wrote `[object Object]` (server-only, no plugin re-import). On top of v1.40.8's min/max width and height survive every way of extracting a component as JSON — the design system kit, the reconstruction format and `figma_get_file_data` now carry them, design-code parity compares them, and the reconstruction format returns real geometry again instead of 50×50 placeholders when the plugin is connected (server-only, no plugin re-import). On top of v1.40.7's clearer Color Tokens table in generated docs — layers revealed by a boolean property are printed once under that property, and stroke/fill rows name the nested instance they belong to (server-only, no plugin re-import). On top of v1.40.6's overrides in extended variable collections are visible (previously reported as nonexistent), and a third round of generated-doc fidelity fixes — radius tokens read per corner, boolean props compared in Design-Code Parity, source links pinned to a commit, hidden layers linked to the property that shows them (server-only, no plugin re-import). On top of v1.40.5's clean dependency audit for the npm package — Worker-only dependencies (`@cloudflare/puppeteer`, which pulled in a high-severity `extract-zip` advisory with no upstream fix, and `agents`) no longer install for npm users, so `npm install figma-console-mcp` audits at 0 vulnerabilities and is 64 MB instead of 173 MB; the server also reports its real version to MCP clients instead of `0.1.0` (server-only, no plugin re-import). On top of v1.40.4's fixes found by live-testing v1.40.3 on hard production components — tools given a `fileUrl` now read variables, descriptions and annotations from THAT file rather than whichever file is active (previously every token name could vanish, or on an id collision come from the wrong file; `figma_get_component` could return a different component), a tool result over 16 MB no longer disconnects the server, anatomy labels sizing by the layout's real axes, and the icon/typography/anatomy output stays readable on large components (server-only, no plugin re-import). On top of v1.40.3's fidelity fixes for generated component docs on harder components — hidden layers are labeled instead of documented as rendered, one-sided borders report the weight that actually renders, typography and layer structure are compared across every variant instead of read from the first, gradients/shadows/opacity are no longer silently dropped, truncated trees say so, and `SLOT` properties are listed (server-only, no plugin re-import). On top of v1.40.2's data-loss fix for `figma_export_tokens` — with a different file active in Figma than intended, an export could replace a real token file with an empty one (or with another file's tokens) and report success; exports now refuse to write when a requested collection is missing, when the export is empty, when the target was generated from a different Figma file, or when overwriting would destroy tokens the export doesn't manage, and `strategy: "merge"` — documented as preserving code-only tokens but never implemented — is now a genuinely safe default (server-only, no plugin re-import). On top of v1.40.1's accuracy fixes for generated component docs across variants — a descendant's fill (typically an icon's) could be documented as a variant's Background, Color Token headings kept only a property literally named `Variant`, spacing was read from the first variant alone, and icons were only detected by layer name — plus `figma_export_tokens` accepting a file path as `outputPath` instead of failing with a raw `EEXIST` or silently creating a directory (server-only, no plugin re-import). On top of v1.40.0's design system extraction from production codebases — seven Local-Mode `figma_ds_*` tools that scan one or more apps (framework/styling/vendor detection, usage-ranked component inventory classified vendored/wrapped/pure-vendor/bespoke, variant inference from real call sites, duplicate detection, and an architecture pass separating UI-kit specializations from the missing generic primitives), mine the de-facto styling into DTCG tokens with per-token provenance (multi-mode CSS custom properties, SCSS, Tailwind config, shadcn HSL triples, utility-class frequency mining valued from the app's own theme), scaffold the design-system package with showcase docs pages, wire a fresh Storybook workshop to the app's real theme layers and fonts, deep-extract components for porting with CSF3 story scaffolds, verify with deterministic fidelity evals, and persist porting progress across sessions — the extracted tokens import straight into Figma variables via `figma_import_tokens`. On top of v1.39.1's fix for a plugin-update banner that could never be cleared — an older server instance nagging a newer plugin to re-import, which only ever installs the same or a newer plugin. On top of v1.39.0's multi-file execution — `figma_execute_across_files` runs one script against several Desktop Bridge-connected files concurrently (four files x 2s of work in ~3s, not ~8s), and `figma_execute` takes an optional `fileKey` to target a single file without moving the active file or releasing target lock; targeting must be explicit (`fileKeys` or `allFiles: true`), so nothing fans out to files you did not name. From community PR #107 by @Wolfr. Ships with a relay fix: `handleResult()` in the plugin's ui.html rebuilt each response field by field and silently dropped `resultAnalysis` and `fileContext`, so the `resultAnalysis.warning` check that `figma_execute`'s own description tells callers to perform has never been possible (re-import `manifest.json` for those two fields; everything else works without it). On top of v1.38.2's connection-stability fix — the server's own orphan reaper was terminating healthy MCP servers because its liveness probe used an IPv4 literal while the server binds the IPv6 loopback, which turned every kill-safety gate into a rubber stamp; this was the cause of recurring "Server disconnected" errors across all MCP clients. Alongside v1.38.1's fix for the shared-library variable tools, which had silently reported zero collections since v1.29.0 and reported failed imports as successes. On top of v1.38.0's ongoing component changelogs in generated docs — `figma_generate_component_doc` takes an opt-in `history` parameter that pulls per-component **design history** from Figma version history (each version diffed and scoped to that component, so renames, added properties, and token bindings show up as rows) alongside **code history** from `git log` on the component's source files, rendered as a `## History` section with design, code, and release-note tables. Prefers labeled versions, falls back to auto-saves when a file has none, and is off by default so existing calls are unchanged (server-only, no plugin re-import). On top of v1.37.x's design-system health audit for every client — `figma_audit_design_system_report` runs the dashboard's deterministic six-category scoring engine and returns the report as data with per-finding remediation, live-first fileKey-verified data (source disclosed), chunked per-category drill-down, and a 5-minute cache (server-only, no plugin re-import). On top of v1.36.0's target lock for multi-file parallel work — — `figma_navigate` takes a `lock: true` flag that pins the active file so an AI agent can work in one file while you work in another without commands routing to the wrong file. On top of v1.35.0's Figma Slots write support — create, inspect, populate, and reset Slots (GA at Config 2026) via 5 new tools (`figma_create_slot`, `figma_get_slots`, `figma_append_to_slot`, `figma_reset_slot`, `figma_add_slot_property`), live-validated against the GA Plugin API and based on community PR #77. On top of v1.34.0's Bidirectional Token Sync v2 + DTCG 2025.10 — `figma_import_tokens` applies the complete diff plan (creates missing collections/variables, applies renames, writes real `VARIABLE_ALIAS` references, and deletes only under explicit `replace`), `figma_export_tokens` speaks the DTCG 2025.10 dialect on request (legacy default byte-identical), variable scopes/codeSyntax round-trip via `$extensions`, `figma_setup_design_tokens` accepts alias values via DTCG brace references, and `figma_create_component_set` builds a full variant set from an axes matrix in one call. On top of the v1.33.x line: version-handshake fix (re-import banner only fires when plugin files actually changed), security dependency sweep, and the v1.33.0 connection UX overhaul (honest status pill derived from live connection state, `/health` auto-discovery with self-healing reconnect) + a 33-fix full-codebase audit (lossless DTCG multi-mode round-trips, cross-collection alias resolution, branch-URL correctness across REST tools, cache-poisoning and CSWSH fixes, bridge-first screenshots). Built on WCAG-accurate accessibility auditing (line height below 1.5× is no longer mis-flagged as a failure; readability hints decoupled from conformance checks and scoped to multi-line text; code-side WCAG 1.4.12 check), a self-healing Desktop Bridge connection (zombie-process reaper + auto-reconnect watchdog — fixes the recurring "not connected until restart" bug), native variable binding on fills/strokes + typography control in the write tools, shared-library inspection (key-based component resolution + library variable read/import without Enterprise plan), 10-format token export pipeline (DTCG, CSS, Tailwind v4, Tailwind v3, SCSS, TS module, JSON flat/nested, Style Dictionary v3, Tokens Studio), bidirectional Figma↔code token sync, version history & time-series awareness, FigJam + Slides support, Cloud Write Relay, Design System Kit, WebSocket-only connectivity, smart multi-file tracking, **130 tools** (Local) / **105 tools** (Cloud) / read-only subset (Remote), Comments API, cross-MCP identity disambiguation, and MCP Apps.
924
931
 
925
932
  **Recent Releases:**
933
+ - [x] **v1.46.0** - `figma_list_shaders`, `figma_apply_shader` (properties by name), `figma_set_layout` (auto layout and grid), CSS colors on fills/strokes, text wrap and variable-font axes (#150). Server-only.
926
934
  - [x] **v1.45.1** - `figma_get_code_layers` no longer hangs on background-window files (#149); tool docs audited against real parameters (#151). Server-only.
927
935
  - [x] **v1.45.0** - Motion tools (apply, read, remove, export MP4/GIF/WebM, custom animation styles), Timing/Easing token import and writes, easing export fix, and `figma_get_code_layers` (#147). Server-only.
928
936
  - [x] **v1.44.2** - Cloud sign-in requests `library_assets:read`; component lookups by key work over OAuth. Server-only.
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Layout tool: auto layout and grid layout on a frame in one call.
3
+ *
4
+ * Covers what used to need figma_execute: direction, gaps, padding,
5
+ * alignment (including SPACE_EVENLY / SPACE_AROUND), wrap, sizing, and
6
+ * Figma's grid layout (tracks written CSS-style, gaps, auto rows, auto flow,
7
+ * and child placement with spans).
8
+ *
9
+ * Builds a Plugin API script and runs it through executeCodeViaUI, so no
10
+ * Desktop Bridge plugin change (or re-import) is needed.
11
+ */
12
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ export interface GridTrack {
14
+ type: "FLEX" | "FIXED" | "HUG";
15
+ value?: number;
16
+ }
17
+ /**
18
+ * Parse grid tracks written CSS-style: "1fr 2fr 120px", "repeat(3, 1fr)",
19
+ * "auto 1fr" (auto = hug content), bare numbers are pixels. A number input
20
+ * means that many equal 1fr tracks.
21
+ */
22
+ export declare function parseGridTracks(input: string | number): GridTrack[];
23
+ export declare const SET_LAYOUT_BODY = "\nconst node = await figma.getNodeByIdAsync(P.nodeId);\nif (!node) throw new Error('Node not found: ' + P.nodeId);\nif (!('layoutMode' in node)) throw new Error(node.type + ' nodes have no layout. Use a frame, component or instance.');\nif (P.mode) node.layoutMode = P.mode;\nconst mode = node.layoutMode;\nconst isGrid = mode === 'GRID';\nconst isStack = mode === 'HORIZONTAL' || mode === 'VERTICAL';\nconst needs = (what, ok) => { if (!ok) throw new Error(what + ' needs ' + (what.startsWith('grid') ? 'mode GRID' : 'mode HORIZONTAL or VERTICAL') + ' (this frame is ' + mode + ').'); };\n\nif (P.padding !== undefined) {\n needs('padding', mode !== 'NONE');\n const p = typeof P.padding === 'number' ? { top: P.padding, right: P.padding, bottom: P.padding, left: P.padding } : P.padding;\n if (p.top !== undefined) node.paddingTop = p.top;\n if (p.right !== undefined) node.paddingRight = p.right;\n if (p.bottom !== undefined) node.paddingBottom = p.bottom;\n if (p.left !== undefined) node.paddingLeft = p.left;\n}\nif (P.wrap !== undefined) { needs('wrap', isStack); node.layoutWrap = P.wrap ? 'WRAP' : 'NO_WRAP'; }\nif (P.gap !== undefined) {\n if (isGrid) { node.gridRowGap = P.gap; node.gridColumnGap = P.gap; }\n else { needs('gap', isStack); node.itemSpacing = P.gap; }\n}\nif (P.counterAxisGap !== undefined) { needs('counterAxisGap', isStack); node.counterAxisSpacing = P.counterAxisGap; }\nif (P.primaryAlign !== undefined) { needs('primaryAlign', isStack); node.primaryAxisAlignItems = P.primaryAlign; }\nif (P.counterAlign !== undefined) { needs('counterAlign', isStack); node.counterAxisAlignItems = P.counterAlign; }\nif (P.alignContent !== undefined) { needs('alignContent', isStack); node.counterAxisAlignContent = P.alignContent; }\nif (P.reverseZIndex !== undefined) { needs('reverseZIndex', isStack); node.itemReverseZIndex = P.reverseZIndex; }\n\nif (P.grid) {\n needs('grid', isGrid);\n const g = P.grid;\n const applyTracks = (tracks, countKey, sizesKey) => {\n node[countKey] = tracks.length;\n tracks.forEach((t, i) => {\n const s = node[sizesKey][i];\n s.type = t.type;\n if (t.value !== undefined) s.value = t.value;\n });\n };\n if (g.autoRows === false || (g.rows && g.autoRows !== true)) { if (node.gridAutoTracks === 'ROWS') node.gridAutoTracks = 'NONE'; }\n if (g.columns) applyTracks(g.columns, 'gridColumnCount', 'gridColumnSizes');\n if (g.rows) applyTracks(g.rows, 'gridRowCount', 'gridRowSizes');\n if (g.autoRows === true) node.gridAutoTracks = 'ROWS';\n if (g.autoFlow !== undefined) node.gridItemsPositioning = g.autoFlow ? 'ROW_AUTO_FLOW' : 'MANUAL';\n if (g.rowGap !== undefined) node.gridRowGap = g.rowGap;\n if (g.columnGap !== undefined) node.gridColumnGap = g.columnGap;\n}\n\n// Sizing last: HUG and FILL depend on the layout set above.\nif (P.width !== undefined || P.height !== undefined) node.resize(P.width !== undefined ? P.width : node.width, P.height !== undefined ? P.height : node.height);\nif (P.horizontalSizing !== undefined) node.layoutSizingHorizontal = P.horizontalSizing;\nif (P.verticalSizing !== undefined) node.layoutSizingVertical = P.verticalSizing;\n\nconst children = [];\nfor (const c of P.children || []) {\n const child = await figma.getNodeByIdAsync(c.nodeId);\n if (!child) throw new Error('Child not found: ' + c.nodeId);\n if (child.parent !== node) {\n if (isGrid && c.row !== undefined && c.column !== undefined && node.gridItemsPositioning !== 'ROW_AUTO_FLOW') node.appendChildAt(child, c.row, c.column);\n else node.appendChild(child);\n } else if (c.row !== undefined || c.column !== undefined) {\n needs('grid placement (row, column)', isGrid);\n if (node.gridItemsPositioning === 'ROW_AUTO_FLOW') throw new Error('This grid places children automatically (autoFlow). Turn autoFlow off to place ' + c.nodeId + ' by row and column.');\n child.setGridChildPosition(c.row !== undefined ? c.row : child.gridRowAnchorIndex, c.column !== undefined ? c.column : child.gridColumnAnchorIndex);\n }\n if (c.rowSpan !== undefined) { needs('grid rowSpan', isGrid); child.gridRowSpan = c.rowSpan; }\n if (c.columnSpan !== undefined) { needs('grid columnSpan', isGrid); child.gridColumnSpan = c.columnSpan; }\n if (c.horizontalAlign !== undefined) { needs('grid horizontalAlign', isGrid); child.gridChildHorizontalAlign = c.horizontalAlign; }\n if (c.verticalAlign !== undefined) { needs('grid verticalAlign', isGrid); child.gridChildVerticalAlign = c.verticalAlign; }\n if (c.positioning !== undefined) child.layoutPositioning = c.positioning;\n if (c.horizontalSizing !== undefined) child.layoutSizingHorizontal = c.horizontalSizing;\n if (c.verticalSizing !== undefined) child.layoutSizingVertical = c.verticalSizing;\n const info = { nodeId: child.id, name: child.name };\n if (isGrid) Object.assign(info, { row: child.gridRowAnchorIndex, column: child.gridColumnAnchorIndex, rowSpan: child.gridRowSpan, columnSpan: child.gridColumnSpan });\n children.push(info);\n}\n\nconst out = { nodeId: node.id, name: node.name, mode: node.layoutMode, width: node.width, height: node.height };\nif ('layoutSizingHorizontal' in node) Object.assign(out, { horizontalSizing: node.layoutSizingHorizontal, verticalSizing: node.layoutSizingVertical });\nif (node.layoutMode !== 'NONE') out.padding = { top: node.paddingTop, right: node.paddingRight, bottom: node.paddingBottom, left: node.paddingLeft };\nif (isStack) Object.assign(out, { gap: node.itemSpacing, wrap: node.layoutWrap === 'WRAP', counterAxisGap: node.layoutWrap === 'WRAP' ? node.counterAxisSpacing : undefined, primaryAlign: node.primaryAxisAlignItems, counterAlign: node.counterAxisAlignItems, alignContent: node.layoutWrap === 'WRAP' ? node.counterAxisAlignContent : undefined });\n// Built from the track sizes: gridColumnSizingCSS can lag a write within the same script.\nconst css = (sizes) => sizes.map(t => t.type === 'FIXED' ? t.value + 'px' : t.type === 'HUG' ? 'auto' : (t.value === undefined ? 1 : t.value) + 'fr').join(' ');\nif (node.layoutMode === 'GRID') out.grid = { columns: css(node.gridColumnSizes), rows: css(node.gridRowSizes), columnCount: node.gridColumnCount, rowCount: node.gridRowCount, columnGap: node.gridColumnGap, rowGap: node.gridRowGap, autoRows: node.gridAutoTracks === 'ROWS', autoFlow: node.gridItemsPositioning === 'ROW_AUTO_FLOW' };\nif (children.length) out.children = children;\nreturn out;\n";
24
+ export declare function layoutScript(payload: unknown): string;
25
+ export declare function registerLayoutTools(server: McpServer, getDesktopConnector: () => Promise<any>): void;
26
+ //# sourceMappingURL=layout-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout-tools.d.ts","sourceRoot":"","sources":["../../src/core/layout-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAMzE,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,EAAE,CAmBnE;AAED,eAAO,MAAM,eAAe,iyMAuF3B,CAAC;AAEF,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAErD;AAKD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CA+EpG"}
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Layout tool: auto layout and grid layout on a frame in one call.
3
+ *
4
+ * Covers what used to need figma_execute: direction, gaps, padding,
5
+ * alignment (including SPACE_EVENLY / SPACE_AROUND), wrap, sizing, and
6
+ * Figma's grid layout (tracks written CSS-style, gaps, auto rows, auto flow,
7
+ * and child placement with spans).
8
+ *
9
+ * Builds a Plugin API script and runs it through executeCodeViaUI, so no
10
+ * Desktop Bridge plugin change (or re-import) is needed.
11
+ */
12
+ import { z } from "zod";
13
+ import { createChildLogger } from "./logger.js";
14
+ const logger = createChildLogger({ component: "layout-tools" });
15
+ /**
16
+ * Parse grid tracks written CSS-style: "1fr 2fr 120px", "repeat(3, 1fr)",
17
+ * "auto 1fr" (auto = hug content), bare numbers are pixels. A number input
18
+ * means that many equal 1fr tracks.
19
+ */
20
+ export function parseGridTracks(input) {
21
+ if (typeof input === "number") {
22
+ if (!Number.isInteger(input) || input < 1)
23
+ throw new Error(`Track count must be a positive whole number, got ${input}`);
24
+ return Array.from({ length: input }, () => ({ type: "FLEX", value: 1 }));
25
+ }
26
+ const expanded = input.replace(/repeat\(\s*(\d+)\s*,\s*([^)]+)\)/gi, (_, n, body) => Array.from({ length: Number(n) }, () => body.trim()).join(" "));
27
+ const tokens = expanded.trim().split(/\s+/).filter(Boolean);
28
+ if (tokens.length === 0)
29
+ throw new Error("No grid tracks given");
30
+ return tokens.map((token) => {
31
+ const t = token.toLowerCase();
32
+ if (t === "auto" || t === "hug" || t === "fit-content")
33
+ return { type: "HUG" };
34
+ const fr = /^(\d*\.?\d+)?fr$/.exec(t);
35
+ if (fr)
36
+ return { type: "FLEX", value: fr[1] ? Number(fr[1]) : 1 };
37
+ const px = /^(\d*\.?\d+)(px)?$/.exec(t);
38
+ if (px)
39
+ return { type: "FIXED", value: Number(px[1]) };
40
+ throw new Error(`Can't read grid track "${token}". Use Nfr, Npx, auto, or repeat(n, …)`);
41
+ });
42
+ }
43
+ export const SET_LAYOUT_BODY = `
44
+ const node = await figma.getNodeByIdAsync(P.nodeId);
45
+ if (!node) throw new Error('Node not found: ' + P.nodeId);
46
+ if (!('layoutMode' in node)) throw new Error(node.type + ' nodes have no layout. Use a frame, component or instance.');
47
+ if (P.mode) node.layoutMode = P.mode;
48
+ const mode = node.layoutMode;
49
+ const isGrid = mode === 'GRID';
50
+ const isStack = mode === 'HORIZONTAL' || mode === 'VERTICAL';
51
+ const needs = (what, ok) => { if (!ok) throw new Error(what + ' needs ' + (what.startsWith('grid') ? 'mode GRID' : 'mode HORIZONTAL or VERTICAL') + ' (this frame is ' + mode + ').'); };
52
+
53
+ if (P.padding !== undefined) {
54
+ needs('padding', mode !== 'NONE');
55
+ const p = typeof P.padding === 'number' ? { top: P.padding, right: P.padding, bottom: P.padding, left: P.padding } : P.padding;
56
+ if (p.top !== undefined) node.paddingTop = p.top;
57
+ if (p.right !== undefined) node.paddingRight = p.right;
58
+ if (p.bottom !== undefined) node.paddingBottom = p.bottom;
59
+ if (p.left !== undefined) node.paddingLeft = p.left;
60
+ }
61
+ if (P.wrap !== undefined) { needs('wrap', isStack); node.layoutWrap = P.wrap ? 'WRAP' : 'NO_WRAP'; }
62
+ if (P.gap !== undefined) {
63
+ if (isGrid) { node.gridRowGap = P.gap; node.gridColumnGap = P.gap; }
64
+ else { needs('gap', isStack); node.itemSpacing = P.gap; }
65
+ }
66
+ if (P.counterAxisGap !== undefined) { needs('counterAxisGap', isStack); node.counterAxisSpacing = P.counterAxisGap; }
67
+ if (P.primaryAlign !== undefined) { needs('primaryAlign', isStack); node.primaryAxisAlignItems = P.primaryAlign; }
68
+ if (P.counterAlign !== undefined) { needs('counterAlign', isStack); node.counterAxisAlignItems = P.counterAlign; }
69
+ if (P.alignContent !== undefined) { needs('alignContent', isStack); node.counterAxisAlignContent = P.alignContent; }
70
+ if (P.reverseZIndex !== undefined) { needs('reverseZIndex', isStack); node.itemReverseZIndex = P.reverseZIndex; }
71
+
72
+ if (P.grid) {
73
+ needs('grid', isGrid);
74
+ const g = P.grid;
75
+ const applyTracks = (tracks, countKey, sizesKey) => {
76
+ node[countKey] = tracks.length;
77
+ tracks.forEach((t, i) => {
78
+ const s = node[sizesKey][i];
79
+ s.type = t.type;
80
+ if (t.value !== undefined) s.value = t.value;
81
+ });
82
+ };
83
+ if (g.autoRows === false || (g.rows && g.autoRows !== true)) { if (node.gridAutoTracks === 'ROWS') node.gridAutoTracks = 'NONE'; }
84
+ if (g.columns) applyTracks(g.columns, 'gridColumnCount', 'gridColumnSizes');
85
+ if (g.rows) applyTracks(g.rows, 'gridRowCount', 'gridRowSizes');
86
+ if (g.autoRows === true) node.gridAutoTracks = 'ROWS';
87
+ if (g.autoFlow !== undefined) node.gridItemsPositioning = g.autoFlow ? 'ROW_AUTO_FLOW' : 'MANUAL';
88
+ if (g.rowGap !== undefined) node.gridRowGap = g.rowGap;
89
+ if (g.columnGap !== undefined) node.gridColumnGap = g.columnGap;
90
+ }
91
+
92
+ // Sizing last: HUG and FILL depend on the layout set above.
93
+ if (P.width !== undefined || P.height !== undefined) node.resize(P.width !== undefined ? P.width : node.width, P.height !== undefined ? P.height : node.height);
94
+ if (P.horizontalSizing !== undefined) node.layoutSizingHorizontal = P.horizontalSizing;
95
+ if (P.verticalSizing !== undefined) node.layoutSizingVertical = P.verticalSizing;
96
+
97
+ const children = [];
98
+ for (const c of P.children || []) {
99
+ const child = await figma.getNodeByIdAsync(c.nodeId);
100
+ if (!child) throw new Error('Child not found: ' + c.nodeId);
101
+ if (child.parent !== node) {
102
+ if (isGrid && c.row !== undefined && c.column !== undefined && node.gridItemsPositioning !== 'ROW_AUTO_FLOW') node.appendChildAt(child, c.row, c.column);
103
+ else node.appendChild(child);
104
+ } else if (c.row !== undefined || c.column !== undefined) {
105
+ needs('grid placement (row, column)', isGrid);
106
+ if (node.gridItemsPositioning === 'ROW_AUTO_FLOW') throw new Error('This grid places children automatically (autoFlow). Turn autoFlow off to place ' + c.nodeId + ' by row and column.');
107
+ child.setGridChildPosition(c.row !== undefined ? c.row : child.gridRowAnchorIndex, c.column !== undefined ? c.column : child.gridColumnAnchorIndex);
108
+ }
109
+ if (c.rowSpan !== undefined) { needs('grid rowSpan', isGrid); child.gridRowSpan = c.rowSpan; }
110
+ if (c.columnSpan !== undefined) { needs('grid columnSpan', isGrid); child.gridColumnSpan = c.columnSpan; }
111
+ if (c.horizontalAlign !== undefined) { needs('grid horizontalAlign', isGrid); child.gridChildHorizontalAlign = c.horizontalAlign; }
112
+ if (c.verticalAlign !== undefined) { needs('grid verticalAlign', isGrid); child.gridChildVerticalAlign = c.verticalAlign; }
113
+ if (c.positioning !== undefined) child.layoutPositioning = c.positioning;
114
+ if (c.horizontalSizing !== undefined) child.layoutSizingHorizontal = c.horizontalSizing;
115
+ if (c.verticalSizing !== undefined) child.layoutSizingVertical = c.verticalSizing;
116
+ const info = { nodeId: child.id, name: child.name };
117
+ if (isGrid) Object.assign(info, { row: child.gridRowAnchorIndex, column: child.gridColumnAnchorIndex, rowSpan: child.gridRowSpan, columnSpan: child.gridColumnSpan });
118
+ children.push(info);
119
+ }
120
+
121
+ const out = { nodeId: node.id, name: node.name, mode: node.layoutMode, width: node.width, height: node.height };
122
+ if ('layoutSizingHorizontal' in node) Object.assign(out, { horizontalSizing: node.layoutSizingHorizontal, verticalSizing: node.layoutSizingVertical });
123
+ if (node.layoutMode !== 'NONE') out.padding = { top: node.paddingTop, right: node.paddingRight, bottom: node.paddingBottom, left: node.paddingLeft };
124
+ if (isStack) Object.assign(out, { gap: node.itemSpacing, wrap: node.layoutWrap === 'WRAP', counterAxisGap: node.layoutWrap === 'WRAP' ? node.counterAxisSpacing : undefined, primaryAlign: node.primaryAxisAlignItems, counterAlign: node.counterAxisAlignItems, alignContent: node.layoutWrap === 'WRAP' ? node.counterAxisAlignContent : undefined });
125
+ // Built from the track sizes: gridColumnSizingCSS can lag a write within the same script.
126
+ const css = (sizes) => sizes.map(t => t.type === 'FIXED' ? t.value + 'px' : t.type === 'HUG' ? 'auto' : (t.value === undefined ? 1 : t.value) + 'fr').join(' ');
127
+ if (node.layoutMode === 'GRID') out.grid = { columns: css(node.gridColumnSizes), rows: css(node.gridRowSizes), columnCount: node.gridColumnCount, rowCount: node.gridRowCount, columnGap: node.gridColumnGap, rowGap: node.gridRowGap, autoRows: node.gridAutoTracks === 'ROWS', autoFlow: node.gridItemsPositioning === 'ROW_AUTO_FLOW' };
128
+ if (children.length) out.children = children;
129
+ return out;
130
+ `;
131
+ export function layoutScript(payload) {
132
+ return `const P = ${JSON.stringify(payload)};\n${SET_LAYOUT_BODY}`;
133
+ }
134
+ const sizing = z.enum(["FIXED", "HUG", "FILL"]);
135
+ const gridAlign = z.enum(["MIN", "CENTER", "MAX", "AUTO"]);
136
+ export function registerLayoutTools(server, getDesktopConnector) {
137
+ server.tool("figma_set_layout", "Set a frame's auto layout or grid layout in one call, and place its children. Auto layout: mode HORIZONTAL or VERTICAL, gap, padding, primaryAlign (MIN, CENTER, MAX, SPACE_BETWEEN, SPACE_EVENLY, SPACE_AROUND), counterAlign, wrap with counterAxisGap and alignContent, and sizing (FIXED, HUG, FILL). Grid: mode GRID with tracks written CSS-style (grid.columns: '240px 1fr 1fr', 'repeat(3, 1fr)', 'auto 1fr', or a count), gaps, autoRows, autoFlow, and per-child row, column, spans and alignment. Only the fields you pass change. Returns the resulting layout, with grid tracks as CSS. Requires Desktop Bridge.", {
138
+ nodeId: z.string().describe("Frame, component or instance to lay out"),
139
+ mode: z.enum(["NONE", "HORIZONTAL", "VERTICAL", "GRID"]).optional(),
140
+ gap: z.number().min(0).optional().describe("Space between items. On a grid, sets both row and column gaps"),
141
+ counterAxisGap: z.number().min(0).optional().describe("Space between wrapped rows or columns (wrap only)"),
142
+ padding: z
143
+ .union([
144
+ z.number().min(0),
145
+ z.object({ top: z.number().min(0).optional(), right: z.number().min(0).optional(), bottom: z.number().min(0).optional(), left: z.number().min(0).optional() }),
146
+ ])
147
+ .optional()
148
+ .describe("One number for all sides, or {top, right, bottom, left}"),
149
+ primaryAlign: z.enum(["MIN", "CENTER", "MAX", "SPACE_BETWEEN", "SPACE_EVENLY", "SPACE_AROUND"]).optional(),
150
+ counterAlign: z.enum(["MIN", "CENTER", "MAX", "BASELINE"]).optional(),
151
+ wrap: z.boolean().optional().describe("Wrap children onto new lines (HORIZONTAL or VERTICAL)"),
152
+ alignContent: z.enum(["AUTO", "SPACE_BETWEEN"]).optional().describe("How wrapped lines are spaced"),
153
+ reverseZIndex: z.boolean().optional().describe("Stack earlier children on top (canvas stacking)"),
154
+ width: z.number().positive().optional(),
155
+ height: z.number().positive().optional(),
156
+ horizontalSizing: sizing.optional(),
157
+ verticalSizing: sizing.optional(),
158
+ grid: z
159
+ .object({
160
+ columns: z.union([z.string(), z.number().int().min(1)]).optional().describe("'1fr 2fr 120px', 'repeat(4, 1fr)', 'auto 1fr' (auto = hug), or a count of equal columns"),
161
+ rows: z.union([z.string(), z.number().int().min(1)]).optional().describe("Same format as columns. Leave out with autoRows"),
162
+ columnGap: z.number().min(0).optional(),
163
+ rowGap: z.number().min(0).optional(),
164
+ autoRows: z.boolean().optional().describe("Add and remove rows automatically as children are added"),
165
+ autoFlow: z.boolean().optional().describe("Place children in order, left to right then top to bottom, instead of by row and column"),
166
+ })
167
+ .optional(),
168
+ children: z
169
+ .array(z.object({
170
+ nodeId: z.string().describe("A child, or a node to move into this frame"),
171
+ row: z.number().int().min(0).optional().describe("Grid row (0-based)"),
172
+ column: z.number().int().min(0).optional().describe("Grid column (0-based)"),
173
+ rowSpan: z.number().int().min(1).optional(),
174
+ columnSpan: z.number().int().min(1).optional(),
175
+ horizontalAlign: gridAlign.optional().describe("Alignment inside its grid cell"),
176
+ verticalAlign: gridAlign.optional().describe("Alignment inside its grid cell"),
177
+ horizontalSizing: sizing.optional(),
178
+ verticalSizing: sizing.optional(),
179
+ positioning: z.enum(["AUTO", "ABSOLUTE"]).optional().describe("ABSOLUTE takes the child out of the layout flow"),
180
+ }))
181
+ .optional(),
182
+ fileKey: z
183
+ .string()
184
+ .optional()
185
+ .describe("Local Mode only. Run against this specific connected file instead of the active file. Get connected fileKeys from figma_list_open_files. Rejected in Cloud Mode, which pairs with a single plugin instance."),
186
+ }, async ({ fileKey, grid, ...rest }) => {
187
+ try {
188
+ const payload = { ...rest };
189
+ if (grid) {
190
+ payload.grid = {
191
+ ...grid,
192
+ columns: grid.columns !== undefined ? parseGridTracks(grid.columns) : undefined,
193
+ rows: grid.rows !== undefined ? parseGridTracks(grid.rows) : undefined,
194
+ };
195
+ }
196
+ const connector = await getDesktopConnector();
197
+ const result = await connector.executeCodeViaUI(layoutScript(payload), 30000, fileKey);
198
+ if (!result?.success)
199
+ throw new Error(result?.error || "Failed to set layout");
200
+ return { content: [{ type: "text", text: JSON.stringify(result.result) }] };
201
+ }
202
+ catch (error) {
203
+ logger.error({ error }, "figma_set_layout failed");
204
+ return {
205
+ content: [{ type: "text", text: JSON.stringify({ error: error instanceof Error ? error.message : String(error) }) }],
206
+ isError: true,
207
+ };
208
+ }
209
+ });
210
+ }
211
+ //# sourceMappingURL=layout-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout-tools.js","sourceRoot":"","sources":["../../src/core/layout-tools.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Figma Shader tools (Plugin API, beta: Update 130+).
3
+ *
4
+ * Shaders are created in Figma (UI, or Figma's own MCP). The Plugin API can
5
+ * apply them by id and set their properties. Applied shaders carry
6
+ * `propertyMetadata` (author-defined names and types per property key), so
7
+ * these tools let callers set properties by name ("turbulence") instead of
8
+ * the opaque uniform keys ("3561834497:1861196824").
9
+ *
10
+ * Both tools build a Plugin API script and run it through executeCodeViaUI,
11
+ * so no Desktop Bridge plugin change (or re-import) is needed.
12
+ */
13
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
+ export declare const SHADER_TARGETS: readonly ["fills", "strokes", "effects"];
15
+ export declare const LIST_SHADERS_BODY = "\nconst results = new Map();\nconst limitUses = 20;\nconst describe = (meta) => Object.entries(meta || {}).map(([key, d]) => ({ key, name: d.name, type: d.type, defaultValue: d.defaultValue, description: d.description }));\nconst scan = (node, page) => {\n for (const target of ['fills', 'strokes', 'effects']) {\n const arr = node[target];\n if (!Array.isArray(arr)) continue;\n arr.forEach((p, index) => {\n if (!p || p.type !== 'SHADER') return;\n let entry = results.get(p.id);\n if (!entry) {\n entry = { id: p.id, kind: target === 'effects' ? 'effect' : 'fill', usageCount: 0, usedIn: [], properties: describe(p.propertyMetadata) };\n results.set(p.id, entry);\n }\n entry.usageCount++;\n if (entry.usedIn.length < limitUses) entry.usedIn.push({ nodeId: node.id, name: node.name, page: page.name, target, index });\n });\n }\n};\nlet pages;\nif (P.nodeId) {\n const root = await figma.getNodeByIdAsync(P.nodeId);\n if (!root) throw new Error('Node not found: ' + P.nodeId);\n let page = root; while (page && page.type !== 'PAGE') page = page.parent;\n scan(root, page || figma.currentPage);\n if ('findAll' in root) for (const n of root.findAll(() => true)) scan(n, page || figma.currentPage);\n} else {\n pages = P.scope === 'file' ? figma.root.children : [figma.currentPage];\n for (const page of pages) {\n if (page !== figma.currentPage) await page.loadAsync();\n for (const n of page.findAll(() => true)) scan(n, page);\n }\n}\nlet available = [];\nlet availableNote;\ntry {\n const list = await Promise.race([figma.listAvailableShaders(), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000))]);\n available = list.map(s => ({ id: s.id, name: s.name, kind: s.type, imported: s.imported, inFile: results.has(s.id), properties: describe(s.propertyDefinitions) }));\n} catch (e) {\n availableNote = e && e.message === 'timeout'\n ? \"Figma didn't return the library shader list in time. This happens when the file's window isn't the focused one.\"\n : 'Could not list library shaders: ' + (e && e.message ? e.message : e);\n}\nconst out = { scope: P.nodeId ? 'node' : P.scope, inFile: Array.from(results.values()), available };\nif (availableNote) out.availableNote = availableNote;\nif (out.inFile.length > 0) out.note = 'Figma drops a shader from the file when nothing uses it and the file reloads. Keep at least one layer using each shader you want to reuse.';\nreturn out;\n";
16
+ export declare const APPLY_SHADER_BODY = "\nconst node = await figma.getNodeByIdAsync(P.nodeId);\nif (!node) throw new Error('Node not found: ' + P.nodeId);\nconst target = P.target;\nif (!(target in node) || !Array.isArray(node[target])) throw new Error(node.type + ' nodes have no ' + target);\nconst original = node[target].map(p => JSON.parse(JSON.stringify(p)));\nconst clone = (v) => JSON.parse(JSON.stringify(v));\n\nlet index;\nlet paint;\nif (P.index !== undefined) {\n const existing = original[P.index];\n if (!existing || existing.type !== 'SHADER') throw new Error(target + '[' + P.index + '] is not a shader. Omit index to add a new shader.');\n index = P.index;\n paint = clone(existing);\n if (P.shaderId && P.shaderId !== existing.id) { paint = { type: 'SHADER', id: P.shaderId }; }\n} else {\n if (!P.shaderId) throw new Error('Pass shaderId to add a shader, or index to change one already applied.');\n paint = { type: 'SHADER', id: P.shaderId };\n index = P.replace ? 0 : original.length;\n}\nif (target === 'effects') paint.visible = P.visible !== undefined ? P.visible : (paint.visible !== undefined ? paint.visible : true);\nelse if (P.visible !== undefined) paint.visible = P.visible;\nif (target !== 'effects') {\n if (P.opacity !== undefined) paint.opacity = P.opacity;\n if (P.blendMode !== undefined) paint.blendMode = P.blendMode;\n}\n\nconst strip = (p) => { const c = clone(p); delete c.propertyMetadata; return c; };\nconst place = (p) => {\n const next = P.replace && P.index === undefined ? [] : original.map(strip);\n next[index] = strip(p);\n return next;\n};\nconst write = async (p) => {\n try { node[target] = place(p); }\n catch (e) {\n const msg = e && e.message ? e.message : String(e);\n if (/not imported/i.test(msg)) {\n await figma.importShaderById(p.id);\n node[target] = place(p);\n return;\n }\n if (/Invalid shader id|Expected an id/i.test(msg)) {\n throw new Error(msg + (target === 'effects'\n ? ' Effect shaders and fill shaders are separate kinds: a fill shader goes on fills or strokes.'\n : ' Use an id from figma_list_shaders. A shader nothing in the file uses is dropped when the file reloads.'));\n }\n throw e;\n }\n};\n\n// Apply first (properties keep their current or default values) so Figma\n// returns propertyMetadata, then map names to keys and write the changes.\nawait write(paint);\nconst applied = node[target][index];\nconst meta = applied.propertyMetadata || {};\nconst props = clone(applied.properties || {});\nconst byName = {};\nfor (const [key, d] of Object.entries(meta)) byName[d.name] = key;\nconst requested = P.properties || {};\nconst unknown = [];\nfor (const [name, raw] of Object.entries(requested)) {\n const key = meta[name] ? name : byName[name];\n if (!key) { unknown.push(name); continue; }\n const type = meta[key].type;\n let value = raw;\n if (value && typeof value === 'object' && value.variableId) value = { type: 'VARIABLE_ALIAS', id: value.variableId };\n else if (type === 'COLOR' && typeof value === 'string') value = figma.util.rgba(value);\n else if (type === 'COLOR' && value && typeof value === 'object' && value.a === undefined && 'r' in value) value = { r: value.r, g: value.g, b: value.b, a: 1 };\n props[key] = value;\n}\nif (unknown.length) {\n node[target] = original.map(strip);\n throw new Error('Unknown shader propert' + (unknown.length > 1 ? 'ies' : 'y') + ': ' + unknown.join(', ') + '. This shader has: ' + Object.values(meta).map(d => d.name + ' (' + d.type + ')').join(', ') + '. Nothing was changed.');\n}\nif (Object.keys(requested).length) {\n try { await write(Object.assign(strip(applied), { properties: props })); }\n catch (e) { node[target] = original.map(strip); throw new Error('Figma rejected the property values: ' + (e && e.message ? e.message : e) + ' Nothing was changed.'); }\n}\nconst final = node[target][index];\nconst named = {};\nfor (const [key, v] of Object.entries(final.properties || {})) named[meta[key] ? meta[key].name : key] = v;\nreturn {\n nodeId: node.id, name: node.name, target, index, shaderId: final.id,\n properties: named,\n definitions: Object.entries(meta).map(([key, d]) => ({ key, name: d.name, type: d.type, defaultValue: d.defaultValue })),\n};\n";
17
+ export declare function shaderScript(payload: unknown, body: string): string;
18
+ export declare function registerShaderTools(server: McpServer, getDesktopConnector: () => Promise<any>): void;
19
+ //# sourceMappingURL=shader-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shader-tools.d.ts","sourceRoot":"","sources":["../../src/core/shader-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAazE,eAAO,MAAM,cAAc,0CAA2C,CAAC;AAEvE,eAAO,MAAM,iBAAiB,o8EAgD7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,gqIAwF7B,CAAC;AAEF,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnE;AA8CD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,IAAI,CA4CpG"}