
Company News
Jerod Santo Joins Socket as Head of Media
Allow myself to introduce... myself.
figma-bridge-mcp
Advanced tools
Local MCP server for inspecting and editing Figma Desktop through an authenticated localhost plugin bridge.
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.
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"}.
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.
figma_connect
directly. It starts the local bridge and returns an access key plus a plugin
manifest path.Plugins → Development → Import plugin from manifest…
and choose ~/.figma-bridge-mcp/plugin/manifest.json (the path
returned by figma_connect).Plugins → Development → Figma Bridge in Figma Design, paste the
access key, and click Save & connect.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.
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:
~/.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.Select a frame or layer in Figma and describe the outcome you want. For example:
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.
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:
figma_status show your handle.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.
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
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.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.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.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.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.figma_reference {name:"fidelity"}.FIGMA_BRIDGE_EXTRA_ROOTS (use the platform path-list separator: : on
macOS/Linux, ; on Windows). The launch directory always remains allowed.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./health, /exec) require a per-request HMAC signature
keyed with the session token, a 0600 file — the token itself never crosses
the wire./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.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.| 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>
The Figma Bridge plugin window is more than the connection status:
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.figma_selection will return. Select a frame, say "build this" —
no node-id copying.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.
figma_screenshot on the target frame, then read the saved PNG — the
visual ground truth. Never build from a node tree alone.
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.
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.
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.
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.
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.
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.
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.{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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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_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.@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.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.figma_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.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.figma_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.execFile (shell:false).Origin/Host allowlisted). Neither the session token nor the access key is
ever transmitted in either direction — see Handshake.plugin/manifest.json restricts
networkAccess.allowedDomains to WebSocket and discovery HTTP traffic on
localhost:3456–3460 only.~/.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.~/.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.
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:
eval it was sent — impersonating the daemon
needed no key at all. The panel now refuses every command until the ack
verifies.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.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.
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.
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).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.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/.
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.
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.
FAQs
Local MCP server for inspecting and editing Figma Desktop through an authenticated localhost plugin bridge.
The npm package figma-bridge-mcp receives a total of 59 weekly downloads. As such, figma-bridge-mcp popularity was classified as not popular.
We found that figma-bridge-mcp demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Company News
Allow myself to introduce... myself.

Research
/Security News
A Twitch browser extension on Chrome and Firefox forwards users’ live OAuth session tokens through proxies controlled by a Russian bot service.

Security News
Anthropic found biased reasoning and recklessness drove Claude Mythos 5 to publish malware on PyPI and compromise a security vendor.