# figma-bridge-mcp A local MCP server that lets AI assistants inspect, create, and update designs in **Figma Desktop**. It connects through a small Figma development plugin and exposes focused tools for screenshots, design specs, JSX rendering, tokens, assets, components, FigJam, and Figma Slides. Everything runs on `127.0.0.1`. No Figma Personal Access Token required. No cloud. No binary patching of the Figma app. An [optional REST add-on](#rest-add-on-optional) adds version history, comments, and published-library metadata. Its Figma token stays on your machine and is never placed in your MCP client configuration or chat. Requirements: Node.js 18 or newer, Figma Desktop, and an MCP client that can start local stdio servers. ### Codex, Claude Code, and Cursor: MCP and skills in one bundle Figma Bridge ships three focused shared skills plus thin plugin adapters for all three clients: - `figma-bridge-design-to-code` — exact Figma implementation in the target stack; - `figma-bridge-code-to-figma` — semantic, componentized screens from code; - `figma-bridge-component-library` — tokens, styles, components, variants and properties. | Client | Plugin format | Full install path | | --------------- | --------------------------------- | ------------------------------------------------ | | Codex / ChatGPT | `.codex-plugin/plugin.json` | This repository's Codex marketplace | | Claude Code | `.claude-plugin/plugin.json` | This repository's Claude marketplace | | Cursor | Agent Plugins 1.0 (`plugin.json`) | GitHub-backed team marketplace or local checkout | The adapters all discover the same `skills/` directory and start the same local MCP package. Users do **not** download or maintain the skills separately. For Codex, add this repository as a marketplace and install the bundle: ```bash codex plugin marketplace add KaiUweHella/figma-bridge-mcp codex plugin add figma-bridge-mcp@figma-bridge ``` Start a **new Codex task** after installation; plugin MCP servers and their tools are discovered when a task starts. Figma Bridge is a local STDIO server, so Codex may label its MCP auth as **Unsupported**. That label only means STDIO does not offer Codex's OAuth/Bearer login flow; the Figma connection still uses its own authenticated localhost handshake. Once the server is loaded, Codex discovers these 12 tools dynamically: `figma_connect`, `figma_status`, `figma_pairing`, `figma_run`, `figma_render`, `figma_selection`, `figma_history`, `figma_comments`, `figma_inspect`, `figma_reference`, `figma_screenshot`, and `figma_spec`. If a task still shows **Tools: (none)**, the server did not start in that task; it is not a static plugin capability list. This is a GitHub-hosted repository marketplace, not a submission to the universal OpenAI plugin directory. The catalog follows the repository, while each released plugin entry pins an exact `v` Git tag and starts the matching npm runtime version. `main` and `@latest` therefore cannot silently move an installed skill bundle onto a different server contract. For Claude Code, add this repository as a marketplace and install the bundle: ```bash claude plugin marketplace add KaiUweHella/figma-bridge-mcp claude plugin install figma-bridge-mcp@figma-bridge ``` Start a new Claude Code session after installing or updating the plugin, then open `/mcp`. The `figma-bridge` server should be **connected** and show 12 tools: `figma_connect`, `figma_status`, `figma_pairing`, `figma_run`, `figma_render`, `figma_selection`, `figma_history`, `figma_comments`, `figma_inspect`, `figma_reference`, `figma_screenshot`, and `figma_spec`. Claude namespaces MCP tools internally (for example, `mcp__figma-bridge__figma_connect`) and resolves their schemas at runtime. It is therefore normal for `claude plugin details figma-bridge-mcp@figma-bridge` to report `tool schemas resolved at runtime; not counted`; `/mcp` is the source of truth for the live tool count. Choose either the full Claude plugin above or the MCP-only fallback below; do not register both under the same `figma-bridge` server name. Claude gives an existing manual MCP registration precedence and suppresses the plugin's copy, so a stale manual command can make a healthy plugin appear to have no tools. If `/mcp` reports a failed or zero-tool server, run `claude mcp get figma-bridge`. When it reports a manual project, local, or user configuration, use the scope-specific removal command it prints, start a new session, and check `/mcp` again. If `/mcp` reports `CONNECTION_CLOSED` and the debug output says `No version is set for command node`, the process exited before the MCP handshake. In the same shell environment that launches the MCP client, verify: ```bash command -v node command -v npx node --version npx --version ``` Both version commands must succeed and Node must be 18 or newer. If the command paths resolve to `asdf` shims without an active Node version, configure one in `asdf`; if `nvm` should own Node instead, place its active Node directory ahead of stale `asdf` shims in `PATH`. Restart the MCP client after correcting the runtime environment. The Claude marketplace uses the same pinned GitHub release and shared skill tree. The matching npm package must be published before users install that release because the plugin launches its local stdio server through `npx`. Its public plugin source is an explicit HTTPS Git URL, so installing or updating it does not require SSH keys or a GitHub `known_hosts` entry. For Cursor Teams or Enterprise, import this GitHub repository into a team marketplace and install **Figma Bridge** from Customize. Individual users and contributors can use the same GitHub source without a central Cursor listing: clone the tagged release, link that checkout into Cursor, and reload the window: ```bash mkdir -p ~/.cursor/plugins/local ln -s /absolute/path/to/figma-bridge-mcp ~/.cursor/plugins/local/figma-bridge-mcp ``` Cursor detects the root Agent Plugin manifest and loads both the skill and MCP server. Clients without plugin or Agent Skill support keep using the normal server configuration below. They still receive the compact mandatory workflow through MCP instructions, the user-invoked `design-to-code`, `code-to-figma`, and `create-figma-component` MCP prompts, and `figma_reference {name:"workflow"}`. ## Quick start ### 1. Add the MCP server (MCP-only fallback) Use this when the full plugin install is unavailable or you only want the MCP tools without the bundled skill. Do not add this fallback when the full plugin is already installed. The `npx` setup needs no clone or build step. For Claude Code: ```bash claude mcp add figma-bridge -- npx -y figma-bridge-mcp@latest ``` For another MCP client, add the equivalent server configuration: ```json { "mcpServers": { "figma-bridge": { "command": "npx", "args": ["-y", "figma-bridge-mcp@latest"] } } } ``` Restart the MCP client if it does not discover the server immediately. There is intentionally no `env` block: the bridge creates its local credentials during pairing.
From source (contributors) ```bash git clone https://github.com/KaiUweHella/figma-bridge-mcp.git cd figma-bridge-mcp npm install ``` ```json { "mcpServers": { "figma-bridge": { "command": "node", "args": ["/absolute/path/to/figma-bridge-mcp/src/server.js"], "env": { "FIGMA_BRIDGE_PROFILE": "local-development" } } } } ``` Use the `local-development` profile when a released Bridge is running at the same time (for example from Claude Code). It pins the source checkout to `localhost:3460` and isolates its daemon, tokens, pairing and audit data below `~/.figma-bridge-mcp/profiles/local-development/`. `figma_connect` generates the gitignored, checkout-owned `plugin/local/manifest.json` bundle; import that exact file and open **Figma Bridge Local Development**. You can also refresh both checkout import bundles explicitly with `npm run prepare:plugin-imports`. The main Plugin passes that manifest identity's exact port to the shared UI, while Figma's manifest policy also permits only `:3460`. Omitting `FIGMA_BRIDGE_PROFILE` retains the normal released profile and its 3456–3460 fallback behavior.
### 2. Pair each Figma editor once 1. Ask your AI assistant to **connect to Figma**, or call `figma_connect` directly. It starts the local bridge and returns an access key plus a plugin manifest path. 2. In **Figma Desktop**: `Plugins → Development → Import plugin from manifest…` and choose **`~/.figma-bridge-mcp/plugin/manifest.json`** (the path returned by `figma_connect`). 3. Open `Plugins → Development → Figma Bridge` in Figma Design, paste the access key, and click **Save & connect**. 4. Figma stores plugin data separately for Design, FigJam and Slides. To use another editor, open the plugin there and save the same key once. In an already paired editor, **Setup → Copy access key** copies the in-use key without displaying or logging it. 5. When the plugin shows **Connected (authenticated)**, the assistant can work with that Figma file. The pairing is remembered. Starting the configured MCP automatically wakes a daemon that exited after its idle timeout and reuses a healthy daemon/socket, so a new AI session does not interrupt the open Figma file. If the automatic wake fails, call `figma_connect` once before deeper diagnosis. If the only socket is open but its plugin iframe no longer answers a read-only probe, `figma_connect` reloads just that iframe and it reconnects with the stored key. If a timed-out compatibility command leaves only the execution lane uncertain while health reads still answer, call `figma_connect` with `{forceRecovery:true,fileKey:""}`. This explicitly reloads only that authenticated Plugin UI; it refuses an unknown file and never reloads while another Figma operation is still active. The plugin keeps looking for the local bridge if it was opened first. It uses a small bounded WebSocket burst for instant startup, then a quiet localhost health probe every three seconds and opens a socket only after the daemon is present. Use the reload icon to force one immediate scan. After an npm upgrade, `figma_connect` refreshes the files at the stable path above, but Figma may keep the previously imported build in its application cache. `figma_status` detects that mismatch. Re-import the same `manifest.json` path once when it reports an older plugin build; the saved access key is retained. ### Figma Dev Mode requires its own manifest.json bundle The normal `manifest.json` does not work in Dev Mode. Figma does not support combining the existing FigJam editor target with `dev` in one manifest: - Import `~/.figma-bridge-mcp/plugin/dev-mode/manifest.json` for **Figma Bridge Dev Mode**. It keeps the authenticated MCP bridge connected for selection, inspection, specs and exports. Dev Mode is read-only, so rendering and canvas edits still require switching the file to Design mode and opening the normal **Figma Bridge** plugin there. ### 3. Use it with Figma Select a frame or layer in Figma and describe the outcome you want. For example: - "Inspect my current selection and explain its layout." - "Create a settings card next to the selected frame." - "Export the selected screen's tokens and assets into this project." - "Implement the selected frame, then compare the result with Figma." The assistant can read the current selection, capture screenshots and specs, render JSX, export assets, or apply targeted edits. Keep the Figma Bridge plugin open in every document the assistant should access. If more than one document is connected, pass a Figma URL or file key so the target is unambiguous. ## REST add-on (optional) Everything above works with **zero Figma credentials**. Three things the local plugin bridge structurally cannot reach live behind Figma's REST API, and can be unlocked with a personal access token: | Feature | What it adds | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Version history** | `figma_history {includeVersions:true}` merges what _designers_ saved (when, by whom) into the local audit+git timeline — the plugin API can only _write_ versions, not read them. `figma_history {diff:{from:"version:…", to:"version:…"}}` goes further and diffs the documents themselves. | | **Comments** | `figma_comments` reads design-review feedback (with node anchors and thread ids) and can reply. Posting always shows a preview first and requires `confirm:true` — comments are visible to other people. | | **Library metadata** | `map storybook` automatically enriches `figma-map.json` with the published components' `description` and documentation links — a far stronger matching signal than name normalization. | **Enabling it — the token never leaves your machine:** 1. Create a personal access token in Figma (Settings → Security → Personal access tokens) with scopes: **File content (read)**, **File versions (read)**, **Comments (read and write)**. _Current user (read)_ is optional — it only makes `figma_status` show your handle. 2. Open the **Figma Bridge plugin** in Figma Desktop, connect, expand **Setup**, paste the token under **Figma REST token**, and select **Save token**. The plugin's **Save history** button becomes available after the token is stored. 3. `figma_status` reports that the token is configured without making a remote request. Run `figma_status {validateRest:true}` when you want an explicit validity check; it reports your handle or verifies file access when the optional _Current user_ scope is absent. The token travels from the plugin over the **authenticated localhost WebSocket** to the daemon, which stores it in `~/.figma-bridge-mcp/rest-token` (mode 0600). It is never entered in chat, never stored in your MCP client config, never echoed back by any tool, and never written to the audit log (REST calls retain only the allowlisted endpoint capability, read/write class, and non-reversible summaries). _Clear token_ in the plugin removes the file. Headless/CI alternative: set the `FIGMA_REST_TOKEN` environment variable — it overrides the file. **Scope:** by default REST calls target the file currently open in Figma Desktop (the plugin pushes its file key). Other files require an explicit `fileKey` parameter (bare key or full Figma URL). Note that a PAT itself can read every file its account can access — keep the scopes minimal. The REST client is a closed internal allowlist, not a generic HTTP escape hatch. It permits token health, version lists, version-pinned document contents, comments, and file-wide published-component metadata. A bare current file fetch and all node/CSS/export/variable/style/Dev-Resource endpoints are rejected before the token is read or the network is touched; those operations must use the local Plugin API commands above. ## How it works ``` MCP client ──stdio──▶ figma-bridge-mcp (src/) │ MCP tool adapters ─▶ Capability Catalog ─▶ CommandPlan │ ┌───────────────────────┴──────────┐ Command Application Modules generic CLI adapter │ │ Design Capture │ Asset Policy │ └──────┬─────┘ Daemon Client Module │ HTTP: signed requests ▼ local daemon :3456–3460 │ WS: challenge/response ▼ Figma Bridge plugin in Figma Desktop ``` - The **engine** lives under `engine/`. It began as a fork of `figma-ds-cli` v2.1.0 and has diverged well past it (see [attribution](#inspiration--attribution)). The Chrome-DevTools "Yolo mode" — which patches the Figma app binary — was removed entirely; there is no code path to it. - Specialized MCP reads (`figma_spec`, `figma_inspect`, `figma_screenshot`) and migrated `figma_run` actions execute directly through value-returning **Command Application Modules**. `export assets`, `node set-text`, `create frame|rect|ellipse|polygon|star`, bounded `node tree`, current-page Node Discovery through `find`, direct `node bindings`, native Inspect `node css`, exact Prototype Inspection, Component Identity Facts, Style Facts through `style show`, bounded Style Catalog pagination through `style list`, Layout Grid Facts through `grid list`, paginated Style Consumer Facts through `style consumers`, Variable Facts through `var show`, Design Link set/inspect, and Design Contract capture/check use the same Implementation from MCP and Commander. Other `figma_run` actions remain on the deliberately broad child-process compatibility Adapter while they migrate one vertical slice at a time. One **Daemon Client Module** owns signing, timeouts and transport errors for both paths. - The **Design Capture Module** walks an explicit node once and locally projects structure, style and the lossless output formats from those same facts. A Capture is reused only after a cheap revision probe proves the authenticated plugin connection and Figma document revision are unchanged. Missing or unstable revision metadata disables reuse; selection and named-section calls remain uncached in this first Slice. Captures distinguish authored Figma Auto Layout/Grid, Figma's marked `inferredAutoLayout` heuristic and geometry fallback. They also preserve Code-to-Figma semantic/fallback metadata separately from later native Figma annotations, plus full component and variable-mode contracts. - The **Design Link Registry** gives a component, screen or frame one durable, repository-owned **Design Entity** id. `figma-bridge.json` holds portable code/Storybook/Figma links; Figma plugin data holds only the same id and kind. This dual anchor lets future agents resolve the exact existing component from either side without putting repository paths into a Figma document. - The report-only **Round-trip Planner** compares current code and the current normalized Figma subtree with an explicitly **Accepted Design Baseline**. The **Project Design Context** projects that status, the entity links and the exact next reads through one in-process Command Application. When semantic paths exist, changed subtrees are reported with their current node ids; plugin markers themselves never count as visual changes. - A **Design Contract** turns one linked Design Entity's complete Design Capture into a deterministic repository gate. Run `figma_run ["contract", "capture","ui.button"]` once and review the JSON; later `figma_run ["contract","check","ui.button"]` reports canonical drift and separately enforces variant matrices, token-binding floors, geometry tolerances and prototype transitions. Volatile Figma handles are ignored and depth-limited captures are refused. - A versioned **Round-trip Fidelity Contract** classifies each core Figma fact independently for Code → Figma and Figma → code, including its concrete implementation and verification seam. CI fails when a core fact or direction is unclassified; inspect the current projection with `figma_reference {name:"fidelity"}`. - One **Capability Catalog** resolves every Figma Command entering through MCP into an immutable plan before either execution adapter runs it. That plan is the single source for exposure, Figma/workspace/external-network/shared-local-state effects, target need, confirmation, normalized paths, retry, timeout, accepted exit codes and background-job identity. Unknown commands default to denied/write/no-retry. - One **Workspace I/O** module confines Command Capability file effects to the MCP launch directory by default. Paths are checked after symlink resolution, opened files are revalidated before bytes are read or written, and structured whole-file inputs have byte, nesting, collection and YAML-alias limits. This does not narrow normal Figma-to-code or code-to-Figma work inside the project. For a legitimate second repository, add canonical roots explicitly with `FIGMA_BRIDGE_EXTRA_ROOTS` (use the platform path-list separator: `:` on macOS/Linux, `;` on Windows). The launch directory always remains allowed. - One immutable **Figma Target Context** resolves explicit `fileKey`, a pasted Figma URL from that command's documented node field, a Design Link Registry, or implicit single-window targeting once per command and then accompanies planning, audit, job identity and daemon execution. URLs in text, JSX, names, annotations, paths, labels, and remote assets remain payload and cannot retarget a command. One shared **Asset Policy** classifies image fills, vector art and vector clusters for both Design Capture projections and export. - One versioned **Dispatch Receipt** binds every authenticated Plugin action to the selected connection epoch, file or draft session, expected editor and advertised operation capability. The Plugin validates it immediately before execution and echoes the same identity with a terminal state; receipt identities never enter Audit or user-facing output. - One deadline-aware **Plugin Write Lane** serializes conservative writes per resolved document session before dispatch. Different files continue in parallel; queued expiry or caller cancellation never reaches Figma, while an unknown dispatched outcome quarantines only its target until the Plugin main thread replays the exact settlement receipt. Aggregate queue state is visible without exposing file, connection or receipt identities. - Runtime protocol validators reject malformed HTTP execution payloads and plugin frames at the transport boundary. TypeScript checks the JavaScript seams (including the Figma plugin), while deterministic context, payload and median and tail latency budgets catch architectural regressions in CI without treating brief shared-runner scheduling pauses as a sustained regression. - The **daemon** brokers commands to the Figma plugin over a localhost WebSocket. Two gates protect it: - **HTTP routes** (`/health`, `/exec`) require a per-request HMAC signature keyed with the session token, a 0600 file — the token itself never crosses the wire. - **The plugin WebSocket** (`/plugin`) requires the **access key**: an `Origin`/`Host` allowlist plus a **mutual challenge-response handshake** in which the key is only ever an HMAC secret and never crosses the wire either. This closes the upstream gap where _any_ local process could connect to the plugin socket and run code in your Figma document — and the inverse gap, where anything answering on a local port could drive an honest plugin. - The unauthenticated `GET /plugin-ready` route is a CORS-scoped discovery beacon containing only bridge identity, readiness and port. It carries no file, token, version or connection data; every command route remains signed and the discovered WebSocket must still complete the mutual handshake. ## Tools | Tool | Purpose | | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `figma_connect` | Ensure Safe Mode is available without replacing a healthy daemon/socket; self-heal a single unresponsive plugin iframe; explicitly recover one exact split execution lane with `{forceRecovery:true,fileKey}`; generate/show the access key and print plugin setup steps. | | `figma_status` | Report local daemon/plugin/file/key state and, by default, run one bounded closed Figma main-thread responsiveness probe per target; `probePlugin:false` checks sockets only and `validateRest:true` explicitly checks the optional REST token. | | `figma_pairing` | Show the access key; `{rotate:true}` generates a fresh one. | | `figma_run` | Run a Capability Catalog-approved engine command; discover them with `figma_reference {name:"capabilities"}`. | | `figma_render` | Render JSX into the open Figma design. | | `figma_inspect` | Inspect a node by id: geometry, fills/strokes/effects, clip, opacity (YAML). | | `figma_screenshot` | Save a PNG of a node/selection to a temp file (path + dimensions + applied scale returned). | | `figma_spec` | Design-to-code spec of one node or a bounded `nodeIds[]` batch: real content, component names, tokens, vector-art refs, clip/abs — multiple same-scope reads can share one Manual Mode approval. | | `figma_reference` | Offline Figma Plugin API reference (`api setup` once builds private state from the exact official typings with no network); `{name:"capabilities"}` lists commands and `{name:"fidelity"}` projects both workflow directions and their explicit boundaries without starting Figma. | | `figma_history` | Private local capability history — filter by `nodeId` through redacted references, optionally merge `git log` of generated code files and (REST add-on) Figma version history via `includeVersions:true`. Or pass `diff:{from,to}` for a structural document diff. New records retain no raw commands, JSX, labels, file keys, paths, REST bodies, or errors; bounded legacy records remain readable. | | `figma_selection` | The user's current selection in Figma (ids, names, types, sizes) — pushed live by the plugin. Instances resolve to their stable publish `key`; linked nodes show their Design Entity, code file and Storybook story. | | `figma_comments` | REST add-on: read design-review comments (`action:"list"`) or post/reply (`action:"post"` — always previews first, needs `confirm:true`). | Node ids are accepted in every form a user has at hand: `12:34`, the URL form `12-34`, or a full Figma URL (whose file key is checked against the files you actually have open — see [Several files at once](#several-files-at-once)). `figma_run` with `["node","tree",...]` is a fast bounded outline for discovery and follow-up node ids. It reports depth/output truncation explicitly; use `figma_spec` plus a screenshot for exact Figma-to-code implementation facts. An explicit detached node handle is rejected by `node tree`: Figma can retain direct-id handles after a node leaves the canvas, so handle availability is not present-tense hierarchy evidence. `figma_run` with `["node","delete","12:34",...]` reports `removed`, `already-detached`, or `not-found` per target and verifies every removal against the target's former parent after first proving a complete parent-membership chain to the DOCUMENT root. Nested deletion batches remain supported and execute deepest-first. If Figma returns a 1-pixel PNG axis for a node whose scaled geometry is larger, the Screenshot Command rejects the image before saving it and reports both the actual raster and logical geometry. Refresh the identity with `figma_run {args:["component","list"]}`, retry the exact current Variant id, or capture its parent Component Set. This avoids accepting a stale or unsettled Variant export as visual evidence. The documented `figma_run ["export","node",…]` and `figma_run ["export","screenshot",…]` names remain available. Their PNG mode is a compatibility alias of the same Screenshot Command Application, so it uses one bounded capture, validates the PNG before writing through Workspace I/O, and reports dimensions from the encoded raster. JPG, SVG and PDF use a separate Plan-bound Image Export Application. It validates bounded canonical transport and the requested JPEG, SVG or PDF container before Workspace I/O; these formats remain available without being misrepresented as verification PNGs. Write commands can be gated behind an explicit `confirm:true` by setting `FIGMA_WRITE_CONFIRM=1` in the server's environment. The gate works on subcommand level: reads like `node tree` or `component list` pass freely, mutations like `node delete`, `combos`, or `tokens spacing` require confirm. `figma_status` reports whether that policy is currently `required` or `disabled`. Omitting `confirm` or passing `confirm:false` is not a portable dry run: when the policy is disabled, a valid write executes immediately. Use `preview:true` with `figma_run` or `figma_render` whenever the request must be guaranteed non-mutating. The Bridge validates the request and returns a payload-free Command Plan without dispatching it; `preview:true` also wins when `confirm:true` is present. The engine CLI remains direct execution and uses only each command's explicitly documented `--dry-run` option. Workspace file access is zero-configuration for the project from which the MCP server is launched. For a multi-project workflow, configure additional roots on the server process, for example on macOS/Linux: ```json { "env": { "FIGMA_BRIDGE_EXTRA_ROOTS": "/work/shared-design-system:/work/second-app" } } ``` Use `;` between roots on Windows. Relative extra roots resolve from the launch workspace. Paths outside the launch workspace and these explicit additions are rejected without printing the private absolute path. Remote raster images in `figma_render` JSX are opt-in. Configure exact origins on the MCP/CLI host process; public sources require HTTPS, while intentional private or loopback sources use the separate private-origin setting: ```bash export FIGMA_BRIDGE_IMAGE_ORIGINS=https://images.example.com export FIGMA_BRIDGE_IMAGE_PRIVATE_ORIGINS=http://assets.internal:8080 ``` Multiple origins are comma-separated. Wildcards, URL credentials and fragments are rejected. Signed query strings may be used to retrieve an image, but they are never retained in the Render Plan, result, Audit Trail or source provenance. The host reauthorizes redirects, validates every resolved address, pins the approved connection, rejects compressed or oversized responses, verifies MIME type against PNG/JPEG/GIF/WebP bytes and embeds the content by SHA-256 identity. The Figma Plugin receives only verified bytes and remains restricted to its existing localhost Bridge domains. Local PNG/JPEG/GIF/WebP references use the same bounded asset contract. Literal quoted and braced `src`/`image` forms resolve through the configured workspace roots and converge on one content identity; empty values, runtime expressions, unsupported formats and paths outside those roots fail before any Figma mutation. Safe Mode exposes the core canvas edits as typed commands; raw `eval` remains blocked: ```text figma_run {args:["node","duplicate","12:34","--name","Copy"]} figma_run {args:["node","reparent","12:34","56:78","--index","0"]} figma_run {args:["create","text","Watermark","--parent","56:78","-x","24","-y","24"]} figma_run {args:["create","star","Badge","--points","8","--inner-radius","0.55"]} figma_run {args:["node","boolean","subtract","12:34","12:35","--name","Cutout"]} figma_run {args:["node","set","12:34","--rotation","8","--radii","4,8,12,16","--layout-mode","column","--padding","16"]} figma_run {args:["component","instantiate","12:34","--parent","56:78"]} figma_run {args:["component","prop","set","12:34","State","Active"]} figma_run {args:["gradient","apply","12:34","linear-gradient(90deg, #7c3aed, #06b6d4)","--field","stroke","--stroke-weight","1"]} ``` The read-only `canvas info` and `canvas next` commands share one targeted Canvas Awareness path across CLI and MCP. They wait for dynamic page loading, validate the returned aggregate geometry, and accept only a finite numeric gap plus the explicit `right` or `below` direction before anything reaches Figma. This keeps automatic Code-to-Figma placement useful without treating option values as executable code. `canvas pages` and `canvas page` likewise use one Page Navigation path across CLI and MCP. Page listing is a bounded safe read; switching is explicit, non-retrying, and resolves only an id, exact name, or unique substring. Use `node:` for explicit node identity or `name:` for an explicit page name; an unprefixed value remains the compatible `query:` search. Only the explicit node form may derive the target file from a Figma URL. It changes the active editor page without modifying document content, while ambiguous names fail instead of selecting a page by guesswork. The same discriminator protects union lookups in Figma Slides and `component add-variant`: `node:`, `name:` and (for Slides) `label:` select one identity surface, while bare values preserve the existing query workflow. URL-shaped names and labels therefore stay usable without silently retargeting the Command. `component add-variant --from` also restores and freshly verifies TEXT, BOOLEAN, INSTANCE_SWAP and SLOT sublayer references plus nested Instance exposure that Figma's native clone can drop. Top-level `find` provides bounded Node Discovery on the fully loaded current page across CLI and MCP. Case-insensitive partial-name matching, the optional node-type filter and a 1–200 result limit are parsed as inert data before readiness. Search stops immediately when the result limit is reached and after 100,000 visited nodes; either bound is reported so follow-up reads never rely on a silently incomplete node-id list. The mutating `canvas page-create` and `canvas page-divider` commands share a separate non-retrying Page Structure path. Duplicate page names and divider indexes are validated before creation. Optional divider names must use Figma's native all-asterisk, all-dash or all-space form so the command cannot silently create a normal page. A failure after creation triggers Command-owned cleanup and reports possible residue truthfully if Figma cannot remove it. `node duplicate` works for cloneable design nodes including Sections; `node reparent` accepts an explicit child index and preserves canvas position when the destination is not Auto Layout. `create frame|rect|ellipse|polygon|star` and `vector|slice|line|autolayout|text` use action-specific, readback-verified creation Applications across CLI and MCP. They accept an explicit parent where Figma permits it, so small children can be added to an existing frame without rebuilding it. Text creation also preflights the requested font before creating its node. ID-scoped `node group|ungroup|boolean|flatten` replace selection-dependent structural edits. Components can be created, instantiated, swapped, detached, and have their overrides reset or compacted without relying on the current selection. The direct `node set` surface covers geometry, visibility/locking, fills/strokes/effects, blend and mask settings, individual corners, constraints, Auto Layout container/child properties, polygon/star geometry, and Section visibility. The single-node command preflights all supported properties and Variable references, freshly verifies the complete result, and restores its full field journal if any setter or postcondition fails. Unsupported fields remain explicit skips, while ambiguous Variables stop before mutation. Legacy selection-based `create` subcommands remain unreachable through `figma_run`; VectorNetwork input is bounded inert JSON on its typed route. Dev Mode `measure add|edit|delete` mutations share one Plan-bound Application across CLI and MCP. Add and edit own rollback; delete reports its irreversible freshly verified outcome without retrying after dispatch. `component main ` is the bounded identity read for reuse. Commander and MCP resolve the same explicit Instance through one safe-read Command Application and return its native main component, optional set, publish keys, variants and component-property facts. A full Figma node URL may supply the target file; display names remain descriptive and are never treated as durable component identity. The operation audit is pinned to the installed official `@figma/plugin-typings` version. It exhaustively classifies every PluginAPI creator and structural method, checks every engine command against the Safe Mode Capability Catalog, and checks all official variable/easing value types. A Figma typings upgrade or newly registered command therefore fails CI until the new operation is supported or recorded as an explicit boundary. Run `figma_run {args:["api","gap"]}` for the current direct/alternative/boundary summary. Native JSX instances require durable Registry identity (`entity` plus a published `key` or local `id`). Their editable overrides use the component's real Figma structure: ```jsx ``` `prop:` resolves a component-property definition; `text:` and `fill:` resolve one named descendant. `swap:` values and INSTANCE_SWAP property values are Design Entity ids, resolved from `figma-bridge.json`; component display names are intentionally not accepted as swap identity. Missing, ambiguous or unlinked targets stop preflight before the first canvas node is created. JSX and browser capture carry only the Design Entity plus variant/override intent. Command preparation reads the Registry once and embeds a fingerprinted, target-aware binding table in the Semantic Render Plan. Published keys are preferred; an unpublished local node remains usable only when its Registry file key exactly matches the resolved Figma target. Authored `key`, `id` and `component` name handles cannot replace that binding. The generated compatibility path consumes the same table, so it does not reintroduce a second identity lookup. For raster content, `` is materialized by the host as bounded embedded bytes and stays on the native executor. Image bytes ride inside the plugin payload as base64 against a 5 MB protocol value, so one file may be at most 3.5 MB and a render measures its whole plan before dispatch: several images that each fit still share that one payload, and the refusal says so before the plugin is touched. An intentional `` remains useful, but it selects the generated compatibility executor and returns `completed-with-findings` with the exact `image needs host-materialized embedded bytes` degradation. A placeholder therefore never counts as native or pixel-exact image fidelity. The executable MCP form, Registry setup command, and current element-specific props are kept in `figma_reference {name:"workflow:code-to-figma"}`; use `figma_run {args:["render","--prop-reference","Image"]}` for the bounded Image prop reference. Dimensions and typography accept the same `var:name|fallback` form. The native executor binds width, height and min/max constraints plus font family/style, weight, size, line height, letter spacing, paragraph spacing and paragraph indent. Family/style use STRING variables; the other typography and dimension fields use FLOAT variables. A missing bound font stops preflight with an install-or-choose-another-face message instead of silently substituting it. Named Text Styles are reconciled before canvas creation as well: an explicit `style="Typography/Eyebrow"` is reused only when its complete typography matches; a conflicting same-name style stops, and otherwise exact typography is reused. Unmatched implicit typography stays literal by default instead of creating a parallel style. Set MCP `materializeResources:true` or CLI `--materialize-resources` when the source intentionally asks Figma Bridge to create generated Text Styles and spacing/radius Variables. Explicit named styles and `var:` fallbacks remain available without that flag because they already carry authored resource intent. Line height keeps its authored unit in the Semantic Render Plan: quoted unitless CSS multipliers such as `lineHeight="1.1"` and percentages such as `lineHeight="110%"` become Figma `PERCENT`; explicit `lineHeight="24px"` and numeric JSX `lineHeight={24}` remain `PIXELS`; and `lineHeight="auto"` remains `AUTO`. Negative, non-finite, or unknown values stop before mutation. Text Style matching compares both unit and value. Figma float32 metric readback is normalized for stable comparison, and family-specific faces such as DM Sans/Manrope `SemiBold` and `ExtraBold` are tried before any fallback family. Successful native renders return `textStyleReport` and `variableReport` counts for references, unique reused resources, created resources, bound properties and literals kept as literals. After a native render the text is measured back: every rendered text is matched to the plan by the semantic path the executor stamps on it, and a font family or style that differs from the requested one, or a line count that differs from what the browser measured (`render --dom-capture` carries the browser's line count beside the plan), becomes a finding — the `text-fidelity` stage in the outcome, printed after every CLI render. The native executor falls back to Inter when a family is not installed; that fallback is now visible instead of silent. Ambiguous or unsupported preflight errors include the corresponding zero/nonzero counts and do not leave newly created variables or canvas nodes behind. Command Preview reports the generated-resource policy without echoing JSX or other private payloads. `` also preserves editable inline Rich Text. Nested `/`, `/`, ``, `` and `` markup becomes native Figma ranges; HTML entities are decoded before UTF-16 range offsets are calculated. Span runs support `font`, `fontStyle`, `weight`, `italic`, `size`, `color`, `letterSpacing`, `underline`/`decoration` and safe links: ```jsx Hello{" "} bold and italic red link ``` ## Plugin window The Figma Bridge plugin window is more than the connection status: - **Compact by default** — the always-on view is a small status card plus one control row. Activity and Setup expand only when needed and close each other, so the panel does not cover the canvas after the one-time key entry. - **Save history** — the primary safety action, available after the optional REST token is configured. It writes a human-readable labeled checkpoint such as `Figma Bridge checkpoint · 21 Aug 2026, 14:05` into Figma's own version history. There is no restore API for plugins: you roll back through Figma's version history panel. - **Pause agent** — a kill switch: while paused, the plugin rejects every incoming agent command with an explicit error. - **Activity** — a secondary icon in the title bar opens every command the agent runs, with duration and ok/error state. A small badge carries the event count without competing with the safety controls. - **Selection readout** — whatever the user selects is pushed to the agent automatically (debounced) and shown as "Agent sees: …", so the user always sees what `figma_selection` will return. Select a frame, say "build this" — no node-id copying. - **Setup** — the access key (masked by default, with Show/Hide) and the optional REST token, always reachable whether or not the bridge is connected. ## Design-to-code workflow The design is the complete specification — the tooling makes copying it easier than interpreting it. Build a screen from Figma in six steps: Keep the target project's framework and styling system. Do not add Tailwind, a UI kit or an icon library solely for the screen, and never replace exported Figma artwork with a convenient approximation. Reuse a project component only when its rendered design and states actually match. 1. **`figma_screenshot`** on the target frame, then read the saved PNG — the visual ground truth. Never build from a node tree alone. 2. **One `figma_spec` with `phase: "all"`, `depth: 3–4`** (for a large screen: `depth: 30` plus `outFile`, which stitches the complete capture from bounded plugin evals under one observed revision and writes it to the workspace — implement from that file, never from the screenshot) — build the markup skeleton and its bounded exact styles: real text characters, resolved icon/component names (instances are descended into, so overrides and true main-component names appear), hierarchy and flex direction. Copy texts and icons verbatim. A `layout:inferred (Figma heuristic — verify)` marker is not authored Auto Layout; check the hierarchy before treating it as the component contract. 3. **Export tokens** (`figma_run` with `["export","css"]` or `["export","dtcg"]`) and wire them up as CSS variables / theme. The output names its source Figma file — check it is the file you are building. 4. **Export assets** (`figma_run` with `["export","assets","","-o","/abs/path/src/assets"]`) — every `→ assets/…` reference in the spec points at a file this writes. Pass an absolute path; large exports keep running in the background ("still RUNNING") — re-run the same call to poll. Manifest v2 keeps source identity, content digest, semantic label, physical filename and placements separate. `assets.json` is merged across runs and byte-identical assets are deduped; same-name/different-content collisions get a stable digest suffix instead of overwriting an earlier file. Existing v1 manifests remain readable. Each asset carries placement data (`x`/`y` offsets, `parent` name path, `parentId`, `absolutePosition`, `overhang`), so the manifest alone positions an overlay — no spec cross-reference needed. The export summary lists the absolutely-positioned and overhanging files explicitly: those are the ones builds lose. After export, `assets.json` is authoritative for any collision-resolved filename. Oversized PNGs are downsampled by default to 2× their largest Figma usage (retina density), without upscaling and only when the encoded file becomes smaller. Aspect ratio, manifest placement and CSS crop behavior remain unchanged; pass `--raster-scale 0` to retain original PNG bytes. CLI and MCP now execute this through one **Asset Export Command Application**. Equivalent default flag spellings attach to the same tracked job; polling never starts a duplicate. Every multi-step read is pinned to one daemon-confirmed Figma connection, file and target-Page revision. A change aborts before publication. Document-wide revision observation initializes only for freshness-sensitive reads; ordinary eval/render paths keep Figma's dynamic page loading. A process-safe output lock serializes CLI/MCP merges into the same directory. A partial byte-export failure keeps successful files, preserves last-known-good failed placements and reports every failed asset. If all selected assets fail, the command returns an error and leaves an existing `assets.json` unchanged instead of replacing it with an empty manifest. Asset bytes use an authenticated, digest-checked stream outside the generic JSON result: each 256 KiB chunk is acknowledged before the next, the Daemon download is signed and one-shot, and the publication Worker stages each asset before requesting another. A single PNG/SVG may therefore exceed the former Base64 ceiling without raising the 5 MiB result limit or retaining all export bytes in memory. Figma still supplies one complete byte array per individual asset; the bound is the largest current asset (maximum 512 MiB), not the total export. Pass `force:true` to `figma_run` when an intentional fresh export should bypass the short retry-result cache; a running job is still never duplicated. 5. **One `figma_spec` `nodeIds[]` batch for any missing deeper styles** — put all section/node reads into the same call instead of requesting each section separately. In Manual Mode this means one approval for the batch. Use `phase: "style"`, `depth: 0` for exact containers and `dedup: true` for repeated lists/cards. Apply sizes, gaps, padding, alignment, fill/hug sizing, paints incl. gradients (`→ var(name)` marks a design-token binding), radii, shadows, typography, `opacity`, `clip` (overflow hidden) and `abs` positioning. Decorative vectors appear as `vector art → assets/…` lines with placement — place the exported SVGs, never approximate them in CSS. Structured YAML/JSON additionally retains exact component property definitions and values (including INSTANCE_SWAP and SLOT), property references, preferred values, direct overrides, exposed instances and slot violations. Variable bindings include collection identity, authored scopes, explicit/resolved modes, `codeSyntax.WEB` and the resolved value; `inferredVariables` is emitted separately as suggestion-only evidence. For a large section, request `depth:0` first. This is a complete contract for the section container itself (including background, border, radius and layout) without descendants. Then request child node ids in bounded calls. Use `dedup:true` for repeated cards/lists; shared `S` references remain lossless and stop identical instance styles exhausting the result budget. 6. **Verify** — screenshot your build and compare against the PNG from step 1, then run the mechanical check: ``` figma_run ["verify-build", "/abs/path/to/project"] ``` It greps the project against `assets.json` and lists every exported file that is _not_ referenced in the build — with size, offsets and parent, so placing it is one step — plus a `border-image` lint (CSS `border-image` ignores `border-radius`; gradient strokes on rounded boxes need the wrapper or mask pattern). Exit code 1 when files are missing, so it works as a CI gate too. With a build screenshot it also runs the **visual pass**: ``` figma_run ["verify-build", "/abs/path/to/project", "--compare", "/abs/build.png"] ``` The reference render is fetched live from Figma (`--node `, default: the manifest's export root) or supplied offline via `--design `. Both images are normalized to a common width and pixel-diffed (antialiasing-tolerant); the output reports the overall diff percentage, a height-mismatch finding (build too tall/short = inserted or dropped block), the worst differing regions in node-pixel coordinates — the same space the spec and `assets.json` use — and writes a diff PNG (red = differing, on the dimmed design). Informational by default; `--max-diff ` gates the exit code. For large screens, section-level agents are an optional elapsed-time optimization after the screenshot, structure map, tokens and assets are fixed. Use them only for substantial sections with disjoint component/style files; the coordinator keeps ownership of the shared shell, tokens, `assets.json`, integration and final pixel diff. Parallel agents usually consume more total tokens because each needs project context, so use sequential work when token cost matters more than wall-clock time. Use an existing browser tool or project harness for the build screenshot. Do not install Playwright (or another browser dependency) solely for capture without the user's approval; if it is already present, it is a valid capture mechanism rather than a Figma Bridge dependency. The same spec is available as `figma_run ["export", "code-spec", ""]`. Its default is the readable tree; pass `-f yaml` or `-f json` for the canonical model. ### Lossless structured spec formats `figma_spec` and `export code-spec` default to `format:"tree"`, the concise, line-oriented agent view whose footers carry the required asset and fidelity actions. Use `yaml` or formatted `json` explicitly when a consumer needs the versioned canonical model. Both structured formats serialize the **same model**; only syntax differs. Roundtrip tests require every field — text, ids, layout provenance, paint, typography, mode-aware variables, assets, component contracts, Bridge intent, native annotations, capture completeness, and fidelity checks — to survive exactly. Minified JSON is not offered: real agent tests showed that a single huge line was materially harder to act on despite carrying the same raw fields. The model's `capture` field explicitly reports requested/actual depth, payload completeness, hidden-node policy, and whether the requested depth cut off descendants. There is no silent tool-result truncation: if a spec exceeds the configured output budget, the call returns `complete:false` with a section-by-section retry recipe and returns **no misleading partial design**. `depth:0` intentionally means “the requested node only” and is complete, not a depth-truncated tree. Every Design Capture also carries a hidden-content census independent of depth and `includeHidden`: total, visible, hidden and hidden-text counts plus bounded layer ids and text previews. Hidden layers remain excluded by default so they cannot become phantom UI, while Code-Spec marks exact hidden content and alternate-state inspection incomplete until the same node is requested with `includeHidden:true`. Used Component Sets are keyed by Design Entity, published set key, or local set id—not by their mutable display name. Code-Spec normalizes `State`, `Status`, `Interaction`, and Boolean component properties and reports every set as `defined`, `noneDefined`, or `notCaptured`. A `notCaptured` set blocks style output and returns one bounded `nodeIds[]` batch recipe, avoiding both guessed states and repeated Manual Mode approvals. CSS and DTCG token export preserve every Figma collection mode and resolve aliases per mode. DTCG keeps the complete mode table in the `figma-bridge-mcp` extension while its normal `$value` remains the default mode for standard consumers. CSS emits the default under `:root` and each additional mode under `[data-figma-mode=""]`; applying that attribute is an explicit application responsibility. Different mode values are never converted into `clamp()` without authored viewport/min/max intent. Color aliases with Figma-authored opacity are read as the native closed `COMPOSE_COLOR(alias, opacityPercent)` expression. CSS receives the resolved alpha-composed color; DTCG additionally preserves the exact Alias name, Variable id and opacity percentage in the Bridge extension. Public MCP failures use stable error kinds and user-facing details; internal Node executable paths and command arguments are never returned. Safe read recovery keeps its single retry, but the operation, readiness wait and CLI fallback all spend the same overall deadline. A missing daemon token goes straight to the existing startup fallback instead of waiting for readiness. For large screens, first request one shallow structure map, then send up to eight bounded section/style reads through `nodeIds[]` in the next single `figma_spec` call. The batch fails closed on the first failed read or when its combined result would exceed the output budget; it never presents partial batch data as complete. IMAGE-fill filenames are keyed by Figma's stable image hash, not by the local layer name/path. This keeps `figma_spec`, isolated child calls, asset export and `assets.json` on the same filename even when generic layers such as “Frame 64” are reached through different roots. Linked vectors use their Design Entity identity; unlinked vectors fall back to canonical visual content identity, never a volatile node id. `verify-build` recomputes Manifest v2 digests from the physical files and exits non-zero for tampering or missing asset files; v1 entries without digests remain compatible but are reported as unverified. For MCP design-to-code calls, `dedup:false` is the default: every visible layer keeps its own id, native Figma Inspect `css{…}`, layout/paint/token facts and complete text. Mixed rich-text layers carry their individual styled ranges. The footer reconciles the live visible-layer count with explicit rows, SVG internals, component internals and non-rendering helpers. A style projection is rejected when depth limits or an unaccounted layer would force guessing; split it by the node ids from the structure map. Set `dedup:true` only for a compact overview using shared `S` style and repeat references. For repeated explicit-node calls, `phase`, `format` and deduplication do not trigger another full Figma walk. The in-memory Design Capture cache is bounded to 8 entries / 8 MiB by default (`DESIGN_CAPTURE_CACHE_ENTRIES` and `DESIGN_CAPTURE_CACHE_BYTES`). Every hit still probes the live document revision; there is no TTL and no stale-while-revalidate path. ## Code ↔ Figma design memory Give each important component, screen or frame a durable **Design Entity** id. The id describes the concept, not its current location: use names such as `ui.button`, `ui.account-card` or `screen.settings`. After selecting or identifying a Figma node, an agent can create the link with: ```text figma_run {args:["link","set","9:9","screen.settings","--kind","screen","--source","src/routes/settings.tsx","--export","SettingsScreen","--story","screens-settings--default"], confirm:true} ``` This converges two small adapters: - `figma-bridge.json` is the committed, reviewable Registry with repo-relative code paths plus optional Storybook and Figma handles. - Figma stores only `{version,id,kind}` as plugin data on the node. It contains no local path, credential or machine-specific state. Use `figma_run ["link","inspect","9:9"]` to resolve a node and `figma_run ["link","list"]` to inspect the repository memory without reading Figma. Once linked, `figma_selection` and `figma_spec` automatically expose the same id and the Registry's code/Storybook targets. Agents should reuse or edit that code component instead of creating a look-alike. Repeating the same `set` command is safe and repairs either side after an interrupted write. After visually verifying that code and Figma correspond, explicitly record their current fingerprints. Screen entities require a real browser screenshot and a passing pixel threshold: ```text figma_run ["link","accept","screen.settings","--compare","/abs/build.png","--max-diff","5"] ``` Source code is never stored in the Registry. The initial code Adapter hashes the complete linked file plus its export identity; therefore an unrelated edit in a shared file may conservatively report a code change, but a real change is never hidden. The Figma Adapter hashes the normalized linked subtree. For Code-to-Figma nodes it also stores each unique `figmaBridge.semanticPath` with that node's subtree hash. A later `link status` can therefore list the exact added, removed or changed semantic paths and recommend node-scoped specs. Changing a semantic marker alone does not change the visual fingerprint; duplicate paths are reported as ambiguous instead of guessed. ```text figma_run ["link","status","screen.settings"] figma_run ["link","context","screen.settings"] ``` | Status | Meaning | | ------------ | ---------------------------------------------- | | `unchanged` | Neither side moved from the accepted baseline. | | `code-only` | Only the linked code file moved. | | `figma-only` | Only the linked Figma subtree moved. | | `conflict` | Both moved; neither side is overwritten. | | `untracked` | No baseline has been explicitly accepted yet. | `link context` is the preferred agent entry point after a link exists. It returns the smallest relevant projection: entity, code/export, Figma root, Storybook story, current Round-trip Plan, discovered `DESIGN.md`/token files and exact next reads. It is generated on demand, not persisted as another memory file. `link accept` writes only `figma-bridge.json`; it never changes Figma or code. For screens it also stores the measured diff and SHA-256 hashes of both comparison images, so a structural fingerprint cannot certify a visibly wrong baseline. Conventional `DESIGN.md`, `design/DESIGN.md`, `tokens.json` and `design/tokens.json` locations are discovered automatically. Configure custom repo-relative locations once when needed: ```text figma_run ["link","configure","--design-doc","docs/product-design.md","--tokens","src/theme/tokens.json"] ``` Commit `figma-bridge.json`. Do not put secrets, absolute paths or generated credentials in it. ### Reviewed CSS ↔ Figma boundary strategies Semantic Code-to-Figma uses stable policy ids rather than silent visual substitutions: `minmax.native-grid`, `space-around.equal-slots`, `border.single-paint-native`, `sticky.metadata-only`, `filters.layer-stack`, `masks.vector-mask`, `font.named-faces`, and `figma-effects.native`. The full matrix and its remaining hard stops live in [`docs/css-figma-semantic-matrix.md`](https://github.com/KaiUweHella/figma-bridge-mcp/blob/main/docs/css-figma-semantic-matrix.md). Reviewed lossy policies can opt into an automatic native Figma annotation on the exact affected semantic node. The annotation explains the unsupported CSS fact, links the relevant Figma properties and is mirrored as versioned `figmaBridge.fallbackAnnotations` plugin data for future agents. Equivalent native conversions remain unannotated to avoid review noise. The first active policy is `border.single-paint-native`: Figma receives the first explicitly painted CSS side as the shared native stroke, retains all four side weights, and marks `strokes` plus `strokeWeight`. Native renders report how many fallback annotations were added, deduplicated or unsupported. Intrinsic auto-sized DOM containers and their single-line text map to Figma HUG sizing when the parent does not stretch them. Explicit fixed dimensions stay fixed. Text that participates in an authored stretch/available-width or wrapping relationship maps to FILL instead; centered or end-aligned FILL text keeps that intent through the native Text alignment property. Positioned text keeps measured box geometry. The bridge does not add arbitrary percentage width headroom to prevent wrapping. Variable-font axes are captured, but the structural gate asks whether the required font should be installed or an available named face should be used before rendering. Native Figma Glass remains an editable native effect with all effect parameters retained; it is not silently treated as CSS `backdrop-filter`, because Figma's CSS export does not expose those Glass parameters. ## Storybook mirroring Figma components carry a **stable publish key** (survives library publishing; node ids are file-local). The key now flows through `figma_spec` (canonical structured model + the "Component sets used" tree trailer), `figma_selection`, `component list`, `figma_inspect`, and DESIGN.md. To link them to their code mirror: ``` figma_run ["map", "storybook", "http://localhost:6006"] ``` The conventional local origins `localhost:6006`, `127.0.0.1:6006`, and `[::1]:6006` work without configuration. Approve an exact hosted HTTPS origin or an intentional private-network origin before using it: ```bash export FIGMA_BRIDGE_STORYBOOK_ORIGINS=https://storybook.example.com export FIGMA_BRIDGE_STORYBOOK_PRIVATE_ORIGINS=http://storybook.internal:6006 ``` Configured values are exact origins, not wildcard URLs. A Storybook base path such as `https://storybook.example.com/design-system` may then be passed to the command. Every redirect must lead to another approved origin. You can also pass a local Storybook directory; those reads stay inside the configured workspace roots. This matches the file's components against the Storybook index by normalized name and writes **`figma-map.json`** into your project: Figma key ↔ story id / import path, with a `confidence` per match plus both unmatched lists. Edit entries by hand and set `"matchedBy": "manual"` to pin them — pinned entries survive re-runs. When the file exists, `figma_selection` and `figma_spec` annotate components with `↔ story ()` automatically. `figma-map.json` remains a legacy read adapter, so existing mappings continue to work. New durable links belong in `figma-bridge.json`; `link set` never copies legacy rows into it. Migrate a component when you next touch it by assigning its real Design Entity id and passing its story via `--story`. Remove the legacy file only after `link list` shows every mapping you still need. ## Bring your own design system This project ships **no** design system — no shadcn, no Tailwind preset, no icon pack. That is deliberate: a bundled system is someone else's opinion rendered into your file. What it ships instead is a way to make _your_ system legible to an agent in one command: ``` figma_run ["kit", "init", "./my-app", "--storybook", "http://localhost:6006"] ``` Four reads, one report: | Step | Result | | ---------------------------- | -------------------------------------------------------- | | `extract` | `design/DESIGN.md` — structure, tokens, variant matrices | | `export dtcg` | `design/tokens.json` — W3C design tokens | | `component list --all-pages` | inventory with stable publish keys | | `map storybook` | `figma-map.json` — Figma component ↔ story | It ends by naming what is still missing — an unmapped Storybook, components with no story, the `tokens sync` command that keeps the two in step — because a setup that quietly lacks the mapping looks finished until an agent needs it. DESIGN.md is what an agent should read first; `tokens.json` is what it binds to. ## Several files at once The bridge holds one connection per Figma window in which you started the plugin. That is the consent model: a file is reachable because _you_ opened it and launched the plugin there — not because a flag widened the scope. - **One window** — nothing changes. Commands go there. - **Several windows** — a command must name its target, or it fails with the list of connected files: ``` figma_status # lists every connected window figma_run {args: ["canvas","info"], fileKey: "GY5SasBJ…"} figma_spec {nodeId: "12:34", fileKey: "GY5SasBJ…"} ``` `figma_render`, `figma_selection`, `figma_inspect`, `figma_screenshot`, and `figma_spec` accept the same `fileKey` parameter. A full Figma node URL also supplies its file key automatically. Without a target, `figma_selection` says which files are open rather than guessing. On the engine CLI the flag is `--figma-file`, not `--file`: `eval` and `spec` already use `-f, --file` for a local path. There is deliberately **no "all files" option**. Every write names one file, so a mistaken command cannot fan out across a library. Two windows on the _same_ file are indistinguishable for routing, so the newer one takes over and the older is told it lost the bridge. Audit entries carry only a redacted target summary, so multi-file operations remain correlatable without retaining a raw file key. Reaching files you have _not_ opened is out of scope: Figma's REST API cannot write document content, so a bulk rename across thirty library files is not something this tool can honestly offer. ## FigJam The plugin runs in FigJam boards too, over the same bridge — no second transport, no extra permission: ``` figma_run ["jam", "sticky", "Ship the handshake", "--color", "green"] figma_run ["jam", "stickies", "[\"Discovery\",\"Build\",\"Ship\"]", "--columns", "3"] figma_run ["jam", "shape", "Decide?", "--type", "DIAMOND"] figma_run ["jam", "connector", "1:2", "3:4", "--text", "yes"] figma_run ["jam", "table", "3", "4", "--data", "[[\"Step\",\"Owner\"],[\"Handshake\",\"Alex\"]]"] figma_run ["jam", "board"] # read everything back, with connectors figma_run ["jam", "arrange"] # arrange only the current selection figma_run ["jam", "arrange", "--ids", "1:2,3:4"] figma_run ["jam", "arrange", "--all"] # explicit: whole page ``` New nodes land to the right of whatever is already on the board unless you pass `--at x,y`, so an agent adding to a populated board does not stack everything at the origin. Every command checks `figma.editorType` first and says "this is a figma file, not a FigJam board" rather than failing on an undefined API. `figma_status` reports which editor the bridge is attached to. `jam arrange` is deliberately selection-scoped. Agents can pass exact node ids without changing the user's selection; rearranging the whole page requires the visible `--all` flag. Sections and connectors are never moved by this command. The public surface was exercised in Figma Desktop on 2026-08-10. Maintainers keep the detailed command and readback evidence outside the public repository. ## Figma Slides beta Slides uses the same authenticated plugin bridge and the same Semantic Render Plan; there is no separate presentation renderer. The surface covers deck structure, native slide properties, deck-wide reads, bounded batch edits, media insertion and one explicit render target: ``` figma_run ["slides", "inspect"] # deck grid, focus, labels, transitions figma_run ["slides", "manifest"] # compact per-slide projection for review and version control figma_run ["slides", "lint", "--fail-on-issues"] # duplicate-name, skipped-placement, missing-transition, empty-slide, unlisted-slide figma_run ["slides", "create", "Agenda", "--row", "0", "--col", "1"] figma_run ["slides", "duplicate", "Agenda", "--label", "Agenda alternative"] figma_run ["slides", "move", "Agenda alternative", "1", "0"] figma_run ["slides", "transition", "Agenda", "DISSOLVE", "--duration", "0.4"] figma_run ["slides", "skip", "Appendix", "on"] figma_run ["slides", "delete", "1:42"] figma_run ["slides", "batch", "[{\"op\":\"create\",\"label\":\"Q&A\"}]"] # plan only figma_run ["slides", "batch", "[{\"op\":\"create\",\"label\":\"Q&A\"}]", "--apply"] figma_run ["slides", "media", "label:Agenda", "assets/loop.gif", "--fit", "FIT"] figma_run ["slides", "focus", "label:Agenda"] figma_run ["slides", "view", "single-slide"] figma_run ["render", "…", "--slide", "12:34"] # render one plan into one slide ``` Figma renumbers native slide names whenever the canvas grid changes. The optional argument to `create` and `--label` on `duplicate` therefore store a durable Bridge label in plugin data; `inspect` and `manifest` report both the native `name` and the stable `label`. References resolve by id, exact native name or label, then unique substring; typed `node:`, `name:` and `label:` prefixes restrict the lookup. Ambiguity is an error, delete always requires an explicit reference, and duplicate/move refuse a nonexistent target row rather than accepting Figma's fallback placement. `render --slide ` takes a node id from `inspect`; it deliberately does not accept a name or label, because a render must never pick its own target when a reference turns out ambiguous. The root lands in the slide's own coordinate space. `render-plan-batch` stays refused in Slides. `batch` is a plan by default and performs nothing until `--apply`; every reference and coordinate is validated against one grid snapshot first, a coordinate that only becomes valid because of an earlier item is refused, and the first failing item reverses everything applied before it. A created slide whose grid position Figma has not listed yet is verified by identity, label, focus and selection and reported as `gridObserved: false`. `media` inserts a still image (`IMAGE` paint), an animated GIF (a playable `MEDIA` node) or a video (`VIDEO` paint) into one slide. The file rides inside the plugin payload as base64, so the ceiling is 3.5 MB of file bytes — the same ceiling `render` and `node set-image` apply to images. Two facts about Figma's canvas grid shape every command. `figma.getCanvasGrid()` can lag the page tree by minutes: a slide that exists on the page may be missing from the grid, or the grid may keep a removed slide's id and throw on every read. The Bridge treats the grid as authoritative for coordinates and the page tree as authoritative for existence. Readers list such slides under `unlisted` with no coordinate (and say why when the grid cannot be read at all), `lint` reports them as `unlisted-slide`, `delete`, `transition`, `skip`, `media` and `focus` still reach them, and `move` refuses one with the reason — without a coordinate there is no origin to restore. Every operation checks `figma.editorType === "slides"` before touching a Slides-only API. Polls, facepiles, embeds and speaker notes have no public create or read API; the roadmap in [`docs/slides-roadmap.md`](https://github.com/KaiUweHella/figma-bridge-mcp/blob/main/docs/slides-roadmap.md) records what was probed against the live runtime, and editor acceptance is maintainer-verified separately from the public repository. ## Token sync (two-way) `tokens import` only ever creates, so a value edited in code never reaches an existing Figma variable and a value edited in Figma never reaches code. `tokens sync` closes that loop: ``` figma_run ["tokens", "sync", "src/tokens.json"] # plan only figma_run ["tokens", "sync", "src/tokens.json", "--apply"] # write it ``` The import surface is broader than the sync surface. One-shot `import` accepts Tailwind v3 config, Tailwind v4/CSS, Storybook indexes, DTCG/W3C JSON, and the DTCG-compatible token shapes exported by Style Dictionary and Tokens Studio. That compatibility does not include Tokens Studio theme semantics or arbitrary preprocessors; metadata such as `$themes` is ignored while token sets and aliases are read. Tailwind JS/CJS/MJS/TS configuration is parsed as static data. The importer understands ordinary object exports, `theme`, `theme.extend` and typed `satisfies Config` forms without resolving imports or executing plugins. Dynamic expressions inside token-bearing theme fields stop with instructions to import Tailwind v4 `@theme` CSS or resolved W3C JSON instead. Unrelated fields such as `content` and `plugins` remain compatible but are discarded. New FLOAT variables in explicit `spacing/*` or `space/*` namespaces are scoped to Figma's `GAP` consumers only. `radius/*` and `radii/*` variables are scoped to `CORNER_RADIUS` only. The inference is deliberately namespace-exact: names such as `spacingFactor` are left at Figma's default scope, and rendering does not silently change the scopes of existing user or library variables. Numeric spacing and radius literals may reuse an exact generated variable only when its identity and value are unambiguous. If duplicate collections expose the same generated name, the native renderer keeps the exact visual literal unbound and reports that decision; explicit `var:` references still fail closed until their collection identity is resolved. If no exact generated Variable exists, the literal remains literal unless the caller explicitly sets `materializeResources:true` / `--materialize-resources`; the opt-in preserves the former generated-token workflow rather than removing it. Other new COLOR, FLOAT, or STRING variables surface `SCOPE DECISION REQUIRED` with only the compatible Figma choices. The agent should ask before narrowing them; inspect the catalog with `figma_reference {name:"variable-scopes"}` and apply the answer with `figma_run ["var","update","","--collection", "","--scopes","TEXT_FILL,STROKE_COLOR"]`. Safe three-way sync accepts only **DTCG / W3C design tokens** (`.json`, what `export dtcg` emits) and **CSS custom properties** (`.css`, what `export css` emits). Sass `$variables` are not CSS custom properties and `.scss` is refused rather than partially parsed. Note that `export dtcg` writes _every_ local variable into one file while sync targets one collection — pass `--collection` accordingly. If most names in the file already live in another collection, sync says so instead of offering to duplicate them. Tailwind configs are an import source only — their parser buckets values into colour/spacing/radius and cannot round-trip, so sync refuses them by name rather than silently dropping tokens it did not understand. **Why a lockfile.** A two-way sync without memory cannot tell "the code changed" from "Figma changed" — it only sees that the two differ, and whichever direction it picks destroys the other side's work. `figma-tokens.lock.json` records the state at the last successful sync, so every decision is a three-way comparison: | code | Figma | result | | ------------ | --------- | ------------------------------------------------------- | | changed | unchanged | update Figma | | unchanged | changed | **reported, never overwritten** — update your code file | | both changed | | **conflict** — nothing is applied | | unchanged | unchanged | unchanged | Commander and `figma_run` execute this comparison through the same closed Command Plan. The source, prior lock and exact Collection are resolved before Plugin readiness; duplicate Collection or Variable names and flattened DTCG or CSS name collisions stop explicitly instead of selecting the first match. For compatibility with the original sync command, an existing Collection uses its first declared mode. If the Collection has more modes, sync says so; it does not silently switch to `defaultModeId` or change values in the other modes. Conflicts stop the whole run. Resolve them by editing one side, or decide them all at once with `--ours` (the code file wins) / `--theirs` (Figma wins, and nothing is written to Figma). Deletions need `--prune`, and even then only touch variables sync itself created — a variable it never tracked is reported as untracked and left alone. The lockfile also stores each variable's Figma id, which is what makes a **rename** one rename instead of a delete plus a create that would drop every layer binding. Pairing is by value and only when unambiguous: renaming _and_ re-valuing a token in the same commit falls back to create + delete, so do those as two steps if the bindings matter. Without `--apply` the command exits 1 when changes are pending, so it works as a CI check for "is Figma in sync with the repo?". Apply is intentionally non-atomic inside Figma: retype and delete have no general safe inverse. It rechecks the complete observed state immediately before the first write, stops after the first failed operation, verifies the fresh post-state and reports `no-mutation`, bounded `partial-residue`, or `verification-unavailable`. An apply is never automatically retried after dispatch. The lockfile advances only after complete verified success. A per-lockfile process lease serializes source/lock read, Figma work and publication across CLI and MCP processes, and publication atomically replaces the prior file from a fully written sibling. If publication fails, the prior lock remains intact and the command reports any Figma changes that already committed. ### Binding, and switching which collection a design follows `tokens sync` writes token _values_. Two neighbouring things it deliberately does not do: ``` figma_run ["node", "bind", "12:34", "radius", "radius/lg", "--collection", "TARGET_COLLECTION"] figma_run ["tokens", "rebind", "TARGET_COLLECTION", "--node", "12:34"] # plan figma_run ["tokens", "rebind", "TARGET_COLLECTION", "--node", "12:34", "--apply"] # write ``` `node bind` attaches a variable to a property of an **existing** node — `fill`, `stroke`, `radius`, `gap`, `padding` (or one side), `opacity`, `stroke-width`, `width`, `height`. Its read counterpart is `node bindings`. The legacy `--batch` mutation is currently disabled until it has a typed bounded request, complete preflight and truthful rollback/residue result; run one explicit node per command in the meantime. For a bounded machine-readable read, add `--json`: ``` figma_run ["node", "bindings", "12:34"] figma_run ["node", "bindings", "12:34", "--json"] ``` The result preserves direct `node.boundVariables` topology: scalar aliases, array positions such as `fills.1`, and component-property names. It also keeps the variable id, name, resolved type and collection identity when Figma can resolve them. This is intentionally a fast discovery outline, not proof of every variable influencing the node. Use Design Capture/Code Spec for bindings inside styles, paints, effects or text ranges, mode values, resolved values and provenance. A variable name that is not unique is **refused, not guessed** — this file has `radius/lg` in two collections, and the answer names both so `--collection` can settle it. The variable's type is checked against the property first, so a COLOR on `radius` fails with a sentence rather than a plugin stack trace. Typography variables have their own range-aware command because text can carry different bindings on different character spans: ``` figma_run ["font", "bind", "12:36", "fontWeight", "type/weight", "--collection", "Typography"] figma_run ["font", "bind", "12:36", "line-height", "type/line-height", "--start", "0", "--end", "12"] figma_run ["font", "unbind", "12:36", "lineHeight", "--start", "0", "--end", "12"] ``` Bindable fields are `fontFamily`, `fontSize`, `fontStyle`, `fontWeight`, `letterSpacing`, `lineHeight`, `paragraphSpacing` and `paragraphIndent`; kebab-case spellings are accepted too. Existing fonts—and for family/style/ weight bindings the relevant available family styles—are loaded before the binding is changed. Variable names are refused when ambiguous, and STRING vs FLOAT is checked before Figma is called. A numeric `fontWeight` binding still is not a general variable-font-axis setter: Figma selects a valid weight for the active font. `tokens rebind` is the theme switch: it walks a subtree and repoints every binding at the same-named variable in a target collection. Design a card against `SOURCE_COLLECTION`, run rebind with `TARGET_COLLECTION`, and the same card follows the target collection's values — no redesign. It plans by default; `--apply` writes. Tokens with no counterpart in the target are listed and left pointing where they were, so a partial theme is a report rather than a half-broken design. Page-wide rebind remains available as a dry-run report, but `--page --apply` is temporarily disabled. A scoped `--node --apply` write remains available; the page-wide write returns only after it has bounded traversal, exact collection resolution, complete preflight and rollback evidence. `node set` changes properties on nodes that already exist — `fill`, `stroke`, `strokeWidth`, `radius`, `opacity`, `x`, `y`, `width`/`height`, `name`, `visible` — one explicit node at a time: ``` figma_run ["node", "set", "12:34", "--name", "Card", "--radius", "12"] ``` The legacy `node set --batch` path is disabled until FID-03 replaces it with a typed bounded mutation Operation that preflights all entries and can report rollback or residue without oversizing the Plugin response. `var delete-all` is also disabled. Use exact `var delete ` or ambiguity-aware `col delete ` so destructive scope is explicit. Collection deletion always returns a non-mutating confirmation preview before dispatch, even when global write confirmation is disabled. Prefer `col delete --only-if-empty` when cleanup must never remove member Variables: the Plugin verifies empty membership immediately before the native irreversible removal and refuses a populated Collection without mutation. A colour takes a hex or `var:`. The difference is not cosmetic: a hex is frozen, a `var:` reference stays **bound**, so a later `tokens rebind` can still move it. ### Local styles, variable metadata, and modes The local design-system primitives that used to require manual UI work now have first-class Figma Commands. They use the live Plugin API, not REST: ``` figma_run ["style", "list", "--type", "TEXT"] figma_run ["style", "show", "--type", "TEXT", "Heading/H1"] figma_run ["style", "create", "PAINT", "Brand/Primary", "--properties", "{\"paints\":[{\"type\":\"SOLID\",\"color\":{\"r\":0.1,\"g\":0.3,\"b\":0.9}}]}"] figma_run ["style", "apply", "Brand/Primary", "12:34,12:35", "--field", "fill"] figma_run ["style", "consumers", "Brand/Primary"] figma_run ["style", "publish-status", "Brand/Primary"] figma_run ["style", "bind-font", "Body", "fontSize", "--variable", "type/size/body"] figma_run ["style", "unbind-font", "Body", "fontSize"] ``` `style` covers local PAINT, TEXT, EFFECT and GRID styles. `update` accepts the same type-specific JSON properties as `create`; `apply` validates the style type against `fill`, `stroke`, `text`, `effect` or `grid`. Name lookups refuse ambiguity. Consumers come from `getStyleConsumersAsync()`, and publish state is one of Figma's `UNPUBLISHED`, `CURRENT` or `CHANGED` values. `style show` returns exact bounded Style Facts through the same safe-read Application from CLI and MCP. It preserves ids/keys, plain and Markdown descriptions, documentation links, authored PAINT/TEXT/EFFECT/GRID unions and the scalar-versus-array topology of Variable Aliases. A GRID Auto count is projected as `{ "$figmaScalar": "Infinity" }` instead of the lossy JSON value `null`. When the Figma runtime omits a typings-required plain or Markdown description, Style Facts reports `null` instead of fabricating content. `--type` is optional, but narrows name lookup to one style family and avoids loading unrelated local styles. Resolved Variable values, consumers, image/video bytes, shader definitions and complete Design Capture remain separate reads. URL-shaped style names remain payload and cannot retarget the Figma Command. `style list` is the compact bounded catalog rather than a dump of every authored value. It returns id/key/name/type/remote identities plus `total`, `returned`, `omitted`, `complete` and a scope-bound `next` cursor. Use `--type TEXT` (or PAINT/EFFECT/GRID) to load only one native family, `--limit 1..500` to size a page, and `--cursor ` to continue. The default page size is 100. A continuation page remains `complete: false` because that page alone is not the full catalog; `next: null` means no later page remains. Use `style show` with an id for the complete authored facts of a row. `style consumers