Skip to main content
Glama
KaiUweHella

figma-bridge-mcp

by KaiUweHella

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 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:

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<version> 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:

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:

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:

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:

claude mcp add figma-bridge -- npx -y figma-bridge-mcp@latest

For another MCP client, add the equivalent server configuration:

{
  "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.

git clone https://github.com/KaiUweHella/figma-bridge-mcp.git
cd figma-bridge-mcp
npm install
{
  "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:"<exact-file-key>"}. 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.

Related MCP server: tellfigma

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). 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). 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:

{
  "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:

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:

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:<id-or-Figma-URL> for explicit node identity or name:<literal> 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 <instanceId> 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:

<Instance
  entity="ui.card"
  prop:Selected="true"
  text:Title="New title"
  fill:StatusDot="var:status/healthy|#22c55e"
  swap:LeadingIcon="ui.icon.leaf"
/>

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, <Image src="assets/artwork.png" /> 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 <Image name="Artwork placeholder" /> 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.

<Text> also preserves editable inline Rich Text. Nested <strong>/<b>, <em>/<i>, <u>, <Span ...> and <a href="..."> 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:

<Text font="Inter" size="14">
  Hello{" "}
  <strong>
    bold <em>and italic</em>
  </strong>
  <Span color="#ef4444" size="18">
    red
  </Span>
  <a href="https://example.com">link</a>
</Text>

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","<nodeId>","-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<n> 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 <id>, default: the manifest's export root) or supplied offline via --design <png>. 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 <pct> 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", "<nodeId>"]. 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="<Mode name>"]; 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<n> 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:

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:

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.

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:

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.

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:

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 <id> (<importPath>) 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", "<Frame>…</Frame>", "--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 <nodeId> 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 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","<name>","--collection", "<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 <id> --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 <names...> or ambiguity-aware col delete <collection> 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 <collection> --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:<name>. 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 <next> 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 <style> is a separate long-budget read because Figma's native getStyleConsumersAsync() scans the document and cannot be interrupted with a visit limit. The result says this explicitly under scan, while total, returned, omitted, complete and next describe the bounded output page. --type, --limit 1..500 and --cursor mirror the catalog flow; the cursor is additionally bound to the original style query. Rows contain only consumer node identity and official inherited-style fields. No document content is changed.

style publish-status <style> returns only compact Style identity and Figma's official UNPUBLISHED, CURRENT or CHANGED state. It uses the same bounded safe-read path from CLI and MCP; --type can narrow ambiguous names without loading unrelated families. Complete authored values remain available through style show, so publish-status never transports GRID Auto counts or other irrelevant value topology through a lossy generic JSON path.

Variables expose the metadata and mode operations that token-file sync does not own:

figma_run ["var", "show", "space/md", "--collection", "Primitives"]
figma_run ["var", "create-batch", "--collection", "Primitives", "[{\"name\":\"color/neutral/0\",\"type\":\"COLOR\",\"value\":\"#ffffff\"}]"]
figma_run ["var", "create-batch", "--collection", "Roles", "[{\"name\":\"surface/default\",\"type\":\"COLOR\",\"alias\":\"color/neutral/0\",\"aliasCollection\":\"Primitives\"}]"]
figma_run ["var", "update", "space/md", "--description", "Medium spacing", "--scopes", "GAP"]
figma_run ["var", "update-batch", "[{\"variable\":\"space/md\",\"collection\":\"Primitives\",\"description\":\"Medium spacing\",\"scopes\":[\"GAP\"]},{\"variable\":\"space/lg\",\"collection\":\"Primitives\",\"hidden\":false}]"]
figma_run ["var", "set-value", "space/md", "12", "--mode", "Light"]
figma_run ["var", "set-value", "space/card", "--alias", "space/md", "--mode", "Light"]
figma_run ["var", "set-value-batch", "[{\"variable\":\"space/md\",\"collection\":\"Primitives\",\"mode\":\"Light\",\"value\":12},{\"variable\":\"space/card\",\"mode\":\"Dark\",\"alias\":\"space/md\"}]"]
figma_run ["var", "code-syntax", "space/md", "WEB", "var(--space-md)"]
figma_run ["var", "resolve", "space/md", "12:34"]
figma_run ["col", "mode-add", "Primitives", "Dark"]
figma_run ["col", "mode-rename", "Primitives", "Dark", "Dim"]
figma_run ["col", "extend", "Primitives", "Brand"]

var show returns exact bounded Variable Facts through the same safe-read Application from CLI and MCP: values by mode, alias ids, scopes, code syntax, collection/mode identity and publish status. Variable and collection names must resolve uniquely; --collection is the explicit way to disambiguate duplicate variable names. For a color Alias whose opacity was authored in Figma, valuesByMode retains Figma's native COMPOSE_COLOR expression and percentage. The current public Figma Plugin API can read but cannot set that expression (measured on 2026-09-03 with typings 1.136.0: setValueForMode throws Composed color variable values are not supported, and the typings admit an Expression only when reading valuesByMode): var set-value and var set-value-batch therefore refuse before mutation when replacing such a prior value, and exact Snapshot import refuses before Plugin readiness instead of flattening it. Create the variable and its alias through the Bridge, then set or detach the opacity in Figma until Figma exposes a writable public setter. var create-batch accepts either value or an explicit alias per item. aliasCollection scopes a target in another Collection; same-batch forward references are also supported. Alias ambiguity, type mismatch and cycles stop before creation, and equal literals are never inferred as Aliases by the command. A workflow may supply an explicit alias after an opted-in policy proves one exact compatible target; ambiguous candidates remain a report rather than a guess. One selected mode is initialized per call, so separate mode intent remains separate rather than being copied into empty modes. When metadata edits for several known local Variables are already available, collect them into one var update-batch JSON array instead of emitting one var update call per Variable. The batch preflights every identity before its first write and owns reverse rollback under one confirmation; keep var update for a single target. Batch several already-known mode-value assignments into one var set-value-batch call instead of repeating var set-value. Its explicit best-effort-rollback policy does not reject previously empty modes: successful writes commit normally; after a later failure, prior values are restored and any value that Figma cannot remove is reported as exact residue. This preserves normal utility without claiming atomic rollback. Keep var set-value for a single pair. URL-shaped names remain payload and cannot retarget the command. var resolve deliberately requires a consumer node because aliases can resolve differently under that node's selected modes. Collection show, update, mode-add, mode-rename, mode-remove and publish-status follow the same ID/exact-name/unique-substring lookup policy. Figma plan limits on mode count remain Figma-enforced and surface as errors. Collection extensions use VariableCollection.extend() for local collections and extendLibraryCollectionByKeyAsync() for published keys. Figma restricts this feature to Enterprise plans; the CLI reports Figma's plan error unchanged. Text-style bindings support exactly Figma's bindable typography fields: family, style, weight, size, line height, letter spacing, and paragraph values.

Enabled team libraries

Library discovery and imports also stay on the authenticated plugin transport:

figma_run ["library", "collections"]
figma_run ["library", "variables", "Acme/Primitives", "--type", "COLOR"]
figma_run ["library", "import-variable", "<published-variable-key>"]
figma_run ["library", "import-style", "<published-style-key>"]
figma_run ["library", "import-component", "<published-component-key>"]
figma_run ["library", "import-component-set", "<published-component-set-key>"]

collections and variables are reads. The four import-* commands materialize published assets in the current file and are therefore writes in the Capability Catalog. They share one non-retrying Command Application in CLI and MCP: the selected native import runs once, then a fresh lookup verifies the returned id, exact published key, resource kind and remote identity. Figma does not expose a safe inverse and an imported resource may have existed already, so failed or unavailable verification reports possible residue without deleting or retrying the import. Resource descriptions are bounded in the structured result. Figma only exposes discovery for variable collections and variables. Published styles, components and component sets can be imported when their stable key is already known, but the Plugin API cannot enumerate them.

Libraries must be enabled for the current file in Figma's UI before library collections can see them; the Plugin API cannot enable a library. The shipped plugin already declares the required teamlibrary permission. Name lookup uses collection key, exact collection name, then an unambiguous collection or library-name substring. Library discovery owns an 18-second Plugin-API timeout below the Bridge deadline, so a stalled Figma library request names the operation and suggests checking whether the library is enabled instead of degrading into a generic execution timeout.

Prototypes, Dev Mode measurements, and annotations

These document features are also Plugin-API-first:

figma_run ["prototype", "inspect", "12:34"]
figma_run ["prototype", "add", "12:34", "--trigger", "click", "--navigate-to", "12:36"]
figma_run ["prototype", "set", "12:34", "--json", "[{\"trigger\":{\"type\":\"ON_CLICK\"},\"actions\":[{\"type\":\"BACK\"}]}]"]
figma_run ["measure", "add", "12:34:right", "12:36:left", "--offset", "16", "--text", "gap"]
figma_run ["annotate", "categories"]
figma_run ["annotate", "add", "Review spacing", "--node", "12:34", "--category", "Review", "--properties", "width,fontSize"]
figma_run ["annotate", "edit", "12:34", "0", "--text", "Resolved"]

prototype set --json is the lossless form for Figma's multiple actions, SET_VARIABLE, SET_VARIABLE_MODE, and conditional blocks. It writes through setReactionsAsync() so dynamic-page manifests are supported. Measurement writes are guarded to Figma Dev Mode and use PageNode's native measurement methods. Annotation indexes are zero-based; custom category create/edit/remove commands are available alongside categories. These manual review notes are independent from the semantic renderer's automatic Boundary Fallback Annotations, which are emitted only by explicitly opted-in lossy mapping policies and remain machine-readable through plugin data.

prototype inspect is the read-only verification path shared by CLI and MCP. It preserves the complete authored reaction JSON while enforcing explicit depth, collection, value, string and total serialized-size bounds below the Plugin protocol cap. Oversized or malformed facts fail rather than being silently truncated, and safe JSON escaping keeps terminal controls inert without changing the parsed interaction facts.

prototype add, set, and clear use one non-retrying mutation Application in both CLI and MCP. Reaction JSON is bounded before dispatch, the previous reaction set is journaled, and fresh readback proves the exact requested set. Failed verification restores the journal when possible and otherwise reports bounded reaction residue; an exact readback after a native setter error remains an explicit verified-after-error success.

2026 Plugin APIs: video, shaders, grid, slots, and Draw

The current official Plugin API surface is exposed as Figma Commands rather than REST calls:

figma_run ["export", "video", "12:34", "--format", "mp4", "--fps", "30", "-o", "/abs/demo.mp4"]
figma_run ["shader", "list"]
figma_run ["shader", "import", "<shader-id>"]
figma_run ["shader", "apply", "12:34", "<shader-id>", "--field", "fill", "--properties", "{\"definition-id\":0.8}"]
figma_run ["layout", "grid", "set", "12:34", "--rows", "2", "--columns", "3", "--row-gap", "12"]
figma_run ["layout", "grid", "auto-flow", "12:34", "--auto-tracks", "rows", "--positioning", "row_auto_flow"]
figma_run ["layout", "grid", "reorder-rows", "12:34", "--from", "0,2", "--to", "3"]
figma_run ["grid", "list", "12:34", "--json"]
figma_run ["slot", "create", "12:37", "Content", "--settings", "{\"minChildren\":1,\"maxChildren\":3}"]
figma_run ["slot", "validate", "12:37"]
figma_run ["draw", "inspect", "12:38"]
figma_run ["draw", "text-path", "12:38", "--text", "Around the curve"]
figma_run ["draw", "stroke-profile", "12:38", "--preset", "TAPER"]
figma_run ["draw", "pattern", "12:38", "12:39", "--field", "fill"]

Video export resolves a selected descendant to its top-level animated frame and accepts only Figma's format-specific FPS values. CLI and MCP share one long-running, non-retrying Command Application whose Plan owns the exact output. It validates requested settings, canonical bounded Base64 and the MP4, GIF or WebM container before Workspace I/O. The current generic result transport supports exports up to its documented bounded payload; larger video streaming requires a separate versioned media-stream capability rather than widening the image/SVG Asset Stream. Shader properties are keyed by definition ID, not display name, and an available shader must be imported before it is applied. Shader import and application use the same non-retrying verified mutation path in CLI and MCP: apply never imports implicitly, and a failed postcondition either restores the complete prior paint/effect field or reports possible residue. layout grid means the Auto Layout Grid model; the older top-level grid command remains Layout Guide management. layout grid set uses one journaled, non-retrying CLI/MCP path for Grid conversion, dimensions, gaps and explicit track definitions. It refuses a manual shrink beneath an occupied child span, verifies the complete requested configuration while preserving unspecified facts, and restores prior Grid or free-layout state on failure when Figma permits it. layout grid auto-flow uses one non-retrying CLI/MCP mutation path: it treats auto-track and item-positioning settings as one intent, verifies both through a fresh node read, and restores both prior values on a mismatched result or reports the exact remaining field residue. layout grid place likewise shares one non-retrying CLI/MCP path. It validates manual Grid bounds and occupied cells before moving the child, freshly proves the resulting hierarchy, cell, spans and alignment, and restores the complete prior parent/free-layout or Grid state when the postcondition fails. layout grid reorder-rows|reorder-columns shares one irreversible, non-retrying path. It validates the native track permutation and freshly proves the track definitions plus every child anchor/span. A thrown call is reported as no mutation only when fresh state proves the complete old Grid; otherwise confirmed differences remain explicit residue because the Plugin API exposes no safe general inverse. grid list [nodeId] is the exact bounded read for those layout guides and may use the current selection when the id is omitted. It preserves gridStyleId, all ROWS/COLUMNS/GRID fields, colors and Variable Aliases. Figma's Auto count is returned as { "$figmaScalar": "Infinity" }, never the lossy JSON value null; text output renders that value as Auto. Slots expose GA SlotSettings, preferred values, reset, and limit violations. Create and edit share one non-retrying, freshly verified mutation path in CLI and MCP; failures remove only newly created Slot identities or restore the prior effective definition when Figma permits it. Reset is also non-retrying and reports native confirmation plus fresh before/after facts; it does not claim an independently known default subtree or rollback. JSX <Slot> uses ComponentNode.createSlot() and validates configured limits after rendering. An empty Slot on a main Component can have no reported violation even when minChildren is nonzero; instantiate the Component and run slot validate on that exercised Instance to observe BELOW_MIN. Component Property Facts normalize Figma's missing SLOT defaultValue to null and nullable descriptions, preferred values and settings to their complete effective defaults.

When generating a library from code, decompose each rendered subtree from the leaves upward before creating the wrapper. Repeated buttons, icons, avatars, badges, rows and card shells should remain nested reusable Components. Map editable copy to TEXT, optional descendants to BOOLEAN, replaceable children to INSTANCE_SWAP, state/size/tone to VARIANT and structurally flexible regions to native SLOT. A visually polished wrapper with inert descendants is not a finished code-to-Figma component contract. Draw commands cover text paths, repeat transform groups, stretch/scatter/dynamic strokes, variable-width profiles, and async pattern fill/stroke setters. Run the corresponding inspect/validate read before writes when modifying an unfamiliar document.

Finding what needs fixing

figma_run ["analyze", "lint", "--node", "12:34"]

One pass for the four things a design-system review acts on: colours that match an existing variable but are not bound to it, layers still carrying a default name, text with no style, text under 12px. --fail-on-issues makes it a CI gate; --kind narrows it; --json never truncates.

A hardcoded colour is only reported when a variable already holds that exact value — otherwise the finding is noise you cannot act on. Because the match is known, each one arrives with the command that fixes it:

unbound token colour — 1
  12:35    Badge  fill is #8a9a8d, which is sage/400
    fix: node bind 12:35 fill "sage/400" --collection "Sprout Primitives"

analyze colors|typography|spacing still give the full census. Lint is the pass that answers whether anything needs doing at all.

Variable fonts and OpenType facts

Figma does not expose a general variation-axis tuple through the Plugin API. The bridge therefore separates facts Figma actually reports from axis intent that a caller records explicitly:

figma_run ["font", "inspect", "12:36"]
figma_run ["font", "inspect", "12:36", "--start", "0", "--end", "12", "--all-open-type"]

font inspect returns styled text ranges with fontName, numeric read-only fontWeight, size, enabled OpenType feature tags and resolved typography variable bindings. --all-open-type also includes false feature values. The result names the API limit explicitly: a reported fontWeight is not a general wght/wdth/opsz/custom-axis tuple, and OpenType features are read-only.

Typography Variable bindings use the same explicit node/range boundary:

figma_run ["font", "bind", "12:36", "fontSize", "type/body", "--collection", "Typography"]
figma_run ["font", "bind", "12:36", "lineHeight", "type/leading", "--start", "0", "--end", "12"]
figma_run ["font", "unbind", "12:36", "lineHeight", "--start", "0", "--end", "12"]

font bind and font unbind are non-retrying writes shared by CLI and MCP. They preflight type compatibility, fonts, ambiguity and the prior binding, then freshly read the exact node/range binding after the setter. A silent no-op or wrong Variable id is a failure, not success; the prior binding is restored and verified where possible, otherwise the result reports bounded residue.

When the exact axis values are known from the UI or another font tool, preserve them on the text node as range metadata:

figma_run ["font", "remember-axes", "12:36", "wght=357,wdth=82", "--start", "0", "--end", "12"]
figma_run ["font", "axes", "12:36"]
figma_run ["font", "forget-axes", "12:36", "--start", "0", "--end", "12"]
figma_run ["font", "forget-axes", "12:36"]  # clear every stored range

remember-axes changes plugin metadata only — never the font or rendered glyphs — and is therefore classified as a write by the Capability Catalog. figma_spec carries these records as axes-meta[start:end](tag=value,…), plus Figma's reported fw… value and enabled ot(…) tags, so design-to-code capture does not silently discard the documented intent.

Native Plugin API facts

Two read commands expose Figma's own representations without contacting the REST API:

figma_run ["node", "css", "12:34"]
figma_run ["node", "css", "12:34", "--json"]
figma_run ["export", "node-json", "12:34"]
figma_run ["export", "node-json", "12:34", "-o", "facts/card.json"]

node css calls getCSSAsync() and returns the declarations Figma exposes for its Inspect panel. This is deliberately separate from export css, which exports design-token custom properties. Its text and JSON projections share one target-bound, bounded safe-read Application; an oversized or malformed Plugin result is refused rather than silently truncated. export node-json uses exportAsync({format:"JSON_REST_V1"}): Figma returns the official REST nodes response envelope with the requested node under document, alongside components, componentSets, schemaVersion and styles. The bytes come from the live plugin document and need neither a token nor a network request. CLI and MCP share one target-bound safe-read Application: the Command Plan owns the optional output file before readiness, and bounded document.id/type, JSON structure, transport size and formatted output are validated before stdout or Workspace publication.

Version history and diffs

Figma's plugin API can write a version but not read one back, so "what changed since this morning" has no answer from the bridge alone. history supplies one without any credential: record the structure of a subtree, record it again later, diff the two.

figma_run ["history", "save", "Before refactor", "--description", "Agent restore point"]
figma_run ["history", "snapshot", "--label", "before refactor"]
# … agent works …
figma_run ["history", "diff", "latest", "live"]

history save creates the named entry directly through saveVersionHistoryAsync() and is a Figma write. snapshot, list and diff remain local/read-only Figma operations; reading Figma's native historical versions still requires the optional REST add-on.

A snapshot stores one normalized record per node — geometry, layout, paints, typography, component keys — plus a content hash and a subtree hash, so the differ can report an untouched section instead of walking it. They live in ~/.figma-bridge-mcp/snapshots/<fileKey>/, gzipped, newest 20 kept. The Structural Snapshot Store keeps its directories 0700 and files 0600, and bounds both compressed and expanded data before parsing.

Refs are latest, previous, a bounded index from history list, an exact or unique-prefix filename, or live for the document right now. Local refs never accept host paths or escape the owning file namespace. The report separates added, removed, replaced, moved and changed — that last distinction is the one that matters in practice: an agent that deletes a frame and re-renders it keeps the name path but gets new node ids, and without the replaced-detection every re-render would read as a hundred deletions. --changelog emits markdown instead; diff exits 1 when anything differs, so it also works as a CI gate.

Via MCP this is a parameter, not a thirteenth tool:

figma_history {diff: {from: "latest", to: "live"}}
figma_history {diff: {from: "version:1234", to: "version:5678"}}   # REST add-on

version: refs go through the REST layer and diff what designers saved, using the same differ. The two sources cannot be mixed in one diff: a REST document and a plugin snapshot expose different properties, so every node would look changed — the tool says so rather than producing a misleading wall of output.

Motion

Figma Motion (Config 2026 Beta) is reachable through figma_run with ["motion", …]: keyframe tracks (add), whole specs from JSON (apply), named presets (preset), choreographed offsets across nodes (stagger), Figma's first-party animation styles (styles, style), frame duration (timeline), readback (inspect) and removal (clear).

Like every other command it runs over the plugin bridge — there is no separate transport for it. styles, inspect, and timeline without --duration are reads. timeline --duration … and every other Motion action count as writes under FIGMA_WRITE_CONFIRM=1.

Motion is rolling out behind a Figma Beta flag. Without access, the commands fail with a named MOTION_DISABLED error telling you to update Figma Desktop rather than a generic API failure.

Motion also travels with the design in both directions:

  • Figma → code. figma_spec and export code-spec capture a layer's native keyframe tracks and project them to CSS: every animated layer gets a motion: segment and the spec ends with a ## Motion (implement as CSS) section holding one @keyframes block and animation declaration per layer, using Figma's own easing curves. Springs, Variable-bound easings and fields CSS cannot express are listed as limitations; Figma timelines play once, so loops (iteration-count, direction) stay a code decision, and a prefers-reduced-motion: reduce override is expected. The JSON/YAML model carries motion with the same css projection.

  • Code → Figma. The browser capture expression reads each element's CSS animation facts and the referenced @keyframes, pauses playback while it captures resting geometry, and render --dom-capture writes the animation as native Motion tracks on the created layer (negative delays become phase shifts, alternate becomes a two-period timeline). The render result reports Motion: N native keyframe track(s) … plus any layer Figma refused.

Security model

  • No Figma API token required — Figma is driven through the local plugin, never api.figma.com. The REST add-on is strictly opt-in: without a token the code path is inert, and with one the token lives in a 0600 file (or your own env var), not in the MCP client config.

  • No binary patching — Yolo/CDP mode is stripped from the vendored engine.

  • Capability-gated commandsfigma_run only accepts Commands exposed by the Capability Catalog; connect is not exposed, so Safe-Mode-only connection is enforced. The same resolved plan drives the write-confirm gate, target requirement and retry policy, preventing adapter drift.

  • Target/editor-bound dispatch — every Plugin action carries a Dispatch Receipt derived from the resolved connection. Stale epochs, wrong files, wrong editors and missing capabilities fail before mutation; malformed or mismatched terminal receipts are outcome-unknown and never authorize a write retry.

  • Owned Structured Render rollback — native single and batch renders preflight every plan through one shared mutation-free resource catalog, then journal only Command-created variables, collections, Text Styles, image handles and scene nodes. Failures clean removable entries in reverse order; non-removable or failed cleanup is reported as bounded residue rather than false atomicity.

  • Bounded Semantic Render outcomes — the Plugin response must match one closed, versioned and deeply bounded result before any Adapter can report success. CLI text, MCP structured content, JSON/YAML, Audit completion and process code project the same root, structural, resource and mutation facts; stacks, source payloads, file keys, credentials and arbitrary fields cannot enter that result.

  • One compound-render lifecycle — wrapper auto-split and render-batch use the same closed request through CLI and MCP. Gap/direction, placement, presets, collections, Registry links, icons, fallbacks, local assets and optional conversion/resize/verification stages are retained explicitly. Roots prepare before readiness, the visible render dispatches once, and bounded stage evidence preserves exact known partial handles on failure.

  • Host-materialized remote imagesfigma_render never gives the Plugin a remote URL. Explicit exact-origin policy, DNS/address checks, connection pinning, redirect reauthorization, identity encoding, byte/time limits and raster validation complete on the host inside the Command deadline. Local and remote rasters then enter the same embedded, content-hashed Plan shape; repeated references are read once per Command.

  • No shell — the engine is spawned with execFile (shell:false).

  • Two-layer daemon auth, no secret on the wire — signed HTTP requests (per-request HMAC over method/path/body, keyed with the session token, nonce replay guard) + a mutual challenge-response handshake on the plugin socket (Origin/Host allowlisted). Neither the session token nor the access key is ever transmitted in either direction — see Handshake.

  • Localhost-locked pluginplugin/manifest.json restricts networkAccess.allowedDomains to WebSocket and discovery HTTP traffic on localhost:3456–3460 only.

  • Isolated state — token, pid, key, and Audit Trail live under ~/.figma-bridge-mcp/, separate from any upstream figma-ds-cli install. A source checkout can additionally select FIGMA_BRIDGE_PROFILE=local-development for a distinct namespace and a daemon/plugin pinned to :3460 while the released profile remains active. Its importable bundle is generated below the gitignored plugin/local/ directory; tracked plugin/manifest.json always remains the normal released identity.

  • Private Audit Trail — commands attempt a closed, versioned start/outcome lifecycle in ~/.figma-bridge-mcp/audit.log. Capability, effects, redacted data summaries, outcome, duration, terminal Dispatch state and bounded mutation attempted/restored/residue counts remain useful to figma_history; raw commands, JSX, labels, paths, file keys, REST bodies, credentials, and errors or residue identities are never retained in new records. CLI errors and MCP structured errors consume the same closed mutation projection. Directory/file modes are 0700/0600. It rotates at 5 MB with one previous generation, which also bounds readable legacy entries. Storage failure never blocks Figma work: MCP output reports a degraded Audit Trail until a complete record succeeds.

Port fallback. The daemon binds the first free port in 3456–3460 and publishes it in ~/.figma-bridge-mcp/daemon-port; the CLI/MCP layers resolve the port per call (env DAEMON_PORT > port file > 3456), and the plugin scans the whole range, so a foreign process squatting 3456 no longer blocks connecting. After three bounded WebSocket scans, quiet /plugin-ready probes discover a returning daemon every three seconds without flooding Chromium with refused socket attempts. If the daemon is reachable but this iframe still cannot open its authenticated socket, the panel says that this Figma window is disconnected instead of claiming MCP is absent; another Figma window may still be connected. The discovery beacon still exposes no connection state or identity. A normal figma_connect is idempotent and preserves an authenticated, responsive socket. If that one socket is open but a live round-trip fails, it asks the persistent Figma plugin thread to recreate only the UI iframe; if the relay cannot answer, it resets that stale socket so quiet discovery can reconnect. Only explicit key rotation or daemon restart replaces the daemon. Stop/restart signals are sent only after the PID file and the published listening socket identify the same process, and escalation is limited to that listener PID rather than every client sharing the port. The squatter check is an unauthenticated /health probe, and authenticated requests are HMAC-signed — a squatter on a range port sees neither the session token nor anything replayable (signatures bind timestamp, nonce, method, path and body; the daemon rejects reused nonces). The plugin socket is safe on any range port for the same reason: the handshake below carries no secret and binds the port it ran on. Setting DAEMON_PORT explicitly disables the fallback; values outside 3456–3460 are unsupported — the plugin manifest is Figma-enforced and cannot reach them.

Handshake

The plugin socket runs a mutual challenge-response (proto 4, engine/src/lib/plugin-handshake.js):

daemon → plugin   {type:'challenge', proto:4, nonce:<dNonce>, port:<bound>}
plugin → daemon   {type:'hello', proto:4, nonce:<pNonce>, version, capabilities, proof}
daemon → plugin   {type:'hello-ack', connectionEpoch, proof, restTokenConfigured}

where proof = HMAC-SHA256(access key, transcript). The plugin proof binds both nonces, the bound port, the plugin version and its canonical capability list; the daemon proof binds both nonces, the port and its random connection epoch with a distinct role label and nonce ordering, so neither proof can be replayed as the other. Five properties follow:

  • The key never crosses the wire. A process that binds a range port before the daemon and records the whole exchange learns one HMAC over nonces it will never see again. This retires the residual risk earlier versions documented, where the raw key was the first frame the plugin sent.

  • The daemon proves itself too. Before proto 2 the plugin trusted whatever answered and would run any eval it was sent — impersonating the daemon needed no key at all. The panel now refuses every command until the ack verifies.

  • The bound port is inside the transcript and checked against the actual socket. A squatter on 3456 that forwards a challenge from the real daemon on 3457 is rejected before the plugin sends a proof. Rewriting the challenge to 3456 instead makes the daemon's verification fail.

  • Capabilities are authenticated. The daemon verifies the exact canonical capability claim the plugin signed, so a relay cannot add a newer write-lane safety capability to a stale plugin.

  • The connection epoch is authenticated. Every Dispatch Receipt uses the epoch signed by the daemon ack, so a result retained by the Plugin main thread across an iframe reconnect cannot impersonate a request from the new socket.

  • Write-lane settlement survives an iframe reconnect. The Plugin main thread keeps awaiting an uncancellable Figma operation after its caller times out, then sends lane-settled with the exact receipt. If the UI socket was disconnected, the same main thread replays that proof after reauthentication. Reconnect alone never releases quarantine; a new runtime lacks the random request identity. If a runtime was forcibly terminated before it could report settlement, recovery requires first stopping that old Plugin runtime, then explicitly restarting the Bridge Daemon—closing only the socket is not sufficient proof.

  • Status proves more than socket presence. Its bounded health operation is a closed typed read with no JavaScript payload and never enters the write lane. A responsive new connection epoch may release stale slots belonging only to non-mutating reads from an older epoch. Unknown writes remain quarantined until their exact settlement arrives; an attempted retry that reaches its deadline reports duplicate risk and tells the caller to inspect the targeted file before authoring another mutation.

There is no fallback to an older handshake protocol. figma_connect refreshes the installed plugin files on every run. Figma may continue using an application-cached plugin build, so the authenticated handshake reports the imported build separately from the bundled build. figma_status then gives an explicit re-import path instead of treating the mismatch as a generic connection failure.

Multiple Figma MCP servers

Figma Bridge can coexist with another Figma MCP. Its registered server namespace is figma-bridge and every tool result carries _mcp: "figma-bridge-mcp" plus protocol metadata that scopes failures to this server. Errors also repeat that a failed Bridge call does not establish that Figma or another Figma MCP is unavailable.

The bundled skills pin their workflows to Figma Bridge and begin with this server's figma_status. They never silently switch to a different MCP for a write, because another server may use different targeting, approval, retry and security semantics. An MCP cannot control which tool a host selects before the first call, so a generic task with several Figma servers remains a host/agent routing decision; once a Bridge skill is selected, the server identity and workflow contract make that route explicit and testable.

The panel carries its own SHA-256/HMAC implementation: the plugin UI is a sandboxed null-origin iframe, where WebCrypto availability is not ours to guarantee, and a silent fallback to something weaker is the worst outcome for an auth handshake. tests/plugin-handshake.test.js runs that shipped code against Node's crypto so the two implementations cannot drift apart.

Known limitations

  • Figma can terminate the complete plugin runtime. Reconnect loops and UI reloads work only while at least the plugin main thread is still running. If Figma destroys both the iframe and main thread, no plugin JavaScript remains that could reopen itself; start Plugins → Development → Figma Bridge once in that file. The stored access key is reused and discovery reconnects it automatically, so no new pairing is needed.

  • Figma Slides is beta and deliberately bounded. Grid inspection, slide create/duplicate/move/delete, skip state and transitions are supported. Speaker notes, interactive polls/embeds, presenter controls and a complete content-authoring workflow are not. See the Slides roadmap for actionable candidates versus Plugin API boundaries.

  • Non-localhost network actions are few and explicit: api setup has no network effect; it verifies the exact official @figma/plugin-typings dependency and atomically builds a private offline reference for figma_reference. External access is limited to the explicitly approved Storybook origin used by import/map storybook (or the local directory you pass), and — only when you opt into the REST add-on — calls to api.figma.com. Nothing else talks to the network — the upstream's iconify/unsplash/remove.bg/screenshot-url integrations were removed entirely; <Icon> in figma_render JSX renders as a named placeholder (real icons come out of the Figma file via export assets).

  • One transport, no CDP remnants. Every command reaches Figma the same way: engine → daemon → plugin eval. The upstream's Chrome-DevTools client, its figma-use shell round-trip, the binary-patching init wizard and the figma-use dependency are all gone (~5,600 lines removed), so there is no second code path that could bypass the plugin bridge.

Development

npm run check:contracts       # separate host + Figma Plugin static contracts
npm run check:plugin          # generated Plugin runtime has no source drift
npm run check:architecture-latency # warmed latency budget in an idle process
npm run measure:architecture  # context, payload and local latency baselines
npm run build:package-artifact -- --output-dir release-artifact # reproducible reviewed tarball + evidence
npm test                      # all contracts and regression suites

The current domain language lives in CONTEXT.md, API coverage in docs/figma-plugin-api-coverage.md. The public documentation index is docs/README.md.

Avoid running an upstream figma-cli at the same time. The daemon now falls back within 3456–3460 when 3456 is taken, so both can coexist, but the plugin scans the whole range and the two daemons use different access keys — which one the plugin reaches first is a coin toss. This build isolates its own token/pid/port files under ~/.figma-bridge-mcp/.

License

figma-bridge-mcp is released under the MIT License. It is provided "as is", without warranty; the exact warranty and liability terms are in the license itself. Third-party copyright and license notices are retained in NOTICE and engine/LICENSE.

Inspiration & attribution

Two projects shaped this one, in different ways.

figma-cli (Sil Bormüller) is where the engine/ directory comes from: it was vendored at v2.1.0 in July 2026 and has diverged since — the CDP transport and the binary-patching installer are gone, the plugin socket is authenticated, and most of what the engine does now was written here. The remaining explicitly derived source files have all diverged from their upstream bytes. The upstream MIT license is retained in full at engine/LICENSE, and NOTICE records what changed.

Attribution inventory: 3 retained modified-derived source files, 0 byte-identical source files, and 1 verbatim upstream license file. The exact revision, paths, and reference hashes are machine-verifiable in UPSTREAM_PROVENANCE.json.

figma-console-mcp contributed an idea rather than code: that a Figma bridge can be genuinely local — a plugin socket on the loopback interface, no cloud relay, no patched binary. Nothing here is derived from its source; the tool surfaces, the transport and the plugin are unrelated. Where this project differs is that the socket also proves who is on the other end of it.

Plugin identity. The development manifests use the product-aligned ids figma-bridge-mcp and figma-bridge-mcp-dev. Figma scopes clientStorage — where the paired access key lives — to the plugin id and, in the desktop editors, separately across Design, FigJam and Slides. Installations from before 0.5.0 therefore need to re-import the manifest; each editor in use needs the same Bridge access key saved once.

Available Tools

12 tools
figma_commentsA

REST comments: list feedback; post/reply always previews and needs confirm:true. Missing token returns setup.

ParametersJSON Schema
NameRequiredDescriptionDefault
xNopost: canvas x (with y, when no nodeId) or node offset x.
yNopost: canvas y / node offset y.
actionNolist (default) or post.
nodeIdNopost: node anchor (id or URL).
confirmNoRequired true to actually post — first call without it returns a preview.
fileKeyNoFile key/URL; defaults to the open file.
messageNopost: the comment text.
replyToNopost: comment id to reply to (threads under it).

TDQS

A3.8/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description adds meaningful behavioral information beyond the annotations: all post/reply calls preview and require confirm:true, and a missing token returns setup. The annotations only indicate readOnlyHint=false, destructiveHint=false, and openWorldHint=true, so the confirmation and auth details are valuable additions.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and front-loads the purpose and critical confirm behavior. Both sentences earn their place, though 'REST comments' is slightly cryptic and the semicolon-heavy first sentence could be clearer as a full phrase.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The schema covers parameters well and the description adds critical behavioral details, but with no output schema the return values are not described: no mention of what list returns, what a preview response looks like, or pagination. For an 8-parameter tool with multiple modes, this leaves some gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all eight parameters. The description mentions confirm:true, but the schema already explains the preview and confirmation flow; no additional parameter semantics are provided. Baseline 3 is appropriate because the schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description identifies the resource (comments) and the core actions: list feedback, and post/reply with preview behavior. It is clear enough to distinguish this from sibling tools like figma_inspect or figma_spec, though 'REST comments' is a bit terse and doesn't explicitly say 'Figma file comments'.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives practical usage context: listing is the default action, while posting/reply always goes through a preview and requires confirm:true. It also notes the missing-token setup response. It does not explicitly compare to alternatives, but no sibling tool appears to handle comments, so this is acceptable.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

figma_connectC

Connect Safe Mode; recover the plugin, create its key, or show setup.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

C2.9/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations are present: readOnlyHint=false, openWorldHint=true, destructiveHint=false. The description adds that it may 'recover', 'create its key', or 'show setup', suggesting a setup/recovery operation with possible side effects (creating a key). While it doesn't conflict with annotations, it doesn't clearly describe side effects or what 'connect Safe Mode' entails; openWorldHint=true is consistent but vague. No contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is short and front-loaded with the main verb. However, the semicolon list of three actions is somewhat compressed and could be more explicit, but it is not bloated.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given that this is an action-y tool with zero parameters, the description is too vague about what 'Connect Safe Mode' actually does, what the outcomes are, and how it differs from figma_pairing/figma_status. An agent would not know what to expect after calling it. The output schema is missing, which increases the need for behavioral context.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has zero parameters, so the description carries no parameter burden. Baseline for 0 params is 4. The description's ambiguity about actions is not necessarily about parameters, so this score is fair.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose3/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb ('Connect') and resource ('Safe Mode'), and mentions three possible actions: recover the plugin, create its key, or show setup. However, it is ambiguous what 'connect safe mode' actually means and which of these actions will occur on invocation. It distinguishes a bit from siblings like figma_pairing and figma_status, but not clearly enough.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no guidance on when to use this tool versus alternatives like figma_pairing or figma_status. The description implies it is for a setup/recovery scenario, but it doesn't state conditions, prerequisites, or when to prefer another sibling.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

figma_historyB
Read-only

Private command history, git/Figma versions and snapshot diff; payloads omitted.

ParametersJSON Schema
NameRequiredDescriptionDefault
diffNoDiff live/latest/previous, an index, or a Figma version.
limitNoDefault 20; max 200.
formatNoDefault markdown.
nodeIdNoNode-id filter.
fileKeyNoFigma key/URL for versions.
gitPathsNoRepo-relative paths for git history.
repoPathNoRepo root for gitPaths.
includeVersionsNoMerge Figma versions via optional REST.

TDQS

B3.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover readOnly/openWorld/non-destructive, and the description adds useful behavior: 'private' scopes the history and 'payloads omitted' discloses a response constraint. It does not detail auth/rate limits or the optional REST merge, but with annotations present this is a reasonable level of disclosure.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Twelve words with no filler; the opening 'Private command history' sets scope immediately and the trailing 'payloads omitted' is a high-value boundary. Every phrase earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite rich schema and annotations, this is an 8-parameter multi-mode tool with no output schema, and the description gives no mode-selection logic between diff, fileKey/includeVersions, and gitPaths/repoPath. It also does not clarify what 'private' entails or what the output shape looks like beyond payloads being omitted, leaving the agent to infer the intended workflow.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the baseline applies; to/from defaults, nodeId, fileKey, gitPaths, repoPath, format, limit, and includeVersions are already explained in the schema. The description's mention of versions and diffs maps to those parameters but adds no parameter-level detail beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names concrete resources—private command history, git/Figma versions, and snapshot diff—so an agent understands what domain the tool addresses. It lacks an explicit verb like 'retrieves' or 'lists' and does not name a sibling, but the resource set is specific enough to separate figma_history from figma_screenshot, figma_comments, and similar siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No sentence explains when to use this tool or when to prefer one of its sibling tools. The capability list implies a history/diff use case, but there is no exclusion guidance or alternative routing, which is important given 11 sibling tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

figma_inspectA
Read-only

Inspect node geometry, paint, effects, component and text facts as YAML.

ParametersJSON Schema
NameRequiredDescriptionDefault
nodeIdYesFigma node id ("1:2"), URL form ("1-2"), or a full Figma URL.
fileKeyNoTarget connected file: bare key or Figma URL.

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The annotations already declare readOnlyHint=true and destructiveHint=false. The description adds useful behavioral context by specifying the output as YAML and enumerating the fact categories covered. It does not contradict the annotations, though it omits details about failure modes or file resolution.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence with no filler. It front-loads the action and resource, then states the output format, earning full credit for conciseness.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple read-only inspection tool, the description is adequate: it names the output format, the fact categories, and the schema documents parameters. There is no output schema, but the description partially compensates by indicating YAML results. It could specify exact response structure or edge-case behavior, but these are minor gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so both nodeId and fileKey are already well documented. The description adds selection context through 'node facts' but does not provide additional parameter semantics beyond the schema. Baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb ('Inspect') with a clear resource ('node geometry, paint, effects, component and text facts') and an explicit output format ('as YAML'). This makes it readily distinguishable from visual siblings like figma_render or figma_screenshot, even without naming them.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description clearly implies when to use this tool: when an agent needs structured node facts rather than rendered output. It does not explicitly name alternatives or state when not to use it, but the context is sufficiently clear for an agent to select it appropriately.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

figma_pairingA
Destructive

Show the plugin key; rotate:true replaces it and restarts the daemon.

ParametersJSON Schema
NameRequiredDescriptionDefault
rotateNoReplace the key and restart the daemon.

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description explicitly discloses the destructive behavior: rotate:true replaces the key and restarts the daemon. This aligns with destructiveHint=true and adds specific context about what is affected, which goes beyond the annotation alone.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single semicolon-separated sentence with no filler. It front-loads the main purpose and then introduces the optional variant, making it easy to parse quickly.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple tool with one optional parameter and no output schema, the description covers the core behavior and the destructive branch sufficiently. It omits return-value details and usage prerequisites, but these are minor given the tool's low complexity.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema fully documents the rotate parameter, but the description adds the default behavior of simply showing the key when rotate is not true. This clarifies the optional nature of the parameter and the outcome of the default call.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the primary action ('Show the plugin key') and the optional destructive variant ('rotate:true replaces it and restarts the daemon'). This distinguishes figma_pairing from its siblings by specifying a unique resource and operation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance is provided on when to use figma_pairing versus its siblings, nor when rotation is appropriate or what prerequisites exist. The only condition mentioned is the rotate flag, but not the context that should trigger it.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

figma_referenceB
Read-only

Offline API reference plus capabilities, fidelity, variable scopes and workflows.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameNoAPI name or special topic; omit to list names.

TDQS

B3.4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already mark the tool as readOnlyHint=true and destructiveHint=false, so the agent knows this is a safe read operation. The description adds the 'Offline' trait and the content scope, which gives some behavioral context, but it doesn't disclose specifics like list behavior on omitted name or return content format. Given annotation coverage, this meets a baseline without adding much beyond it.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single sentence that leads with the core identity ('Offline API reference') and enumerates topics without filler. It is appropriately small and front-loaded, with no redundant phrasing.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a single-optional-parameter, read-only reference tool with no output schema, the description names the resource and its topic areas, and the schema covers invocation details. It lacks explicit instruction that omitting name lists all entries, but that is already in the schema; the main gap is unclear differentiation from sibling tools, which is more a usage-guideline issue. Overall it is sufficient for the tool's low complexity.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the parameter is fully documented in the schema with 'API name or special topic; omit to list names.' The description contributes no additional parameter semantics beyond the schema, so the baseline score of 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description identifies the tool as an 'Offline API reference' and lists the domains it covers (capabilities, fidelity, variable scopes, workflows), which signals a lookup/reference function. However, it lacks an explicit verb like 'query' or 'look up,' and it does not differentiate itself from siblings like figma_spec or figma_inspect, so an agent must infer the action from the noun.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance is given on when to use this tool instead of alternatives such as figma_spec or figma_inspect. The term 'Offline' hints at a network-independent use case, but no explicit when/when-not conditions or alternative tool references appear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

figma_renderC

Render one JSX value or roots[] batch into Figma.

ParametersJSON Schema
NameRequiredDescriptionDefault
xNo
yNo
gapNo
jsxNo
iconsNo
labelNoOptional intent stored only as non-reversible audit metadata.
rootsNo
presetNo
verifyNo
confirmNoRequired by policy; omission is not a preview.
fileKeyNoFigma key/URL.
previewNoReturn a payload-free, non-mutating Command Plan.
manifestNo
directionNo
collectionNo
asComponentNo
keepWrapperNo
resizeProbeNo
smartPositionNo
allowedFreePathsNo
approvedFallbacksNo

TDQS

C2.5/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already indicate readOnlyHint=false and openWorldHint=true, so the description adds no new behavioral insight. It does not explain side-effects, scope of mutation, reversibility, or the confirm/preview flow. Given the open-world hint, more disclosure would be expected, but the description stays silent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The single-sentence description is concise and free of filler, but it lacks structure and does not front-load critical information like the required confirm parameter or the preview mode. It is appropriately short but at the cost of essential detail.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness1/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with 21 parameters, no output schema, and low schema coverage, the description is severely incomplete. It fails to explain the difference between jsx and roots, the meaning of most parameters, or the behavioral implications. An agent would be guessing at almost every operational aspect.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is only 19%, so the description must compensate, but it only mentions jsx and roots as primary inputs. The remaining 19 parameters (x, y, gap, preset, verify, confirm, etc.) are left unexplained. The description adds only marginal meaning beyond the schema, far from sufficient for correct invocation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action (render), the inputs (JSX or roots[] batch), and the target (Figma). It distinguishes between two input modes, giving a specific verb-resource pair. However, it does not explicitly differentiate it from sibling tools like figma_run or figma_screenshot, so it misses the distinguishing context.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no guidance on when to use this tool versus alternatives. No mention of alternatives, conditions, or exclusions. The description simply declares what the tool does without any contextual routing, leaving an agent without criteria for tool selection.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

figma_runB
Destructive

Run an approved command. See figma_reference {name:"capabilities"} or --help.

ParametersJSON Schema
NameRequiredDescriptionDefault
argsYesCommand argv, e.g. ["canvas","info"].
forceNoAsset Export: bypass completed retry cache.
labelNo
confirmNoRequired by policy; omission is not a preview.
fileKeyNoFigma key/URL.
previewNoReturn a payload-free, non-mutating Command Plan.

TDQS

B3.3/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already flag this as destructive (destructiveHint: true), and the description adds no behavioral nuance beyond 'approved.' It does not mention preview mode, the policy-required confirm flag, mutations, or consequences, so the description contributes little beyond the structured hints.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

One short sentence states the core behavior and immediately directs the agent to the authoritative capability reference. There is no filler or redundant restatement of schema or annotations.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The pointer to figma_reference and --help gives an agent a discovery path, and the schema covers most parameters, but the description alone leaves key operating context unclear: what commands exist, how approval is determined, and what preview/confirm semantics mean operationally. Given the destructive hint and lack of an output schema, more context would improve call correctness.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 83%, so the parameters are mostly self-documenting; the description itself adds no parameter-level detail. Per the high-coverage baseline, a 3 is appropriate rather than a lower score.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names a specific action ('Run an approved command') and distinguishes this as the executor tool while pointing to figma_reference for allowed commands. It is not a tautology, but the command space itself is left unspecified until the agent consults the reference.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The instruction to see figma_reference {name:'capabilities'} or --help implies how to identify approved commands, and the word 'approved' imposes a usage constraint. However, it does not explicitly state when to prefer this tool over sibling tools or what conditions disqualify a command.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

figma_screenshotB
Read-only

MANDATORY: save/read the node PNG before comparing the build.

ParametersJSON Schema
NameRequiredDescriptionDefault
scaleNoScale (default 0.5; 2000px cap); returns exact pixel/logical sizes.
nodeIdNoFigma node id ("1:2"), URL form ("1-2"), or a full Figma URL. Omit to use the current selection.
fileKeyNoTarget connected file: bare key or Figma URL.

TDQS

B3.2/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The annotations already declare readOnlyHint=true and destructiveHint=false, and the description does not contradict them; 'save/read' reinforces a read-oriented behavior. However, the description adds little beyond that—no detail about output format, persistence, caching, or side effects.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is very short and front-loads the key directive 'MANDATORY,' making it efficient to parse. However, the terseness sacrifices a clear purpose statement and leaves much to inference.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no output schema and many sibling tools, the description is incomplete: it does not explain what the PNG contains, how it is returned or saved, how optional parameters interact with the current-selection fallback, or what 'comparing the build' means. An agent would need additional context to invoke this tool confidently.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so scale, nodeId, and fileKey are already fully documented. The tool description adds no extra parameter semantics or usage nuances beyond what the schema provides.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose3/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description identifies a resource (node PNG) and an action (save/read), but it reads as a workflow command—'MANDATORY: save/read the node PNG before comparing the build'—rather than a clear statement of tool function. It does not explicitly say that the tool captures a screenshot of a Figma node, and it does not distinguish itself from sibling tools like figma_render.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides an explicit temporal condition: this tool is mandatory before comparing the build. This gives clear context for when to invoke it, though it does not mention when not to use it or how it compares to alternative tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

figma_selectionB
Read-only

Read the current Figma selection and reuse its id.

ParametersJSON Schema
NameRequiredDescriptionDefault
fileKeyNoFigma key/URL.

TDQS

B3.4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already indicate this is read-only and non-destructive, so the description does not contradict them. It adds the notion that the tool depends on a 'current' selection state, but does not disclose what happens when there is no active selection or whether the returned id is a node id. This is acceptable but not rich.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single sentence with no filler or redundant phrasing. The purpose and output are stated directly, making it efficiently scannable for an agent.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a low-complexity tool with one optional parameter, full schema coverage, and safety annotations, the description is nearly complete. It conveys that the tool returns a reusable id, though it could mention behavior for an empty selection or confirm the return format.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the fileKey parameter is already described as 'Figma key/URL.' The description adds no additional parameter-level meaning, which is consistent with the baseline of 3.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a clear verb ('Read') and a specific resource ('the current Figma selection'), and indicates the output ('its id'). It distinguishes itself from rendering/screenshot/comment tools, though it does not explicitly name any sibling to differentiate from.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no explicit guidance about when to use this tool versus alternatives like figma_inspect or figma_reference, nor any mention of prerequisites such as an active Figma session or a non-empty selection. The usage is only implied by the verb 'Read' and the notion of a 'current selection'.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

figma_specA
Read-only

Exact layer facts; never invent. nodeIds[] batches 8 reads behind one approval.

ParametersJSON Schema
NameRequiredDescriptionDefault
dedupNofalse: inline every layer; true: compact repeats.
depthNoTree depth; 0 = node only, default 12, max 30.
phaseNoall (default), structure, or style.
formatNotree (default), yaml, or json.tree
nodeIdNoNode id or Figma URL.
fileKeyNoTarget connected file: bare key or Figma URL.
nodeIdsNoNode ids for one bounded batch; phase/depth/format options apply to every id.
sectionNoOptional child layer name.
includeHiddenNoInclude invisible nodes (default false).

TDQS

A3.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already establish readOnlyHint=true and destructiveHint=false, so the safety profile is covered. The description adds meaningful beyond-annotation behavior: a 'never invent' no-hallucination promise and batching behind a single approval. It does not mention output shape or errors, but the read-only nature is well covered.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The entire description is two short sentences with zero filler. It front-loads the core promise ('Exact layer facts; never invent') and follows with a specific, actionable batching note. Every word earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

This is a complex tool: 9 optional parameters, anyOf constraints, no output schema, and 11 sibling tools. Two brief sentences do not provide enough context for an agent to understand what 'spec' returns, how nodeId and nodeIds interact, or when to pick this over figma_inspect/reference. The schema fully covers parameter syntax, but not tool selection or output expectations.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the 9 parameters are already fully documented in the input schema. The description adds a small extra semantic around nodeIds — that batching 8 reads happens behind one approval — but it does not compensate for or clarify the other parameters, so the baseline of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The phrase 'Exact layer facts' clearly identifies the resource (layers) and the nature of the output (facts, not guesses), while 'never invent' signals a fidelity guarantee. It does not use an explicit verb like 'get' or 'return', and it does not explicitly distinguish itself from figma_inspect or figma_reference, so it stops short of a 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The line 'nodeIds[] batches 8 reads behind one approval' gives actionable guidance to batch multiple reads for efficiency, which is genuinely useful. However, it never says when to choose figma_spec over sibling tools or when not to use it, so the guidance remains implied rather than explicit.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

figma_statusB
Read-only

This Figma Bridge only: round-trip status, write policy, optional REST check.

ParametersJSON Schema
NameRequiredDescriptionDefault
fileKeyNoFigma key/URL for REST validation.
probePluginNoProbe plugin eval; false checks only its socket.
validateRestNoValidate the optional REST token.

TDQS

B3.1/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare the operation as read-only, non-destructive, and open-world. The description adds that it performs a round-trip status check and can validate a REST token, which is useful, but it does not clarify what 'write policy' means or how the optional REST check behaves when skipped.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact, scannable, and front-loaded with scope and purpose. It loses a point because the telegraphic fragment style creates mild ambiguity, for example whether 'Figma Bridge only' is an API restriction or a domain qualifier.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description should explain what a successful status response contains or what a negative result means, but it does not. It also leaves 'write policy' and 'round-trip' behavior underspecified, so an agent can select the tool but not confidently interpret its result.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, and the schema already documents fileKey, probePlugin, and validateRest clearly. The description's high-level status/REST phrasing maps to those parameters but adds no additional semantic detail beyond what the input schema provides.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description identifies the resource (Figma Bridge) and the three relevant facets: round-trip status, write policy, and optional REST check. It is not a tautology, though it lacks an explicit verb and does not name a sibling tool for differentiation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no when-to-use guidance and no mention of alternative tools such as figma_connect or figma_pairing. The phrase 'Figma Bridge only' restricts scope but does not help an agent decide when this status tool is the right choice.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

  1. 10 tool updatesv0.6.1
    • Changedfigma_comments2 fields changed
      • changedInput schema / properties / fileKey / description
        Previous value: -"Bare file key or full Figma URL. Default: the file open in Figma Desktop."New value: +"File key/URL; defaults to the open file."
      • changedInput schema / properties / nodeId / description
        Previous value: -"post: anchor the comment to this node (\"1:2\", URL form, or full URL)."New value: +"post: node anchor (id or URL)."
    • Changedfigma_history12 fields changed
      • changedInput schema / properties / diff / description
        Previous value: -"Structural diff instead of the log: what nodes were added, removed, recreated, moved or changed between two states. Refs are \"live\" (the document right now), \"latest\"/\"previous\", an index, or a Figma version id (needs the REST layer). Record comparison points with figma_run [\"history\",\"snapshot\"]."New value: +"Diff live/latest/previous, an index, or a Figma version."
      • changedInput schema / properties / diff / properties / changelog / description
        Previous value: -"Emit a markdown changelog instead of the terse report."New value: +"Return markdown changelog."
      • changedInput schema / properties / diff / properties / from / description
        Previous value: -"Older side. Default \"previous\"."New value: +"Older ref; default previous."
      • changedInput schema / properties / diff / properties / nodeId / description
        Previous value: -"Subtree root when a side is \"live\" (default: current page)."New value: +"Live subtree root."
      • changedInput schema / properties / diff / properties / to / description
        Previous value: -"Newer side. Default \"latest\". Use \"live\" for the current document."New value: +"Newer ref; default latest."
      • changedInput schema / properties / fileKey / description
        Previous value: -"File for includeVersions: bare key or full Figma URL. Default: the file open in Figma Desktop."New value: +"Figma key/URL for versions."
      • changedInput schema / properties / format / description
        Previous value: -"Output format (default markdown table)."New value: +"Default markdown."
      • changedInput schema / properties / gitPaths / description
        Previous value: -"Repo-relative file paths whose git log to merge in (generated code files)."New value: +"Repo-relative paths for git history."
      • changedInput schema / properties / includeVersions / description
        Previous value: -"Also merge the file's Figma version history (what designers saved, by whom) via the optional REST layer. Needs a configured REST token; without one a note is appended instead."New value: +"Merge Figma versions via optional REST."
      • changedInput schema / properties / limit / description
        Previous value: -"Max entries (default 20, max 200)."New value: +"Default 20; max 200."
      • changedInput schema / properties / nodeId / description
        Previous value: -"Only entries touching this node id (\"1:2\" or URL form \"1-2\")."New value: +"Node-id filter."
      • changedInput schema / properties / repoPath / description
        Previous value: -"Repo root for gitPaths (default: server working directory)."New value: +"Repo root for gitPaths."
    • Changedfigma_pairing1 field changed
      • changedInput schema / properties / rotate / description
        Previous value: -"Generate a NEW key, invalidating the old one. Run figma_connect afterwards to restart the daemon."New value: +"Replace the key and restart the daemon."
    • Changedfigma_reference1 field changed
      • changedInput schema / properties / name / description
        Previous value: -"API name, capabilities or workflow topic; omit to list API names."New value: +"API name or special topic; omit to list names."
    • Changedfigma_render22 fields changed
      • addedInput schema / properties / allowedFreePaths
        Added value: +{
        +  "items": {
        +    "minLength": 1,
        +    "type": "string"
        +  },
        +  "maxItems": 100,
        +  "type": "array"
        +}
      • addedInput schema / properties / approvedFallbacks
        Added value: +{
        +  "items": {
        +    "minLength": 1,
        +    "type": "string"
        +  },
        +  "maxItems": 100,
        +  "type": "array"
        +}
      • addedInput schema / properties / asComponent
        Added value: +{
        +  "type": "boolean"
        +}
      • addedInput schema / properties / collection
        Added value: +{
        +  "minLength": 1,
        +  "type": "string"
        +}
      • changedInput schema / properties / confirm / description
        Previous value: -"Required when write-confirm mode is on."New value: +"Required by policy; omission is not a preview."
      • addedInput schema / properties / direction
        Added value: +{
        +  "enum": [
        +    "row",
        +    "col"
        +  ],
        +  "type": "string"
        +}
      • changedInput schema / properties / fileKey / description
        Previous value: -"Target connected file: bare key or Figma URL."New value: +"Figma key/URL."
      • addedInput schema / properties / gap
        Added value: +{
        +  "maximum": 10000,
        +  "minimum": 0,
        +  "type": "number"
        +}
      • addedInput schema / properties / icons
        Added value: +{
        +  "minLength": 1,
        +  "type": "string"
        +}
      • removedInput schema / properties / jsx / description
        Removed value: -"JSX markup to render."
      • addedInput schema / properties / keepWrapper
        Added value: +{
        +  "type": "boolean"
        +}
      • changedInput schema / properties / label / description
        Previous value: -"Optional audit-log intent note."New value: +"Optional intent stored only as non-reversible audit metadata."
      • addedInput schema / properties / manifest
        Added value: +{
        +  "minLength": 1,
        +  "type": "string"
        +}
      • addedInput schema / properties / preset
        Added value: +{
        +  "enum": [
        +    "macbook-14",
        +    "macbook-16",
        +    "desktop",
        +    "desktop-hd",
        +    "iphone-15",
        +    "iphone-15-pro-max",
        +    "iphone-se",
        +    "android",
        +    "ipad-11",
        +    "ipad-13"
        +  ],
        +  "type": "string"
        +}
      • addedInput schema / properties / preview
        Added value: +{
        +  "description": "Return a payload-free, non-mutating Command Plan.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / resizeProbe
        Added value: +{
        +  "maximum": 2000,
        +  "minimum": 1,
        +  "type": "number"
        +}
      • addedInput schema / properties / roots
        Added value: +{
        +  "items": {
        +    "minLength": 1,
        +    "type": "string"
        +  },
        +  "maxItems": 100,
        +  "minItems": 1,
        +  "type": "array"
        +}
      • addedInput schema / properties / smartPosition
        Added value: +{
        +  "type": "boolean"
        +}
      • addedInput schema / properties / verify
        Added value: +{
        +  "type": "boolean"
        +}
      • addedInput schema / properties / x
        Added value: +{
        +  "type": "number"
        +}
      • addedInput schema / properties / y
        Added value: +{
        +  "type": "number"
        +}
      • removedInput schema / required
        Removed value: -[
        -  "jsx"
        -]
    • Changedfigma_run6 fields changed
      • changedInput schema / properties / args / description
        Previous value: -"Engine subcommand and flags, e.g. [\"canvas\",\"info\"]."New value: +"Command argv, e.g. [\"canvas\",\"info\"]."
      • changedInput schema / properties / confirm / description
        Previous value: -"Required for write commands when write-confirm mode is on."New value: +"Required by policy; omission is not a preview."
      • changedInput schema / properties / fileKey / description
        Previous value: -"Target connected file: bare key or Figma URL."New value: +"Figma key/URL."
      • addedInput schema / properties / force
        Added value: +{
        +  "description": "Asset Export: bypass completed retry cache.",
        +  "type": "boolean"
        +}
      • removedInput schema / properties / label / description
        Removed value: -"Optional audit-log intent note."
      • addedInput schema / properties / preview
        Added value: +{
        +  "description": "Return a payload-free, non-mutating Command Plan.",
        +  "type": "boolean"
        +}
    • Changedfigma_screenshot1 field changed
      • changedInput schema / properties / scale / description
        Previous value: -"Export scale (default 0.5, capped at 2000px max dimension). The result reports the applied scale — rendered pixels = node size × scale."New value: +"Scale (default 0.5; 2000px cap); returns exact pixel/logical sizes."
    • Changedfigma_selection1 field changed
      • changedInput schema / properties / fileKey / description
        Previous value: -"Target connected file: bare key or Figma URL."New value: +"Figma key/URL."
    • Changedfigma_spec10 fields changed
      • addedInput schema / anyOf
        Added value: +[
        +  {
        +    "required": [
        +      "nodeId"
        +    ]
        +  },
        +  {
        +    "required": [
        +      "nodeIds"
        +    ]
        +  }
        +]
      • changedInput schema / properties / dedup / description
        Previous value: -"false (default): inline every layer; true: compact S<n>/repeat refs."New value: +"false: inline every layer; true: compact repeats."
      • changedInput schema / properties / depth / description
        Previous value: -"Max tree depth (default 12). Use 0 for the requested node itself, complete and without descendants."New value: +"Tree depth; 0 = node only, default 12, max 30."
      • changedInput schema / properties / format / description
        Previous value: -"tree (default) is readable; yaml/json are lossless canonical adapters."New value: +"tree (default), yaml, or json."
      • changedInput schema / properties / includeHidden / description
        Previous value: -"Also list invisible nodes, marked hidden (default false). Useful to understand what a variant toggle would reveal."New value: +"Include invisible nodes (default false)."
      • changedInput schema / properties / nodeId / description
        Previous value: -"Frame node id, URL-form id, or full Figma URL."New value: +"Node id or Figma URL."
      • addedInput schema / properties / nodeIds
        Added value: +{
        +  "description": "Node ids for one bounded batch; phase/depth/format options apply to every id.",
        +  "items": {
        +    "type": "string"
        +  },
        +  "maxItems": 8,
        +  "minItems": 1,
        +  "type": "array"
        +}
      • changedInput schema / properties / phase / description
        Previous value: -"structure = hierarchy + real content; style = layout/paint/typography detail; all (default) = both."New value: +"all (default), structure, or style."
      • changedInput schema / properties / section / description
        Previous value: -"Optional child layer name from the structure map. Prefer its node id when names repeat."New value: +"Optional child layer name."
      • removedInput schema / required
        Removed value: -[
        -  "nodeId"
        -]
    • Changedfigma_status3 fields changed
      • changedInput schema / properties / fileKey / description
        Previous value: -"File key/URL for REST file-access validation fallback."New value: +"Figma key/URL for REST validation."
      • changedInput schema / properties / probePlugin / description
        Previous value: -"Plugin eval probe (default true); false = socket only."New value: +"Probe plugin eval; false checks only its socket."
      • changedInput schema / properties / validateRest / description
        Previous value: -"Also validate the optional REST token remotely."New value: +"Validate the optional REST token."
  2. 12 tool updatesv0.5.1
    • First observedfigma_comments
    • First observedfigma_connect
    • First observedfigma_history
    • First observedfigma_inspect
    • First observedfigma_pairing
    • First observedfigma_reference
    • First observedfigma_render
    • First observedfigma_run
    • First observedfigma_screenshot
    • First observedfigma_selection
    • First observedfigma_spec
    • First observedfigma_status

TDQS

B3.1/5.0
Disambiguation3/5

Most tools are distinct, but figma_inspect and figma_spec both target node/layer facts, and figma_connect, figma_status, and figma_pairing all relate to setup/connection. The descriptions provide enough detail to choose correctly in most cases, but the boundaries are not fully crisp.

Naming Consistency3/5

All tools share the figma_ prefix and snake_case, but the suffix pattern is mixed: connect, run, render, and inspect are actions, while status, pairing, comments, history, and spec are nouns. The convention is readable and consistent in prefix, but not consistently verb_noun.

Tool Count4/5

Twelve tools is a reasonable size for a Figma bridge covering setup, rendering, inspection, comments, screenshots, and history. Some tools overlap slightly and could be consolidated, but the count is neither excessive nor too thin.

Completeness4/5

The surface covers connection/setup, status, rendering, selection, comments, node inspection, specs, screenshots, history, and an offline reference. Minor gaps like comment update/delete or explicit file-scoped operations exist, but core workflows appear supported.

Maintenance

ActivityActive
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    An open-source MCP server that gives AI assistants full read-write access to Figma, enabling creation, editing, and deletion of designs directly without plugins or API keys.
    24
    10
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    A self-hosted MCP server that enables AI agents to retrieve Figma design data for generating code, templates, or custom prompts.
    2,160
    -

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/KaiUweHella/figma-bridge-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server