Skip to main content
Glama
Figma Local Bridge

Figma Local Bridge

by Miart

Figma Local Bridge

Local MCP server for Figma Desktop. Read and edit the open Figma file from Claude Code, OpenCode, Cursor, or any MCP client — including ChatGPT and Gemini clients that support local MCP — no Figma Dev/Full seat, no API key in local mode, no telemetry. Local mode stays on your machine; an optional, explicitly enabled read-only adapter can also read files over Figma's REST API. Unofficial project, not affiliated with Figma, Inc.

What is it?

Figma Local Bridge is an open-source Model Context Protocol (MCP) server for Figma Desktop. It connects an AI client to a small Figma plugin over a local WebSocket, so the model can inspect, screenshot, build, audit, compare, checkpoint, and edit the file that is open in the Figma desktop app — without a Dev/Full seat. Local mode needs no cloud API and no API key; an optional read-only REST adapter can be enabled for files that are not open.

It is a practical alternative when the official Figma MCP server is unavailable, when the file is only open locally, or when an agent needs guarded writes, visual regression checks, design-token sync, or sprite/atlas export.

At a glance

Fact

Value

Version / source

0.35.1 · github.com/Miart/figma-local-bridge

What it is

Local stdio MCP server + Figma Desktop plugin + one WebSocket daemon

Client support

Any MCP stdio client; examples: Claude Code, OpenCode, Cursor, and ChatGPT/Gemini clients with local MCP support

Figma requirement

Local mode: Figma Desktop with the included development plugin (no Dev Mode seat). Optional cloud tools: a Figma PAT/OAuth token with file access

Transport

127.0.0.1 WebSocket; no telemetry. Figma REST only when explicitly opted in

Files at once

Up to 16 open Figma files, routed by fileKey

Tool surface

17 parity + 33 local = 50 tools; +5 optional cloud tools = 55 when enabled

Writes

Local writes guarded: dry run, planId, fingerprints, stable keys, one undo transaction, rollback, verification. The cloud adapter never edits Figma; figma_cloud_download_image writes local image files only

Verification

figma_audit, figma_compare, figma_review, figma_checkpoint, figma_design_system, figma_tokens, figma_motion_*, journal/ops

Runtime

Node.js 20+

Installer

macOS 0.35.1: double-click Install.command in the release zip; one Figma plugin import and one token paste remain manual

License

MIT

Local by default, optional read-only cloud

figma_ping returns a capabilities object with the effective local mode (read-only, write, or write+destructive) and an explicit cloud boundary. The default is off:

{
  "local": { "plugin": "Figma Desktop", "mode": "write" },
  "cloud": {
    "available": false,
    "configured": false,
    "mode": null,
    "tools": []
  }
}

Set both an opt-in switch and one credential to enable the read-only REST adapter:

FIGMA_CLOUD=1
FIGMA_CLOUD_TOKEN=...          # personal access token
# or: FIGMA_CLOUD_OAUTH_TOKEN=...   # OAuth access token

Then figma_ping reports available: true, mode: "read-only", the five tools and the reads they support (file, nodes, images). The adapter calls only documented GET endpoints — /v1/me, /v1/files/:key, /v1/files/:key/nodes, /v1/images/:key — and never edits a file. A token alone does not enable it: this avoids surprising cloud traffic when an editor already has a Figma token in its environment.

Figma-hosted file creation, AI generation, generative plugins, shaders and Weave remain unavailable in both modes. They are not proxied and are never simulated; use the official remote Figma MCP or the Figma UI for those. Tools the official server marks remote-only — download_assets, get_libraries, whoami, search_design_system — have local counterparts here, answered from the open file rather than the cloud.

Common tasks

Ask in natural language

Start with

"What Figma file is open? Show me the selection."

figma_ping, figma_get_selection, figma_whoami

"Inspect this frame / find its node id."

figma_get_metadata, figma_inspect, figma_index

"Take a screenshot or export a PNG/SVG."

figma_get_screenshot, figma_download_assets

"Build or update this UI from a keyed spec."

figma_build, figma_patch

"Make many small edits in one transaction."

figma_batch_edit

"Apply or remove an animation style, keyframes, or timeline duration."

figma_motion_styles, figma_motion_inspect, figma_motion_apply_style, figma_motion_remove_style, figma_motion_set_keyframes, figma_motion_remove_keyframes, figma_motion_set_duration

"Audit the design system and suggest tokens."

figma_audit (suggest:true by default)

"Compare the render with a mockup."

figma_compare

"Save a baseline, edit, then tell me what changed."

figma_review (save → edit → compare)

"Export, diff, or import design tokens."

figma_tokens

"Swap a component or build a variant matrix."

figma_swap_component, figma_variant_set

"Save/restore a subtree without an undo stack."

figma_checkpoint

"Export a sprite atlas, 9-slice, states, or web handoff."

figma_export_ui_atlas, figma_export_9slice, figma_export_ui_states, figma_export_web_handoff

"Search the design system or file."

figma_search, figma_search_design_system, figma_design_system

"Run a raw Plugin API snippet."

figma_use (destructive opt-in + scope)

"A write timed out — did it run?"

figma_ops, figma_cancel, figma_journal

"Read a file that is not open in Figma."

figma_cloud_get_file, figma_cloud_get_nodes (opt-in)

"Give me render URLs for a remote Figma node."

figma_cloud_get_image (opt-in)

"Download remote Figma renders to disk."

figma_cloud_download_image (opt-in)

"Check whether my Figma REST token works."

figma_cloud_ping (opt-in)

Example requests

Audit the selected frame, list only errors, then fix the contrast findings without touching spacing.
Save a figma_review baseline of "Checkout / Card", apply the patch, and compare the result.
Export the Theme variables, change the dark background token, diff, and apply with the plan.
Build a 4x4 sprite atlas from the selected frame and verify the manifest rects against the PNG.

FAQ

Does Figma Local Bridge need a Figma Dev or Full seat? No. It uses a custom plugin with editorType: ["figma", "figjam"], not the devid manifest used by Dev Mode.

Does it use the Figma cloud API? Only if you enable the optional read-only adapter. Local mode is a plugin → daemon on 127.0.0.1 → stdio MCP server, with no cloud calls. FIGMA_CLOUD=1 plus a PAT/OAuth token adds five tools for files that are not open; it never writes to Figma and never proxies Figma's hosted AI.

Can Claude, ChatGPT, or Gemini use it? Any client that can launch a local stdio MCP server can. The repository includes an OpenCode-style config; Claude Code and Cursor use the same command + args. ChatGPT/Gemini clients need local MCP support and the same server command.

Is it the official Figma MCP server? No. It is an independent MIT project and is not affiliated with Figma, Inc. It mirrors 17 official tools, adds 33 local ones, and has five optional cloud tools (Figma read-only).

Can the agent edit the file? Yes, through guarded tools. Every mutating family supports a dry run and a planId/fingerprint boundary; figma_use is the raw escape hatch and needs explicit destructive access plus a scope.

Can it prove an edit is safe? No tool can prove intent. It can prove what changed: figma_review reports new/resolved audit findings and pixel/structural deltas; figma_checkpoint rebuilds and verifies a subtree; figma_compare reports where pixels differ.

Where are screenshots and reviews stored? Screenshots are returned as MCP images or written where you ask. figma_review baselines live locally under ~/.local/share/opencode/figma-bridge-reviews/<fileKey>/ (override with FIGMA_BRIDGE_REVIEWS_DIR).

Related MCP server: figma-meta-mcp

Why this exists

Figma's official MCP server (Dev Mode, 127.0.0.1:3845) requires a Dev/Full seat on Professional+. This project ships its own Figma plugin instead: the manifest uses editorType: ["figma", "figjam"] rather than devid, so it runs in an ordinary editor window. Local mode stays on the machine — a WebSocket on 127.0.0.1, no telemetry, no API key. The optional REST adapter is off unless you set FIGMA_CLOUD=1 and a token, and it is read-only.

Architecture

Figma Desktop ──WebSocket──▶ daemon (port 8787, one per machine)
                                  ▲
                                  │ WebSocket client
            ┌─────────────────────┼─────────────────────┐
      MCP process           MCP process           MCP process
      (workspace A)         (workspace B)         (workspace C)
            ▲
            │ stdio
      OpenCode / Claude / Cursor

The daemon exists for three concrete reasons:

  • the Figma plugin pairs with one socket per open file, and Figma's manifest CSP can only allowlist fixed origins, so the port cannot be ephemeral;

  • MCP clients spawn one server process per workspace — on a fixed port, they would collide;

  • one token, one plugin import: the second Figma window costs nothing extra.

One daemon = one port = one token = one plugin import — for up to 16 open files at once.

Repo layout

Path

What

server/src/

stdio MCP server, daemon (WebSocket hub), link state, tool implementations

plugin/

Figma plugin: code.ts bundled to code.js by esbuild, UI panel, dispatcher, scope guard

install/

macOS installer, uninstaller, double-click wrappers and the pure config/plist helpers

scripts/build-release.mjs

builds the self-contained macOS zip (npm run build:release)

skills/figma-local-bridge/SKILL.md

the installable agent skill: a short playbook with frontmatter, safe to copy into ~/.claude/skills/figma-local-bridge/

skills/figma-local-bridge/GUIDE.md

the full operating guide, served both as MCP instructions and as the figma-local://guide/skill resource

scripts/mascot.py

the ASCII octopus the panel ships: the art lives on the panel's own 21x10 grid, and its JS block is emitted from here, never hand-edited

scripts/mcp-smoke.mjs

npm run smoke: build smoke test with no Figma and no daemon

llms.txt

short plain-text project summary

test/

unit tests (node --test) and e2e tests against a real daemon / WebSocket / MCP client

scripts/check-privacy.mjs

local gate for home paths, real e-mail addresses, credentials, HAR captures and company/project names

Repository privacy

This is a public repository; names from private client projects must not enter it, even as “just an example”. npm run check:privacy scans every tracked file and matches a configured term across spaces, hyphens, underscores and camelCase, so an identifier such as projectName is caught as well as the prose form. A finding reports the file and line, never the term itself. The optional pre-commit hook scans the exact staged bytes, so a secret or a project name cannot slip in after the last manual check:

npm run check:privacy          # tracked files
npm run hooks:install          # git config core.hooksPath .githooks

The denylist is intentionally outside the repository: put one client or project name per line in ~/.config/figma-local-bridge/privacy-denylist.txt (override with PRIVACY_DENYLIST_FILE or pass comma-separated terms in PRIVACY_DENYLIST). A scanner that contained the names would itself leak them. Home paths, real e-mail addresses, private keys and common token shapes are always checked; test placeholders such as example.com, /Users/example and /Users/me are allowlisted. .gitignore also excludes local secrets, HARs and .privacy-denylist. | spike/ | the first prototype, kept for history |

Easy install (macOS, 0.35.1)

The release zip is self-contained: it already contains server/dist, plugin/code.js and the production node_modules, so the user does not run npm install or npm run build.

  1. Build the zip from a checkout with npm run build:release (or use a release artifact attached by the maintainer). The archive is release/figma-local-bridge-<version>-macos.zip.

  2. Unzip it and double-click Install.command. If macOS Gatekeeper blocks an unsigned download, use right-click → Open once. The installer is unsigned in 0.35.1; signing and notarization are a later step.

  3. The installer checks Node.js 20+, copies the app to ~/Library/Application Support/Figma Local Bridge/app, installs a per-user LaunchAgent (com.miart.figma-local-bridge), backs up and updates the MCP configs it finds for OpenCode, Claude Code, Claude Desktop and Cursor, and copies the daemon token to the clipboard. It never prints the token.

  4. In Figma, do the two things Figma exposes no API for: Plugins → Development → Import plugin from manifest… and choose ~/Library/Application Support/Figma Local Bridge/app/plugin/manifest.json; then run Figma Local Bridge and paste the token once.

  5. Restart the MCP client. figma_ping should answer.

The installer does not create a Figma account, install a Figma development plugin, or bypass Gatekeeper. To remove the app, LaunchAgent and this bridge's MCP entries, double-click Uninstall.command (config backups and daemon data are kept; add --purge from the command line to remove the token too).

npm run setup runs the installer from a built source checkout. The lower-level manual setup remains below.

Quick start (from source)

Requirements: Node.js 20+ (developed on v25) and the Figma Desktop app. Developed on macOS; the Node side is cross-platform.

git clone git@github.com:Miart/figma-local-bridge.git
cd figma-local-bridge
npm install
npm run build
npm run daemon        # keep it running; log: /tmp/figma-bridge-daemon.log
  1. Import the plugin (once): in Figma → Plugins → Development → Import plugin from manifest… → <repo>/plugin/manifest.json. (npm run build must have run first — plugin/code.js is generated and not committed.)

  2. Run the plugin: Plugins → Development → Figma Local Bridge.

  3. The panel asks for the daemon token — copy it from ~/.local/share/opencode/figma-bridge-daemon.json and paste it once. It is kept in figma.clientStorage, and the plugin reconnects by itself on every launch. The port is fixed by plugin/manifest.json (networkAccess.devAllowedDomains) — the panel cannot change it.

  4. Point your MCP client at server/dist/index.js (stdio). Example (opencode):

"figmaBridge": {
  "type": "local",
  "command": ["node", "/path/to/figma-local-bridge/server/dist/index.js"],
  "environment": {
    "FIGMA_BRIDGE_PORT": "8787",
    "FIGMA_BRIDGE_WRITE": "1",
    "FIGMA_BRIDGE_DESTRUCTIVE": "0"
  }
}

Clients that use the common mcpServers shape (Claude Code/Desktop, Cursor, Gemini CLI and other local-MCP clients) can use the same server command:

{
  "mcpServers": {
    "figma-local-bridge": {
      "command": "node",
      "args": ["/path/to/figma-local-bridge/server/dist/index.js"],
      "env": {
        "FIGMA_BRIDGE_PORT": "8787",
        "FIGMA_BRIDGE_WRITE": "1",
        "FIGMA_BRIDGE_DESTRUCTIVE": "0"
      }
    }
  }
}

Without the plugin running, the MCP server still starts and every tool answers FIGMA_BRIDGE_OFFLINE — fast, with a precise reason, instead of hanging for 30–115 seconds.

Configuration

Variable

Default

Meaning

FIGMA_BRIDGE_PORT

from plugin/manifest.json (8787)

Daemon port

FIGMA_BRIDGE_HOSTS

127.0.0.1,::1

Interfaces to listen on

FIGMA_BRIDGE_INFO_FILE

~/.local/share/opencode/figma-bridge-daemon.json

State file: pid, port, clientKey, plugin token

FIGMA_BRIDGE_TOKEN_FILE

~/.local/share/opencode/figma-bridge-daemon.secret

Persistent plugin token (0600)

FIGMA_BRIDGE_WRITE

1

0 — read-only: mutation tools are not even registered

FIGMA_BRIDGE_DESTRUCTIVE

0

1 — allow figma_use (raw Plugin API snippets) and a real figma_checkpoint restore

FIGMA_BRIDGE_CHECKPOINTS_DIR

~/.local/share/opencode/figma-bridge-checkpoints

Where figma_checkpoint keeps its files, one directory per fileKey

FIGMA_BRIDGE_REVIEWS_DIR

~/.local/share/opencode/figma-bridge-reviews

Where figma_review keeps iteration baselines (PNG + JSON), one directory per fileKey

FIGMA_BRIDGE_JOURNAL_DIR

~/.local/share/opencode/figma-bridge-journal

Where the journal keeps ops.jsonl (plus two rotated segments)

FIGMA_BRIDGE_JOURNAL

1

0 — stop recording new journal lines; what is already on disk stays readable

FIGMA_CLOUD

unset

1 — enable the five optional cloud tools; 0/unset leaves them out

FIGMA_CLOUD_TOKEN / FIGMA_CLOUD_OAUTH_TOKEN

unset

PAT (X-Figma-Token) or OAuth (Authorization: Bearer); set exactly one. Also requires FIGMA_CLOUD=1

Don't run a second bridge instance: the port is fixed, so the second daemon exits with EADDRINUSE (see Troubleshooting).

Several files at once

Run the plugin in each Figma file you want to drive — every panel connects to the same daemon, which holds one socket per file, keyed by fileKey (a file that was never saved gets a session key like unsaved-a1b2c3d4, which it keeps for as long as the document is open, panel reloads included). Up to 16 files; a panel beyond that is refused with 4413 and says so.

  • figma_ping lists the connected files (fileKey + name) — it is the tool to call when you do not know which files are open.

  • Every local tool takes an optional fileKey. With one file connected it may be omitted; with several, a call without fileKey is refused before anything is sent, listing the candidates. Guessing here would edit the wrong file. The optional cloud tools use a required fileKey naming the REST file to read.

  • The daemon routes by file and answers an unroutable request with an error immediately — never a timeout.

  • One file disconnecting fails only its own in-flight calls; the others keep working. Re-opening the plugin in the same file replaces its old socket (old panel gets 4409).

What the plugin panel shows

Row

Meaning

mascot

decorative only: an ASCII octopus whose mood follows the link — asleep while offline, shuffling while connecting, blinking when idle, winking when you click the canvas, upset after a failed call. It reacts (hop / shake) to what happened, and answers a tap on itself with a hop and a beaming face; the status line below always carries the same facts in words. hide pet / show pet in the footer switches it off for good (stored per user in figma.clientStorage)

window height

the panel measures what it rendered and asks Figma to fit the window to it, so hiding the mascot (~120px) or showing the error line (~20px) never leaves dead space behind. If Figma drops the resize, the panel re-asks on its own until the window matches the content

status

connected / offline — retrying in Ns / token rejected / another panel is serving this file — press reconnect to take over / all 16 file slots are taken — retrying in Ns

chips: page, selection

what is open and selected right now — pushed by the plugin, so it stays live even without a connection; hover a chip for the full list

chip: mcp

how many MCP sessions are attached (0 = no client is bound)

chip: calls

how many calls went through; turns red after an error

error line

the last call error (full text in the tooltip)

Tools

17 tools mirroring Figma's official server, plus 33 local ones — 50 in total. An optional read-only REST adapter adds five more when FIGMA_CLOUD=1 and a token are set, for a surface of 55.

Every local tool also accepts an optional fileKey to choose among several open Figma files; with one file it can be omitted, with several a call that omits it is refused and lists the connected files (figma_ping shows the same list). Cloud tools instead take a required fileKey naming the REST file to read.

The server also ships an operating guide — fileKey routing, what rev and replayed mean, which failure may be retried, what figma_use rolls back. It travels as instructions on the MCP handshake and as the resource figma-local://guide/skill, so a client that ignores handshake instructions can still fetch it. Both are read at startup from skills/figma-local-bridge/GUIDE.md, which is the file to edit (a server/dist copied without the repo falls back to a short inline version). skills/figma-local-bridge/SKILL.md is a short, frontmatter-bearing agent skill for clients that load skills from a directory; copy that folder into ~/.claude/skills/figma-local-bridge/ (or the equivalent for your client).

Parity with the official Figma MCP server:

Tool

What it does

figma_whoami

User, open file, pages, viewport

figma_get_metadata

Sparse XML skeleton of a node — cheap, use it to find ids

figma_get_design_context

Full design description of a node for implementing it in code

figma_get_screenshot

PNG / JPG / SVG of a node (the caption carries rev)

figma_get_variable_defs

Variables used in a subtree

figma_get_libraries

Variable collections available from libraries

figma_search_design_system

Search components, styles, variables

figma_get_figjam

FigJam board: stickies, shapes, connectors, sections

figma_get_code_connect_map / figma_add_code_connect_map

Local Code Connect mappings

figma_download_assets / figma_upload_assets

Export / import assets

figma_set_page

Switch the active page

figma_set_selection

Select nodes and pan the viewport to them — the only way to move the user's cursor

figma_get_font_inventory

Fonts actually used by the file, with per-text-node counts

figma_list_file_components_for_code_connect

Inventory of every component in the file

figma_use

Arbitrary Plugin API snippet under scope protection; the answer carries rev, replayed, ms and nodes. A third argument, flb, is a snippet standard library for this manifest's traps — byId (getNodeById throws in dynamic-page mode), find/one, bbox (absolute layout or render bounds), clear, flatten (detach every instance), grid (uniform atlas cells at absolute coordinates), text (loads the font first) — and its write helpers refuse an out-of-scope or other-page target before writing

Local additions:

Tool

What it does

figma_ping

Health check and capability report: verifies the plugin is paired, reports the active page and connected files (fileKey + name), and returns the local mode, whether the optional read-only REST adapter is enabled, Motion (Beta) availability and playhead, and the Figma-hosted features even that adapter does not proxy

figma_get_selection

Current selection, answered from memory — the plugin pushes selection changes, so this costs no round trip

figma_get_layout_tree

Absolute geometry of a whole subtree in one call: per node absoluteTransform (2×3 matrix), absolute layout and render bounds, clip flag. Document-absolute space — the one a PNG export uses — so a rebuild (atlas, sprite sheet) can be checked against a capture without one get_node per layer. Geometry and screenshot stay separate calls (atomicWithScreenshot: false): snapshot, then capture, then compare rev — and figma_compare for the pixels themselves

figma_inspect

Compact structure + semantic fingerprint; compareWith returns EQUIVALENT / DIFFERENT / INCONCLUSIVE before you mutate. includeFingerprint:false skips the whole-subtree snapshot when only structure is needed (a comparison still computes it)

figma_build

Declarative, keyed UI tree in one transaction. Validates the spec and resolves fonts, styles, variables and components before writing; every node gets a stable key in pluginData, so re-sending the same buildId updates instead of duplicating. dryRun defaults to true and returns a real plan plus a scope fingerprint; apply with dryRun:false and that planId/fingerprint. Carries rotation, per-corner radii, stroke cap/join/miter, masks, layout grids, layout inclusion flags, text case/decoration/paragraph spacing, text hyperlinks (URL, or NODE by id), text runs (ordered, non-overlapping character ranges with their own font, size, text style, fills, fill style, variable bindings, case, decoration, line height, letter spacing and hyperlink), a whole-text textVariable for base bindings, an exact LINE length, SVG leaves (a frame created from a source string, updated in place by a repeat build), PATTERN paints (a file-local source node id plus tile type, scaling factor, spacing and horizontal alignment), SHADER paints (a shader id plus optional property assignments keyed by definition id; an unimported fill shader is imported inside the write transaction), BOOLEAN_OPERATION nodes (two or more operands in spec order, UNION / SUBTRACT / INTERSECT / EXCLUDE; changing the operation updates the same node and the size is derived from the operands), and INSTANCE overrides addressed by layer name/key or a structural path (text, font, text style, text formatting, hyperlink, fills/strokes and their styles, effects and effect style, visibility, opacity, blend, rotation, corners, stroke weight, size and name). Refuses type changes, component swaps, unknown keys, unsupported containers, and implicit auto-layout positioning

figma_patch

Sparse update of an existing keyed tree: create / update / delete counts from a dry run, a scope fingerprint, and one undo transaction on apply. A missing key is refused unless allowCreate; a key under the wrong parent is refused rather than duplicated; prune:true deletes omitted keyed children at each patched level. An SVG leaf keeps its frame id and key when its source changes; the interior is replaced in place. A BOOLEAN_OPERATION patch may list only the operands it changes when prune is false; pruning a boolean requires at least two operands in the patch, and a sparse patch keeps the existing order and appends a new operand (send the complete ordered list with prune:true to enforce order)

figma_batch_edit

Many small edits on many targets in one transaction. Ops: set_text (including a text hyperlink; per-run styling is a figma_build/figma_patch feature), set_name/rename, set_fill, set_stroke, resize, move, set_visible, set_opacity, set_rotation, set_layout, set_variant, set_constraints, duplicate, reparent. Targets are nodeId or stable key; duplicate and reparent accept a destination parentId or parentKey, resolved on the current page. Every target, destination, font, style and variant property is resolved before the first write; dryRun defaults to true and returns per-target fingerprints and a fingerprint for each non-page destination, so a changed parent refuses the move. A reparent.index is the final child index and omitted appends; a same-parent reorder keeps layoutPositioning, an auto-layout destination uses ABSOLUTE when preserving the canvas position and AUTO when preserveAbsolute:false. A destination inside an INSTANCE or a COMPONENT_SET is refused; a destination inside a component definition is named as an edit to the definition. A failed op rolls the whole batch back unless stopOnError:false, which reports exactly which op failed and what succeeded

figma_swap_component

Change an INSTANCE's main component in one transaction without losing the overrides that still fit. The dry run (default) compares the old and new structures and returns, per target: a fingerprint of the instance and the target component, an exact or unique-name remapped path for every direct layer override, component properties mapped by display name and type, root-level instance overrides (custom name, resized geometry, opacity, paints/strokes/effects and style links, layout placement), every named loss and every remapped path. Apply needs the dry-run planId and expectedFingerprints; a changed target, a lossy swap without allowLoss, or a library component whose structure was never checked without allowUnverified is refused before the first write. A changed node type, missing layer, changed nested component, dropped/renamed property and SLOT property are reported, never applied to a guess. Variant properties are chosen by the target component, not replayed. A target inside another INSTANCE is refused; an apply is one undo step, and an unexpected loss rolls the whole transaction back.

figma_variant_set

Author a COMPONENT_SET as a property matrix in one transaction. action:"combine" turns standalone COMPONENTs into a new set and replaces their names with the requested Property=Value combination; add clones an existing variant with a new combination; edit renames one variant in place. The dry run (default) requires the same property names across the matrix and refuses a duplicate combination, a component already in a set, a component inside an INSTANCE, or a destination inside an INSTANCE / another set / a COMPONENT definition. It returns per-component fingerprints, a fingerprint of the set and, for a combine, the destination layout. Apply with the dry-run planId and expectedFingerprints; after writing it reads variantProperties and variantGroupProperties back from Figma and rolls the transaction back if the names were not parsed into the matrix. A combine destination inside a COMPONENT is refused because Figma will not reparent a component into a component definition; add and edit still operate on an existing set. A combine appends the set (move it afterwards with figma_batch_edit if needed)

figma_audit

Read-only design-system audit (audit-v2): duplicate/stable-key problems, nodes added to a built tree without a key, literal fills/strokes that are neither a paint style nor a variable, text without a text style, placeholder copy, detached instances, WCAG AA contrast against the effective background, and default layer names, plus opt-in off-scale spacing/radius, empty text and literal effects. Findings carry nodeId, key, name, type, path, severity and the value that caused them. With suggest:true (default) a hardcoded colour also names exact local paint-style and colour-variable candidates, unstyled text names exact local text-style candidates, and spacing/radius findings name the nearest on-scale value; nothing is bound or edited

figma_index

Compact flat index of a page/subtree in document order: id, stable key, name, type, path, depth, visibility, size, child count and the build root. Read-only, capped, and explicit when it truncated

figma_search

Find nodes by name, stable key or text without fetching the whole tree: substring/regex/glob, node-type and key-prefix filters, hasKey, hidden-layer control, match fields and text snippets. Read-only, capped, and explicit about truncated results

figma_design_system

One comparable snapshot of the local design system: paint/text/effect/grid styles, variable collections with modes and per-mode values, and components/component sets with their property definitions. inventory returns it; export writes JSON with a sha256; diff compares that file with the live file by stable key and reports added/removed/changed. Read-only — it never creates or edits a style, variable or component

figma_tokens

Design-token sync for variables. export writes a figma-tokens/v1 document with every collection, mode, per-mode value, description and alias; diff compares that document with the live file; import (dry run by default) creates missing collections/modes/variables, updates values and descriptions, and reads every mode back after writing. A plain DTCG tree is accepted on import with collection/mode, including color, number, dimension, string, boolean, fontFamily and fontWeight; unsupported DTCG types are blocking findings, never silently dropped. The dry run returns a file-and-live-state fingerprint and the server returns a planId over the exact file bytes, target file and options; apply requires both, so a changed file or a changed document is refused before writing. prune is off by default, needs allowDestructive:true on apply, and deletes only variables in the named collections; an alias pointing at a pruned variable blocks the plan. Modes absent from the document are preserved for existing variables and filled from the default managed mode for new variables, and the plan says so. One undo transaction; if the read-back disagrees, the import is rolled back (or ROLLBACK FAILED is named).

figma_motion_styles

Read-only Figma Motion (Beta) catalogue: figma.motion.figmaAnimationStyles() plus local custom animation styles and the current playhead. Missing Motion support is MOTION_UNAVAILABLE, not an empty list.

figma_motion_inspect

Read-only Motion state of one node: applied animation styles, manual keyframe tracks, computed animations, timelines and the node-level methods this build exposes. Use its ids and track shapes as the inputs to the write tools.

figma_motion_apply_style

Apply a Figma or custom animation style. dryRun defaults to true and returns a planId over the operation and the node's live Motion state; apply reads the applied style id, duration, offset and props back and rolls back on a mismatch. Custom styles accept only timelineOffset.

figma_motion_remove_style

Remove one applied style by its applied instance id. Dry run by default; an absent id is a verified no-op. Apply verifies the id is gone and rolls back otherwise.

figma_motion_set_keyframes

Set one manual keyframe track: a property (OPACITY, TRANSLATION_X, SCALE_XY, …), a paint/stroke, or an indexed effect field. Positions are seconds, unique, and all values in a track share a type. Dry run by default; apply verifies every keyframe position, value and easing.

figma_motion_remove_keyframes

Remove the manual track on one field. Dry run by default and reports the current track; an absent track is a verified no-op. Apply verifies the field no longer has a manual track.

figma_motion_set_duration

Set a Motion timeline duration in seconds from a timelineId returned by figma_motion_inspect. Dry run by default; apply reads the same timeline back and rolls back if the duration differs or the timeline disappears.

figma_export_build_spec

The reverse of figma_build: a live subtree as a build spec. Existing keys are kept; unkeyed nodes get the key the build engine would derive. Style and variable bindings become fillStyle/strokeStyle/textStyle/effectStyle, variable or textVariable, with literal values as the fallback. URL text hyperlinks travel in the spec; a NODE link is exported with a file-local warning. Mixed text becomes a base style plus ordered runs, so mixed fonts, sizes, text/fill style links, text variable bindings, case, decoration, spacing and links survive a rebuild. An IMAGE or VIDEO paint carries its crop transform, tile scaling, rotation and filters (a video hash is file-local, like an image hash); a PATTERN paint carries its source node id, tile type, scaling factor, spacing and horizontal alignment (the source node is file-local); a SHADER paint carries its shader id and property assignments keyed by definition id, including variable aliases, which must resolve in the target file (the shader must be available in the target file and is imported on apply); every supported paint carries visible and blendMode, including an invisible paint that stays in the stack. Direct instance overrides become overrides entries with a structural path built from the main component's layer names (so a renamed layer still resolves) plus the fields a rebuild can express; an override field the build cannot carry is named. A BOOLEAN_OPERATION exports its operation and live operands (not a flattened SVG), with its derived size omitted. A node the build cannot express — an unsupported type, a frame created from SVG, a detached instance — is exported as an inline SVG render when one fits (svgFallback, on by default) that rebuilds as a flattened frame: appearance survives, but the original child layers, component identity, overrides and style/variable links do not, text becomes outlines, and every fallback is named. Slices are never flattened; a render that fails or exceeds its budget is listed under unsupported, and with svgFallback:false all such nodes are listed there instead. The reply also names its remaining losses — unsupported instance override fields, unrepresentable paint types, mixed values — and marks a capped tree truncated rather than returning a partial spec that looks complete. Read-only

figma_compare

Diff a live render against a PNG on disk — a mockup, an earlier export, or another node written out with figma_download_assets. The diff runs in the MCP server, so the document is never touched: it answers with the differing area, its bounding box in the reference's own pixels (top-left origin — the box can be cropped straight out of that PNG) and the largest clusters, and writes a heatmap PNG next to the reference. tolerance absorbs anti-aliasing; a size mismatch is reported rather than hidden, and a clean 2×/3×/4× ratio names the scale you meant

figma_review

The iteration harness for one subtree. save writes a local baseline — PNG, the full audit report and the semantic fingerprint — under a review id; compare re-renders, re-audits and re-inspects the live node, diffs the baseline pixels, and reports new / resolved / changed findings, severity deltas, size and fingerprint changes, a heatmap and a verdict: unchanged, changed, improved or regressed (a new error/warning or a size change). The audit rules, hidden-layer setting and scale are reused from the baseline, so a delta means the same thing across iterations; latest, list and drop manage the local files. Read-only for the document — only local review files are written. If the document changes while a capture is running, the call refuses instead of comparing two revisions, and a truncated audit says its delta may be incomplete

figma_replace_repeated_with_component

Replaces repeated identical nodes with instances of one master component, in a single transactional call. Refuses unless every target is provably equivalent; try dryRun: true first

figma_checkpoint

Undo, without an undo stack: save describes a subtree as data (one JSON file per file, under ~/.local/share/opencode/figma-bridge-checkpoints/<fileKey>/), show diffs the live subtree against it field by field, restore replaces the target's children from it in one transaction — dry run by default — and then dumps the subtree again to verify: verified: true only when that second diff is empty. It re-links fill/stroke/effect/text style ids, node-level and run-level variable bindings and explicit variable modes when they still exist, reporting each one that does not; paints and fonts are still restored by value. Text runs come back with their own fonts, sizes, fills, text/fill style links, variable bindings, case, decoration, line height, letter spacing and hyperlinks; STAR and POLYGON are rebuilt natively with their point count (and STAR's inner radius) before the final size is applied; a BOOLEAN_OPERATION tree is rebuilt from its children with the recorded operation, including nested operations, and one with fewer than two rebuildable children is named and skipped; a SLICE comes back with its geometry and exportSettings; a COMPONENT_SET is rebuilt by recreating its variant COMPONENTs, combining them with figma.combineAsVariants, adding back its non-variant component properties by type and default value, restoring their descriptions, preferred values, slot settings and definition-level default-value variable bindings where the property type and a live variable allow it, and remapping child-layer componentPropertyReferences to the rebuilt property names; the #id suffix is Figma-assigned and cannot be restored, so the verification compares component-property definitions and references by display name. Direct instance overrides travel as structural paths plus a layer snapshot and are replayed after the main component is re-instantiated; a text override with mixed runs is replayed range by range (fonts, sizes, fills, text/fill style links, variable bindings, case, decoration, spacing and hyperlinks), not just at the base level, and a range link or binding the override removed is cleared before the snapshot is replayed. A path that no longer matches is named and skipped, and an override field this version cannot rebuild is named in overridesUnsupported. Refuses a truncated dump, a GROUP/INSTANCE/page target, and unsupported node types unless allowPartial; a save lists what a rebuild cannot carry (an INSTANCE's interior structure beyond its direct overrides, component and component-set identities/instances) so the losses travel with the data

figma_ops

What the plugin is doing per file: the call that is running, the writes queued behind it, and what finished recently — plus, for this session, the calls it is still waiting for and the ones it gave up on. With opId (a timeout error names it) it adds the answer recorded for that operation: that is how a call that timed out is settled without re-reading the document. Read-only, and answers even when the plugin is gone (with the local half and the reason)

figma_cancel

Stop an operation whose state is queued — a write whose caller has already given up, still waiting behind another write. Its caller is answered at once with OP_CANCELLED and rev does not move, because nothing of it ran; a retry of the same opId replays the cancellation instead of starting the call. OP_RUNNING (Figma runs plugin code to completion — nothing can interrupt it), OP_DONE (it already ran; the message names the revision) and OP_UNKNOWN are refusals, not failures. Read-only: cancelling only ever prevents a write

figma_journal

The durable half of figma_ops: the tail of a local append-only JSONL file — every call the bridge sent to a plugin, with method, target, revision and outcome, and the ones that did not end cleanly (timeout, dropped, late). Survives bridge and plugin restarts, which is exactly where figma_ops stops helping; filters by fileKey and method, and unsettled: true returns only the may-or-may-not trail. Read-only and entirely local; snippets, text and images are never recorded

figma_export_ui_atlas

A sprite sheet for a game build: one PNG plus a JSON manifest whose rects are in that file's pixels. The sheet frame's direct children are the sprites — nothing is repacked, the layout stays the designer's — and the manifest names what would make the sheet unsliceable: overlapping rects, sprites outside the image, drawing that bleeds out of its own cell, repeated names (keyed coin#1, coin#2), coordinates that had to be rounded, and a PNG whose size is not round(sheet × scale) (the rects follow the file, not the prediction). The file is re-read from disk before the answer, so the manifest cannot describe an image nobody wrote

figma_export_9slice

One sprite with its 9-slice insets: a PNG plus a descriptor — insets in the file's pixels, the stretchable middle, and CSS border-image values with slices in image pixels and widths in design pixels (which is what a @2x image needs). Pass insets, or leave a child layer named exactly 9slice where the middle should stretch. Insets that leave no middle are refused with the numbers that caused it

figma_export_ui_states

The states of a button/component as one file per state, plus a manifest naming them. A COMPONENT_SET names its variants by property — the only one it declares, or the one you pass — and a frame names them by layer name; a set with several properties is refused until you say which one names the states. States whose bytes are identical are called out (a hover that draws exactly like normal is usually a variant that is not what its name says), and every file is re-read from disk: its size checked against the node, its hash recorded

figma_export_web_handoff

One folder for a web developer: preview.png (1x) plus preview@Nx.png, spec.json (flat, document order — every layer's box relative to the frame, text, paints, effects and layout fields), tokens.json and tokens.css (solid colours named by their paint styles when one value has one style, fonts, text classes, spacing), a README.md stating what the bundle is not, and handoff.json with hashes, counts and warnings. A colour bound to several styles, a gradient, an image fill or a mixed text layer is never averaged into an invented value: it is named as missing, or carried as segments/stops. One call writes one folder, refuses to reuse it for another node, and re-reads every file from disk. Read-only — it works with writes off; above 2,500 nodes, 24 MP per preview or 8 MiB per PNG it refuses instead of truncating. Font availability is checked within a time budget: a font the check could not confirm in time is reported as unchecked, not assumed present, and the reply splits where the time went — walkMs reading the tree, checkMs pinging fonts, exportMs rasterising

Optional cloud tools (enabled only by FIGMA_CLOUD=1 plus a token):

Tool

What it does

figma_cloud_ping

Validate FIGMA_CLOUD_TOKEN (PAT) or FIGMA_CLOUD_OAUTH_TOKEN (OAuth) against GET /v1/me; returns the account and token type, never the token. Needs current_user:read

figma_cloud_get_file

Read a file's JSON with optional nodeIds, depth, version, geometry and pluginData. Use ids/depth to keep it small; a reply over maxBytes is refused, never truncated

figma_cloud_get_nodes

Read specific nodes from a file by nodeIds with the same depth/geometry/plugin-data controls

figma_cloud_get_image

Get temporary render URLs for nodes (PNG/JPG/SVG/PDF) with scale and SVG options. It returns URLs, not downloaded bytes; Figma's URLs expire

figma_cloud_download_image

Download nodes as PNG/JPG/SVG/PDF files to outDir (default: a temp directory) with figma-<nodeId>.<ext> names. One call calls /v1/images, then fetches each signed URL without the Figma token, verifies the file signature, caps each file and the total, and refuses to overwrite unless overwrite:true. The Figma document is untouched; this tool writes only local files

Cloud, when explicitly enabled

The cloud adapter is a separate mode that is read-only with respect to Figma. It uses documented Figma REST GET endpoints only; there is no Figma write path and no attempt to reproduce Figma's hosted AI, file creation, shaders, generated plugins or Weave. figma_cloud_download_image writes rendered bytes to a local directory, never to the Figma document.

Setting

Meaning

FIGMA_CLOUD=1

Explicit opt-in. 0 or an unset value leaves the five cloud tools unregistered

FIGMA_CLOUD_TOKEN

Personal access token, sent as X-Figma-Token. Needs file_content:read; figma_cloud_ping also needs current_user:read

FIGMA_CLOUD_OAUTH_TOKEN

OAuth access token, sent as Authorization: Bearer. Set only one credential

FIGMA_CLOUD_TIMEOUT_MS

Request timeout, 1000–120000 ms (default 30000)

FIGMA_CLOUD_API_BASE

Defaults to https://api.figma.com; a custom base must be HTTPS

FIGMA_CLOUD_INSECURE=1

Allows plain HTTP only for 127.0.0.1/localhost test servers; never use it with a real token

The adapter never puts a token in a URL, a log line or a tool reply. It follows no redirects (the credential cannot be forwarded to another host), refuses a response over maxBytes (default 8 MB for JSON) instead of serialising a huge document, and maps failures to CLOUD_UNAUTHORIZED, CLOUD_FORBIDDEN, CLOUD_NOT_FOUND, CLOUD_BAD_REQUEST, CLOUD_RATE_LIMITED (with retryAfterSeconds), CLOUD_UPSTREAM, CLOUD_TIMEOUT, CLOUD_UNREACHABLE, CLOUD_RESPONSE_TOO_LARGE and CLOUD_BAD_RESPONSE. Image downloads add CLOUD_DOWNLOAD_BLOCKED (non-HTTPS URL, credentials in the URL, or a redirect), CLOUD_BAD_IMAGE (bytes do not match the requested format) and CLOUD_DOWNLOAD_FAILED (local path or write failure). The download tool caps each image (default 10 MB) and the whole call (default 50 MB), and never sends the Figma token to the image host. It does not run an OAuth authorization flow or refresh an expired token, and it does not retry automatically; a 429 is the caller's decision. Cloud tools work without the local daemon and are registered in read-only mode too.

Cloud-only in the official server, and not produced by this bridge in either mode: create_new_file, generate_figma_design, generate_diagram, generate_image, html_to_figma, send_code_to_figma, generative plugins, shaders, and Weave. They depend on Figma-hosted services; the bridge reports them as unavailable instead of simulating them. The official server also marks some read tools as remote-only — download_assets, get_libraries, whoami, search_design_system — but those are answered here from the open file's Plugin API and are not cloud features.

Safety model

The bridge edits real design files with an agent at the wheel, so most of the engineering went into making failure modes visible instead of silent:

  • Plan, then apply. figma_build, figma_patch and figma_batch_edit default to dryRun:true. The dry run resolves every resource and returns a content planId plus a fingerprint; an apply with a different planId is refused before it reaches the plugin, and an apply whose targets moved since the dry run is refused before the first write. The safe workflow is one call to plan, one call to apply — not a hopeful write.

  • Stable keys, not node ids. A built node carries its key in pluginData; a re-sent build finds and updates it even after Figma re-creates the node. Keys are unique per page, duplicate keys are refused, and a key under the wrong parent is a refusal rather than a second node with the same identity. A clone is a new subtree: its descendants are rekeyed under newKey instead of inheriting colliding keys. A batch move resolves both ends: the target by key and, when given, the destination by parentKey. The dry run fingerprints the non-page destination too, so a parent whose layout or children changed since the review refuses the move instead of landing it in a different structure.

  • A batch has one undo boundary. figma_batch_edit resolves every target, destination, font, style and variant property before the baseline, then applies the ops in order. A failed op rolls the entire batch back (or, with stopOnError:false, stops pretending: it reports the exact op that failed and what succeeded).

  • A component swap is a migration, not a relink. figma_swap_component records the instance's direct layer overrides and root-level state before writing, compares the old and new component structures in the dry run, and names every layer and component property it cannot carry. Apply requires the planId and per-target fingerprints that cover the instance and the target component; a lossy swap needs allowLoss, a library import needs allowUnverified, a target inside another INSTANCE is refused, and an unexpected loss during apply rolls the whole transaction back. The report keeps predicted losses separate from restored overrides, so swapComponent's editor heuristics are never presented as a contract.

  • A variant set is a matrix, not a folder. figma_variant_set validates every property name and combination before writing. Combine replaces standalone component names; add clones one variant; edit renames one variant. Apply fingerprints the components, the set and the destination, then reads variantProperties and variantGroupProperties back from Figma and rolls the transaction back if the matrix did not land. A tool that reported success by parsing its own names would prove nothing, so it never does. If Figma refuses the undo itself, the error says ROLLBACK FAILED instead of claiming a rollback.

  • A token import is a plan, not a paste. figma_tokens exports every variable mode as data, diffs the document against the live file, and imports through the same dry-run boundary as the other write tools. The dry run hashes the file bytes and options into the planId, and the file identity plus the live variable state into the fingerprint; apply needs both, and a different file or a changed variable refuses the call before the first write. A value is never guessed: an unsupported DTCG type, a missing alias target, an alias type mismatch, an alias cycle, a variable whose type would have to change, or a prune target another variable still aliases is a blocking finding. Apply is one undo transaction; every collection, mode, description, literal and alias is read back from a fresh state, and a mismatch rolls the import back — if Figma refuses the undo, the error says ROLLBACK FAILED rather than claiming success.

  • An iteration is measured against a baseline. figma_review saves the pixels, the audit findings and the semantic fingerprint before an edit, then re-captures all three after it. The delta is the review: new errors/warnings or a size change make the result regressed, resolved problems make it improved, and a pixel/structure change without either is a neutral changed — never a claim that the design is better. The baseline reuses its audit rules and scale, so an audit delta is not an artefact of different settings; a capture that straddles two revisions is refused, and a truncated audit says so. Review files are local; the document is never written.

  • The audit cannot edit. figma_audit only reads; its findings are claims about the file, not a cleanup that might have changed it. The same is true of figma_index, figma_search and figma_design_system.

  • The cloud adapter can only read Figma. The optional REST tools are registered only after an explicit FIGMA_CLOUD=1 plus a token, call documented GET endpoints, carry their credential in a header (never a URL or reply), refuse redirects, cap the response size before serialising it, and keep 401/403/404/429 as their own error codes. There is no Figma write path and no fallback that pretends a hosted AI or file-creation service is local. figma_cloud_download_image is the one tool that writes locally; it verifies the signature and never sends the token to the image host.

  • Motion is a timeline, not a local animation. figma_motion_* only calls the Figma Motion (Beta) API. When figma.motion or a node-level method is absent, the call fails with MOTION_UNAVAILABLE; the bridge never stores pluginData that imitates a style or keyframe, and never interpolates a preview on its own. A Motion write is the same plan/apply transaction as the rest: a dry run fingerprints the node's live styles, manual tracks and timelines, apply must present that planId, and the style/keyframe/duration read-back must match before the call is reported as applied.

  • A design-system diff is by key, not by name. figma_design_system compares two exports by Figma's stable style/variable/component keys, and drops only the volatile node id from the value comparison. A style that keeps its name but changes its colour is a change; a node recreated with a new id is not.

  • Idempotent retries. opId is minted once per logical call and reused by every attempt (server/src/figma.ts). The plugin keeps a bounded opId → result cache and, on a repeat, replays the recorded answer (replayed: true) without touching the document. A repeat arriving while the first attempt is still running joins it instead of running the snippet twice.

  • A key names one call. The cache stores a fingerprint of the method and arguments it answered (plugin/dispatch.ts), so a different call sent under a used opId is refused (OPID_REUSED) instead of being handed the first call's answer — the one path that could report a write that never happened. Key order does not change a call, so a retry re-encoded by another JSON writer still replays.

  • Serialized mutations. Figma's undo barrier is global — two mutations in flight roll each other back. Mutating methods are chained one at a time (plugin/dispatch.ts); reads stay parallel. rev bumps on success and failure.

  • Honest disconnects. An error distinguishes disconnected before <method> was sent (safe to retry) from may or may not have been applied (retried under the same opId, so a replay cannot apply code twice).

  • An abandoned write does not land unseen. Writes are chained, so a call can wait behind another write for longer than its caller is willing to wait — and when the timeout fires, the queued write would have run anyway, at a revision nobody was watching. Instead the call stays visible: figma_ops shows it queued (nothing of it has run), running, or finished after the caller gave up with the answer it returned; figma_cancel stops a queued one, answering its caller immediately with OP_CANCELLED and leaving rev untouched because the document did not move. A running call is refused loudly (OP_RUNNING) — Figma runs plugin code to completion, so any tool claiming to interrupt it would be lying.

  • The operations trail outlives the process. figma_ops settles a timeout only while the client remembers the call and the plugin keeps its opId cache; a restart, a panel reload or a crash erases both. So every call the bridge sends is appended to a local JSONL journal — send, ok, failed, timeout, retry, dropped, late — with method, target, revision and outcome, and none of the payload (no snippets, text or images). figma_journal reads its tail and can show only the calls that did not end cleanly; a send with no terminal line names a call that was in flight when its process died, which is exactly the "may or may not have been applied" nothing else can answer after the fact.

  • Fast-fail offline. A call waits one 2-second grace period (LINK_GRACE_MS) for a reconnecting plugin, then fails with a precise reason. A request that was never sent is not retried at all.

  • No guessing between files. With several files connected, a call without fileKey is refused client-side before the request is sent — both the client (server/src/figma.ts) and the daemon (server/src/link.ts) check independently, and both name the connected files. A wrong file here is a wrong edit, so neither side ever "picks the most recent".

  • Scope guard + fingerprint diff. figma_use runs against a scoped, fingerprint-checked node set and is rolled back if the document drifts outside the scope. Switching pages inside a call is refused (currentpagechange): one call per page, or figma_set_page first. The snippet's own helpers (flb) make the same judgement before a write: a target outside scopeNodeId, or on another page — which the one-page diff could not see — is refused at the line that caused it, not by the rollback at the end.

  • Timeouts don't roll back. A Promise.race stops waiting, but the snippet keeps running and may still write — the message says exactly that instead of pretending "(rolled back)".

  • Stale screenshots are detectable. Figma caches exports. rev travels in the caption, so identical bytes with a higher rev means the render is stale, not that the edit did nothing.

  • A fallback is named, not disguised. figma_export_build_spec can turn an unsupported layer, a frame created from SVG or a detached instance into an inline SVG render, so the rebuild keeps its appearance instead of dropping it. The rebuilt node is a flattened frame, not the original type: the reply counts every fallback (counts.svgFallbacks, counts.svgBytes), names the node in warnings, and states that the original child layers, component identity, overrides and style/variable links are gone and text becomes outlines. The fallback budget is bounded per node and in total because figma_build refuses a spec over 256 KiB; a node over budget is listed as unsupported, never silently shrunk.

  • Exported artefacts describe the file that exists. An atlas manifest that is one pixel off is worse than no manifest: the engine slices the neighbouring sprite and nothing in the build says why. So the plugin reads the PNG's IHDR and computes every rect in the file's own pixels (reporting a mismatch instead of assuming round(size × scale)), the server re-reads what landed on disk and compares it against the manifest, and the numbers that had to be rounded are counted and named. A frame with a stroke — where the image is the node's own box and not the bounding box the rects would otherwise be measured from — is detected from the exported size and reported rather than silently shifting every sprite. A web handoff follows the same rule one level up: its README and handoff.json name the things the bundle cannot carry — gradients as tokens, image fills as files, fonts as embedded bytes, masks and effects as applied CSS — and a font that is in the file but absent from the machine is listed instead of assumed.

  • Gated writes. FIGMA_BRIDGE_WRITE=0 makes the bridge read-only; figma_use additionally needs FIGMA_BRIDGE_DESTRUCTIVE=1 and an explicit scope.

  • Checkpoints are measured, not promised. figma_checkpoint has no undo stack to lean on (Figma's is global and one step deep), so it rebuilds a subtree from a recorded description — and says what a rebuild cannot reproduce: an INSTANCE's interior structure beyond its direct overrides, and component identity. A component set's supported property metadata (SLOT descriptions, preferred values and slot settings; INSTANCE_SWAP preferred values) and definition-level default-value variable bindings are restored too, and a refused edit or missing variable is named. Direct overrides carry their path, overridden field names and a layer snapshot; restore recreates the instance from its main component and replays each override, naming a path that no longer matches instead of applying it to the wrong layer and naming every override field it cannot rebuild. A per-run text override inside an instance is replayed range by range with the same run values as a rebuilt text node; when the override touched range styling, the run snapshot is the final state, so a per-run style link or variable binding the override removed is cleared before the snapshot is replayed. Style ids, node-level variable bindings and explicit modes travel in meta; text runs carry their own style links and variable bindings. A restore re-links them when their style, variable, collection and mode still exist, and reports every one that does not while leaving the recorded value in place. Text runs themselves — font, size, fills, text/fill style links, variable bindings, case, decoration, line height, letter spacing and hyperlink — are saved and rebuilt. A real rebuild needs dryRun: false and allowDestructive: true and a bridge started with destructive writes; after the write the server dumps the subtree again and diffs it against the checkpoint, so verified: true means "no field differs", and a difference nobody predicted is reported with its path instead of being called a success.

Development

npm run build      # plugin (esbuild) + server (tsc)
npm run typecheck  # server + plugin (with @figma/plugin-typings)
npm run smoke      # does server/dist start, speak MCP and fail offline? ~3s, no Figma
npm test           # rebuilds artifacts, then unit + e2e with --test-concurrency=1

npm run smoke is the fast question — it spawns the built server against a scratch info file (so it takes the offline branch even when a real bridge is running), checks the handshake, the guide, the tool surface and the offline path, and prints one line per check. npm test is the slow one: the whole chain against a simulated plugin.

npm test rebuilds server/dist and plugin/*.mjs first because the tests import the built modules — a test running against a stale artifact passes on code Figma will never execute. The suites run serially (--test-concurrency=1): e2e tests spin up real daemons on real ports and share one token file, and parallel runs make them flaky by construction.

Current suites: 621 unit checks — 337 node:test cases (including 10 installer/config-merge cases) plus 284 harness checks (commands, fingerprint, scope, use-figma gate, checkpoint and checkpoint-store) — and 234 e2e checks across daemon, link state, MCP, multifile, tools, checkpoint, exports, journal, cloud and the build/patch/batch/swap/variant-set/audit/review/capability/index/search/design-system/tokens/spec-export chain. The full npm test run is green; the custom harness still prints 518 PASS.

Troubleshooting

Symptom

Cause

FIGMA_BRIDGE_OFFLINE

The plugin is not running in Figma, or the daemon is down

FIGMA_BRIDGE_UNKNOWN_FILE

The fileKey is not one of the connected files — an unsaved file's key changes when its panel reloads. The error lists the keys that exist: call figma_ping and retry with one of them instead of the same key

FIGMA_BRIDGE_FILE_KEY_REQUIRED

Several files are connected and no fileKey was given — the error lists them; pass one

figma_ping → connected: false

The plugin is not paired; the optional cloud adapter can still work without it

figma_cloud_* tools are missing

The adapter is off. Set FIGMA_CLOUD=1 and FIGMA_CLOUD_TOKEN (PAT) or FIGMA_CLOUD_OAUTH_TOKEN (OAuth), then restart the MCP process

CLOUD_UNAUTHORIZED / CLOUD_FORBIDDEN

The token is invalid/expired, or it lacks the scope: current_user:read for figma_cloud_ping, file_content:read for file/node/image reads

CLOUD_RATE_LIMITED

Figma returned 429; read retryAfterSeconds and wait instead of retrying in a loop. The adapter does not retry for you

CLOUD_RESPONSE_TOO_LARGE

The reply exceeded maxBytes; pass nodeIds/depth, or raise maxBytes deliberately. It is refused rather than truncated

CLOUD_TIMEOUT / CLOUD_UNREACHABLE

The REST request failed before a reply; check network/proxy and FIGMA_CLOUD_TIMEOUT_MS. The local daemon is not involved

CLOUD_DOWNLOAD_BLOCKED

The image URL was not HTTPS (except localhost with FIGMA_CLOUD_INSECURE=1), contained credentials, or redirected. The adapter does not follow it

CLOUD_BAD_IMAGE

The downloaded bytes do not have the requested PNG/JPEG/SVG/PDF signature. Nothing is written; the URL probably returned an error page

CLOUD_DOWNLOAD_FAILED

The target exists and overwrite is false, outDir could not be created, or a local write failed. The Figma request may have succeeded; no document was changed

The daemon won't start

Port 8787 is busy — lsof -nP -iTCP:8787 -sTCP:LISTEN

4409 replaced by a newer socket

Another panel took this file's slot (plugin reloaded, or the file is open in two windows). Press reconnect in the panel you want to keep

4413 too many files (max 16)

The daemon already serves 16 files — close a panel

4403 bad key

The daemon restarted with a new clientKey — restart your MCP client

4401 bad token

The token in the plugin doesn't match figma-bridge-daemon.secret

figma_checkpoint restore refused

Three separate gates, each with its own message: a real rebuild needs dryRun: false, allowDestructive: true, and a bridge started with FIGMA_BRIDGE_DESTRUCTIVE=1. Without them the default dry run still reports exactly what would be deleted and created

figma_checkpoint restore rebuilds but verified: false

The second dump differs from the checkpoint: read verification.diff (each entry has a path and both values) and verification.notes, which name the differences checkpoint-v1 knows it causes (HUG sizing, text-run values)

A write timed out — did it ever run?

The timeout error names its opId: figma_ops {opId} reports whether it is still queued, running, or finished after the timeout, with the answer recorded for it. figma_cancel stops it while it is still queued; OP_RUNNING and OP_DONE are refusals that say why it cannot be stopped — and OP_DONE names the revision the call actually reached

The bridge died or was restarted — what was in flight?

figma_journal {unsettled: true} reads the local journal: a send line with no terminal event for the same opId is a call that was in flight when its process died (it may have been applied), timeout is a caller that gave up, dropped got no answer at all, and late is the answer that arrived afterwards. It needs neither the daemon nor the plugin

Liveness check: lsof -nP -iTCP:8787 | grep -i figma — empty output means the plugin is not paired.

Limitations

  • The 0.35.1 installer is macOS-only and unsigned. Gatekeeper can require right-click → Open on the first launch; a signed/notarized .pkg or .dmg is not part of this release. The installer writes only its own MCP entry, backs up each config it changes, and skips a config it cannot parse instead of rewriting it. Figma exposes no API for installing a development plugin, so importing plugin/manifest.json and pasting the token remain manual.

  • Motion support is Figma Motion (Beta) and depends on figma.motion plus the node-level Motion methods being present in the running Figma build. When they are not, every Motion tool fails with MOTION_UNAVAILABLE — it never writes pluginData that merely looks like an animation. Motion writes require a dry-run planId, are read back after apply, and roll back on a mismatch. There is no native Motion GIF/MP4/WebM exporter in this bridge; render or record in Figma if a movie file is needed.

  • The optional cloud adapter reads the public Figma REST API, not the open document. It is registered only with FIGMA_CLOUD=1 and a PAT/OAuth token; the token's scopes decide what works (file_content:read for files/nodes/images, current_user:read for figma_cloud_ping). It exposes no Figma writes, does not run an OAuth authorization flow, and cannot refresh an expired OAuth token. figma_cloud_get_image returns temporary URLs; figma_cloud_download_image is the explicit byte-downloading tool and writes only local files. A response over maxBytes is refused; use nodeIds and depth instead of raising the cap blindly. A custom FIGMA_CLOUD_API_BASE must be HTTPS, except for a localhost test server with FIGMA_CLOUD_INSECURE=1. It follows no redirects and does not retry a 429 automatically; image downloads also refuse redirects and never send the Figma token to the image host.

  • indexTree walks the whole page per call. On very large pages, split the work — the cache cannot be incremental because the user edits the file in parallel.

  • Figma caches screenshot exports; rev makes stale renders visible, but there is no way to force a redraw from the Plugin API.

  • figma_use hands you the full plugin API: it is a notice-and-rollback guard, not a sandbox. Code that runs after the call returns (fire-and-forget promises) escapes the diff; a timeout therefore does not roll back and says so.

  • Figma unloads the plugin UI when the app is backgrounded. In-flight calls are cancelled immediately (instead of hanging until their timeout), and the bridge reconnects on its own when you return.

  • A checkpoint is a rebuild, not a rollback. A restored COMPONENT is a new component (its instances do not follow), and an INSTANCE shows its main component's current content. Every one of those is reported by the tool rather than hidden — see the limitations list in a save reply.

  • Build and patch treat keys as identity. They will not move an existing keyed node to a different parent: that is a refusal, not an implicit reparent, because a move changes the node's relation to auto-layout and clips. Use figma_batch_edit with reparent and a parentId or parentKey when a move is intended.

  • A batch cannot target a node created by an earlier duplicate in the same batch: the key does not exist at plan time. Split that into two calls.

  • A batch destination key is resolved on the current page only; a destination inside an INSTANCE or a COMPONENT_SET is refused because that interior is an override or a variant set, not a normal layer tree. A destination inside a COMPONENT is allowed but reported: it edits the component definition. Moving a node into or out of a protected parent is refused, because that changes the parent's children. A duplicate whose newKey already exists is a no-op; if that existing node is under a different parent, the plan says so instead of silently moving it.

  • A component swap is an instance-root operation. A target that is itself nested inside another INSTANCE is refused — changing it is an override of the outer instance, not an independent swap. A library component key cannot be structurally checked during a dry run; apply imports it and requires allowUnverified:true, then compares the imported structure and reports any loss. Variant properties are not replayed: the chosen target component or variant decides them. SLOT properties, a renamed or type-changed component property, a layer whose node type changed, and a nested instance that now points at another component are named losses, not guessed migrations. Nested instance overrides deeper than 12 levels block the swap instead of being silently truncated. A layer that moved AND was renamed can miss the unique-name fallback and is named as a missing override.

  • figma_variant_set treats a set as a complete matrix: every variant must use the same property names, and a combination must be unique. Property names and values cannot contain comma, equals or a newline. combine requires standalone components and refuses one already in a set; it replaces the old component names (reported as previous names) and appends the set, so move it afterwards with figma_batch_edit if placement matters. add clones the source variant's structure, not its instances, and offsets the clone by 24px when the set is not auto-layout. edit keeps the variant id and its instances, but changes their variant property values. A set whose variants do not all expose variantProperties is refused rather than guessed at. A combine destination inside an INSTANCE, another COMPONENT_SET or a COMPONENT definition is refused; Figma will not reparent a component into a component definition. add and edit still operate on an existing set inside a component and name that edit.

  • figma_tokens manages variables, not paint/text/effect/grid styles. Its native figma-tokens/v1 format is lossless for local variable collections, modes, values, descriptions and local aliases; a plain DTCG tree is an import convenience and covers one collection/mode. DTCG aliases are resolved within that collection; cross-collection aliases use the native {alias:{collection,name}} value. An alias whose target is in a remote/library collection is exported with remote:true, its name and its live variable id; an unchanged value round-trips and is preserved, while a new or different remote binding is refused rather than rebound to a same-named local variable. Collections, modes and variables are matched by name; renaming any of them is a create (and, for prune, a delete), not an identity-preserving rename. A mode rename creates a new mode and leaves the old one, because the import never deletes a mode. A variable is matched by name inside its collection. A variable's resolvedType cannot be changed in Figma, so a type change is refused. Export skips EASING/TIMING variables and names them; import blocks unsupported DTCG types. The document carries variable values, descriptions, modes and aliases, not variable scopes, code syntax, hidden-from-publishing flags, publish keys or extended-collection parentage; those are left as they are. Modes the document does not name are preserved on existing variables and filled from the default managed mode on new ones. prune deletes variables (node bindings break) but never deletes a collection or a mode. Colours are written as 8-bit hex, so alpha/channel precision beyond that is rounded; a dimension loses its px unit because a Figma FLOAT is unitless; a fontFamily array is joined into one STRING. A description omitted from the document is preserved, not cleared.

  • Audit contrast is exact for solid text on a solid effective background. A gradient or image fill, or a non-normal blend mode, is reported as unknown rather than guessed at. Translucent solid ancestor fills are composited bottom-to-top; if the stack never reaches full opacity, the remaining page white is named as the approximation in backgroundSource.

  • figma_review is a local baseline, not a checkpoint. It captures a PNG, an audit and a semantic fingerprint; it does not serialize a subtree and cannot restore one. A baseline is tied to the fileKey and stored review id, not to a published version; a node rebuilt with a new id needs the new nodeId passed to compare. A screenshot can be stale after a document change (the stored rev makes that visible), a missing inspect leaves the structure comparison null, and a truncated audit limits the new/resolved lists even though the counts are complete. Use figma_checkpoint for restoration and figma_compare when the reference is a mockup rather than an earlier iteration.

  • Audit v2 suggestions are candidates, not bindings. A paint-style candidate is an exact 8-bit colour match with one opaque solid paint; a colour-variable candidate is an exact value in at least one of its modes, and allModes says whether every mode matches; a text-style candidate is the same family, style and size. If a local style or variable API throws, the audit still runs, warns, and its suggestion counts stay honest. empty-text and hardcoded-effect are opt-in rules, not defaults.

Versions

Version

What it carries

0.35.1

Performance pass: figma_index and figma_search build paths from the parent prefix instead of walking ancestors per node, read each node's name/type/id/visibility/children once, compute a build root's id once per subtree, and defer key/text reads in search until a node is a candidate. A no-op figma_use reuses the before-index when nodechange never fired instead of walking the page twice. figma_inspect gains includeFingerprint:false for structure-only reads. figma_get_design_context caches style and collection-mode name resolutions for the call. Large tool replies are compact JSON above 128 KiB, and indexTree derives parent ids from the traversal instead of reading parent per node. No tool removal; 50 local + 5 optional cloud.

0.35.0

Figma Motion (Beta) support: figma_motion_styles and figma_motion_inspect read animation styles, applied styles, manual keyframe tracks and timelines; figma_motion_apply_style / figma_motion_remove_style apply or remove a style; figma_motion_set_keyframes / figma_motion_remove_keyframes write one manual track; figma_motion_set_duration sets a timeline duration. Writes default to a dry run and require its planId plus the node's live Motion-state fingerprint; after apply every style id, duration, offset, property and keyframe position/value/easing is read back, and a mismatch rolls the transaction back. The plugin probes figma.motion and the node-level methods at call time and fails with MOTION_UNAVAILABLE when the Beta API is absent; it never fabricates local animations or writes fake Motion metadata. The plugin reports Motion availability in figma_ping. Seven new tools; 50 local + 5 optional cloud.

0.34.1

Dynamic-page correctness and a reusable agent skill. figma_index / figma_search no longer touch the synchronous node.mainComponent getter, which throws under documentAccess: "dynamic-page" (and logs to Figma's console even when caught); an index deliberately omits component metadata, and figma_inspect or the new flb.main(node) resolves one instance through getMainComponentAsync(). Component swap and spec export no longer retain a synchronous fallback either. The old operating guide becomes GUIDE.md; SKILL.md is now a short frontmatter-bearing skill that can be copied to ~/.claude/skills/figma-local-bridge/. A local privacy gate (npm run check:privacy, optional .githooks/pre-commit) scans tracked or staged content for home paths, real e-mail addresses, credentials, HAR captures and a denylist stored outside the repository. No MCP tool changes; 43 local + 5 optional cloud.

0.34.0

macOS double-click installer and release packaging. npm run build:release builds a self-contained zip with server/dist, plugin/code.js, plugin/ui.html, the plugin manifest, the installer and production node_modules; Install.command copies the app under ~/Library/Application Support/Figma Local Bridge, writes a per-user launchd LaunchAgent with restart, backs up and configures OpenCode/Claude Code/Claude Desktop/Cursor MCP entries, copies the token to the clipboard without printing it, and opens the plugin manifest in Finder. Uninstall.command removes the app, LaunchAgent and this bridge's entries while keeping backups/data by default (--purge removes the token too). The installer refuses an occupied port owned by an unknown process, skips unparseable JSONC instead of corrupting it, and leaves the two Figma-manual steps explicit. No new MCP tools; the server and plugin surface stays 43 local + 5 optional cloud.

0.33.0

figma_cloud_download_image adds the byte-downloading half of the cloud image path. It calls GET /v1/images/:key, then fetches each temporary URL without forwarding the Figma token, verifies the PNG/JPEG/SVG/PDF signature, writes figma-<nodeId>.<ext> to outDir (default: a temp directory), caps each image and the call total, and refuses to overwrite unless overwrite:true. Everything is downloaded and checked before the first write; an existing file, a bad signature, a redirect or a local write failure is named instead of silently producing a partial image. The Figma document is untouched. figma_ping now names five cloud tools.

0.32.0

Optional read-only cloud access. With FIGMA_CLOUD=1 and a PAT/OAuth token, four REST tools join the surface: figma_cloud_ping (GET /v1/me), figma_cloud_get_file, figma_cloud_get_nodes and figma_cloud_get_image. They work without the local daemon, never write, never return the token, follow no redirects, and refuse oversized responses rather than truncating. figma_ping reports cloud.available, configured, mode, tokenType, the registered tools and reads; the default remains off, and a token alone does not opt in. Errors keep their HTTP meaning (CLOUD_UNAUTHORIZED, CLOUD_FORBIDDEN, CLOUD_RATE_LIMITED with retryAfterSeconds, CLOUD_RESPONSE_TOO_LARGE, ...). Hosted file creation, AI generation, generative plugins, shaders and Weave remain unavailable.

0.31.0

figma_ping becomes a capability report as well as a health check. It returns the server version, the effective local mode (read-only, write, write+destructive) and a cloud object; at that point cloud.available was false because no REST adapter existed yet, and the object listed the Figma-hosted features (file creation, AI generation, generative plugins, shaders, Weave) that this local bridge does not proxy or simulate. The local tools that the official server marks remote-only — assets, libraries, account identity, design-system search — remain real Plugin API reads. No new tool; no document writes.

0.30.0

figma_review is the iteration harness. save records a local baseline as a PNG, the full audit-v2 report and the semantic fingerprint; compare re-renders, re-audits and re-inspects the node, diffs the pixels, and reports new/resolved/changed findings, severity deltas, size and fingerprint changes, the heatmap and a verdict (unchanged, changed, improved, regressed). A new error/warning or a size change is a regression; resolved problems are an improvement; a visual/structural change without either is neutral. The baseline reuses its audit rules, hidden-layer setting and scale, a capture that spans revisions is refused, and a truncated audit is named. list, drop and latest manage local files; no document writes.

0.29.0

figma_audit becomes audit-v2: with suggest:true (default) every hardcoded fill/stroke also carries exact local paint-style and colour-variable candidates (a variable candidate names the matching modes and whether all modes match), unstyled text carries exact local text-style candidates, and spacing/radius findings name the nearest on-scale value and delta. Suggestions are read-only candidates, never bindings, and a failed style/variable API is a warning rather than a failed audit. Two opt-in rules join the surface: empty-text (visible text with no characters) and hardcoded-effect (visible effects without an effect style). The audit schema is audit-v2; all existing rules and findings keep their meaning.

0.28.0

figma_tokens adds design-token export, diff and guarded variable import. export writes the native figma-tokens/v1 document (collections, modes, values, descriptions and aliases); diff compares a document with the live file; import creates missing collections/modes/variables and updates values/descriptions in one undo transaction. A plain DTCG tree is accepted with collection/mode for color, number, dimension, string, boolean, fontFamily and fontWeight; unsupported types are blocking. The dry run fingerprints the live variable state, and the server hashes the file bytes plus options into a planId; apply requires both, so a changed file or a changed document cannot slip through. Variables are read back mode by mode after writing and the import rolls back on a mismatch, naming ROLLBACK FAILED if the undo itself fails. prune is opt-in, needs allowDestructive:true, only touches the named collections, and refuses to delete a variable another variable still aliases. Modes not named by the document are preserved on existing variables and filled from the default managed mode on new ones, with that fact in the plan.

0.27.0

figma_variant_set authors a component set as a property matrix. combine takes standalone components, validates that every variant uses the same property names, refuses duplicate combinations and nested/already-variant components, and calls figma.combineAsVariants after naming each component Property=Value; add clones an existing variant with a new combination; edit renames one variant in place and is a no-op when the combination is unchanged. The dry run fingerprints every component and the set, plus the destination layout for combine, and refuses a destination inside an INSTANCE, another set or a COMPONENT definition (Figma will not reparent a component into a component definition); add and edit still operate on an existing set. Apply needs the dry-run planId and expectedFingerprints; it reads variantProperties and variantGroupProperties back after writing and rolls the transaction back if Figma did not parse the names into the matrix. A set whose variants lack variantProperties is refused rather than guessed at.

0.26.0

Keyed moves resolve both ends. figma_batch_edit's duplicate and reparent accept a destination parentKey as well as parentId, and the dry run returns the resolved destination plus a fingerprint for every non-page destination. Passing the dry-run fingerprints therefore refuses a move whose parent changed, before the first write. reparent.index is the final child index and omitted appends; a same-parent reorder keeps layoutPositioning instead of turning an auto-layout child absolute, an auto-layout destination uses ABSOLUTE when preserveAbsolute is true and AUTO when false, and an out-of-range index or a destination inside an INSTANCE / COMPONENT_SET is refused. A destination inside a component definition is named as an edit to that definition. A duplicate whose newKey already exists remains a no-op and now reports an existing node under a different parent instead of pretending the requested move happened.

0.25.0

figma_swap_component swaps an INSTANCE's main component without treating the editor's override heuristics as a contract. The dry run compares the old and new structures, returns per-target fingerprints and names every loss: an override path is exact index+name first, then a unique name in the new component; a changed node type, missing layer, changed nested component and dropped/renamed component property are not applied to a guess. Root-level instance state (custom name, resized geometry, opacity, paints/strokes/effects and style links, layout placement, constraints) is preserved, component properties are mapped by display name and type, and direct layer overrides are replayed after swapComponent. Apply requires the dry-run planId and expectedFingerprints, refuses a lossy swap without allowLoss, refuses a library component whose structure was not checked without allowUnverified, refuses a target nested inside another INSTANCE, and rolls the whole transaction back on an unexpected loss. Variant properties are left to the chosen target; SLOT properties are named. The checkpoint reader can now report nested-instance depth truncation instead of presenting a cut-short override list as complete.

0.24.0

Per-run overrides now clear what they removed. When a direct instance override touched range styling, figma_checkpoint records the run snapshot as the final state and, before replaying it, clears the inherited per-run textStyleId / fillStyleId links and range variable bindings that the snapshot no longer carries. styledTextSegments is treated as a supported override field, so Figma's range-styling signal triggers the same replay. A link or binding the file refuses to clear is named in notes; a missing link or variable is still reported through stylesMissing / bindingsMissing.

0.23.0

Checkpoints restore component-property metadata. After adding back a COMPONENT_SET's non-variant properties, figma_checkpoint re-applies a SLOT property's description, preferred values and slot settings, an INSTANCE_SWAP property's preferred values, and a definition-level defaultValue variable binding on BOOLEAN, TEXT and INSTANCE_SWAP properties. A variable that no longer exists or an edit the file refuses is named in bindingsMissing / propertiesFailed, while the property and its literal default stay. Variant options still come from the variant names; component/property ids, component keys and existing instances still do not come back.

0.22.0

Build-spec shader paints close the paint gap. figma_export_build_spec carries a SHADER paint's shader id and property assignments keyed by definition id; figma_build / figma_patch resolve the id through listAvailableShaders, refuse an unavailable id or an effect shader, import an unimported fill shader inside the write transaction, and then apply the paint with visible, opacity and blendMode. propertyMetadata is read-only and is refused if sent.

0.21.0

Build-spec pattern paints survive the loop. figma_export_build_spec carries a PATTERN paint's source node id, tile type, scaling factor, spacing and horizontal alignment; figma_build / figma_patch resolve that source node before writing and block if it is missing, then apply the fields and keep visible, opacity and blendMode. The source node id is file-local, like an image hash, so a build in another file needs that node. SHADER remains named, not silently dropped (SHADER was closed in 0.22.0).

0.20.0

Build specs create boolean operations. figma_build accepts a BOOLEAN_OPERATION with booleanOperation (UNION, SUBTRACT, INTERSECT, EXCLUDE) and at least two children; the operands are built in spec order, combined with figma.union / subtract / intersect / exclude, and keep their keys. Operands are staged in a scratch frame first, so an auto-layout parent cannot discard their coordinates before the boolean is formed. An existing boolean updates in place; changing its operation edits the same node instead of rebuilding it. Its size is derived from the operands, so a supplied size is ignored with a warning. Nested booleans build inside-out. figma_patch may list only the operands it changes when prune is false; with prune:true a boolean patch must list at least two operands. A sparse patch keeps the existing operand order and appends a new operand; a full build enforces spec order. figma_export_build_spec exports a boolean as a live boolean with its operation and children, not as a flattened SVG.

0.19.0

Checkpoints rebuild component sets. figma_checkpoint walks a COMPONENT_SET's variant COMPONENT children, records componentPropertyDefinitions, recreates each variant and combines them with figma.combineAsVariants, then adds back the non-variant properties by type and default value and remaps layer-level componentPropertyReferences to the new names. Figma assigns a fresh #id suffix to each rebuilt property, so verification compares definitions and references by display name. Variant options come from the component names; component/property ids, component keys, existing instances, property descriptions/preferred values/slot settings and definition-level variable bindings do not come back. COMPONENT_SET is no longer a structural refusal; unknown node types are still refused by name.

0.18.0

Checkpoints rebuild slice nodes. figma_checkpoint records a SLICE's position, size, rotation, visibility, lock and exportSettings, creates a new slice on restore and writes the settings back; the dump and restore verification compare export settings field by field. (COMPONENT_SET was closed in 0.19.0.)

0.17.0

Build-spec video paints survive the loop. figma_export_build_spec carries VIDEO's videoHash, videoTransform, scalingFactor, rotation, filters, visible, opacity and blendMode; figma_build / figma_patch apply them and validate the hash. A video hash is file-local, like an image hash. PATTERN and SHADER paints remain named, not silently dropped.

0.16.0

Checkpoints rebuild boolean operations. figma_checkpoint now walks a BOOLEAN_OPERATION's children and records booleanOperation; restore rebuilds the children and recreates the operation with figma.union / subtract / intersect / exclude, then applies the node's paint, strokes, position, rotation, style links and bindings. Nested booleans are rebuilt inside out; a boolean with fewer than two rebuildable children is named and skipped rather than faked. (COMPONENT_SET was closed in 0.19.0.)

0.15.0

Build-spec paints keep the fields that decide how an image is placed and how a paint composites. figma_export_build_spec now carries an IMAGE paint's imageTransform, scalingFactor, rotation and filters, and every supported paint's visible flag and blendMode; figma_build / figma_patch apply them, including an invisible paint that stays in the stack instead of disappearing. Paint types the build still cannot create (VIDEO, PATTERN, SHADER) remain named, not silently dropped (VIDEO was closed in 0.17.0; PATTERN in 0.21.0 and SHADER in 0.22.0).

0.14.0

Checkpoints rebuild star and polygon shapes natively. figma_checkpoint records STAR's pointCount and innerRadius and POLYGON's pointCount, then applies them before the final size so the outline and the bounding box both come back; direct instance overrides of those fields are replayed too. The named loss for these two node types is closed. BOOLEAN_OPERATION and COMPONENT_SET were still refused by name (BOOLEAN_OPERATION closed in 0.16.0; COMPONENT_SET in 0.19.0).

0.13.0

Per-run text styling inside direct instance overrides is no longer a named loss. figma_checkpoint captures each overridden text layer's runs and, after re-instantiating the main component, replays them range by range with the same helper a rebuilt TEXT node uses: every run font is loaded before the character setter, then sizes, fills, text/fill style links, variable bindings, case, decoration, line height, letter spacing and hyperlinks go back through the range setters. A per-run style link or variable binding that the override REMOVED is not cleared from the main component's current run; that residual was closed in 0.24.0.

0.12.0

Build-spec export keeps the appearance of nodes it cannot express. figma_export_build_spec renders an unsupported node type, a frame created from SVG or a detached instance as an inline SVG (svgFallback, on by default) and emits the type: "SVG" leaf that figma_build already knows how to create; the rebuilt frame keeps its name, key, position, visibility, lock, mask and auto-layout placement, while rotation, opacity, effects, paints and text are baked into the SVG. Every fallback is named in warnings and counted, with a 128 KiB per-node / 192 KiB total budget so the spec stays usable under the 256 KiB build limit; a fallback that cannot be rendered or does not fit is listed under unsupported with the reason. Slices are never flattened. A spec over the build limit is named in warnings. A repeated build recognises the SVG-created frame by its stored kind and updates it in place; a changed svg replaces the interior while keeping the node id and key, and an unchanged source is left alone. The remaining losses are the original child layers, component identity, overrides, style/variable links, editable text and mixed values

0.11.0

Checkpoints carry direct instance overrides. figma_checkpoint save records every direct per-layer override on an INSTANCE as a structural main-component path, the fields Figma reports as overridden, a snapshot of the layer and its style/variable links; restore recreates the instance from its main component and replays the overrides field by field — text characters without flattening the base font, formatting, fills/strokes/effects and their styles, visibility, opacity, blend, rotation, corners, stroke weight, size, position, layout and component properties. A path that no longer matches the main component is named in overridesMissing and skipped rather than applied to the wrong layer; a field this version cannot rebuild is named in overridesUnsupported. An INSTANCE's interior structure beyond those overrides remains the main component's current structure; per-run text styling inside an override was closed in 0.13.0

0.10.0

Direct instance overrides survive the build-spec loop. figma_export_build_spec turns every direct override into an overrides entry with a structural path built from the main component's layer names, plus the fields a rebuild can express — text, font/size, text style, text formatting, hyperlink, fills/strokes and their styles, effects and effect style, visibility, opacity, blend, rotation, corners, stroke weight, size and name — and names every field it cannot express. figma_build / figma_patch accept those overrides by path as well as by layer name/key, resolve styles and fonts before writing, and warn instead of silently skipping a stale path. Instance interiors remained a named loss, and checkpoint restoration of their overrides was closed in 0.11.0

0.9.0

Text variable bindings survive the loop. figma_build / figma_patch accept a base textVariable and a per-run variable map for the eight bindable text fields (fontFamily, fontStyle, fontWeight, fontSize, letterSpacing, lineHeight, paragraphSpacing, paragraphIndent), resolved as STRING/FLOAT local variables before writing; null clears a binding inherited from the base. figma_export_build_spec emits the base binding and each run's different or cleared binding, resolving ids back to variable names, and figma_checkpoint saves and re-links per-run text bindings; a missing variable is named while the literal value remains

0.8.0

Per-run style links survive the loop. A run accepts textStyle and fillStyle (local style name/id; null clears a link), resolved before writing and applied as a real range link. figma_export_build_spec emits the base range's style links and each run's different link instead of flattening to literals, and figma_checkpoint saves and re-links per-run text and fill style ids. Per-range variable bindings were closed in 0.9.0

0.7.0

Text runs survive the whole loop. figma_build / figma_patch accept ordered, non-overlapping runs with per-run font, size, fills, case, decoration, line height, letter spacing and hyperlink; the base text properties are the fallback between runs. figma_export_build_spec turns mixed text into a base style plus runs instead of warning, and names per-run style links and per-range bindings as remaining losses (per-run style links closed in 0.8.0, per-range variable bindings in 0.9.0). figma_checkpoint saves and restores the run values and keeps older checkpoint-v1 files restorable

0.6.0

Text hyperlinks survive the whole loop: figma_build / figma_patch and figma_batch_edit's set_text set URL or NODE links on TEXT (including instance overrides; null clears one), figma_export_build_spec emits URL links and warns that a NODE link is file-local, and figma_checkpoint saves and restores the link by value (mixed per-run links were named as a loss until 0.7.0)

0.5.0

figma_checkpoint restores node-level variable bindings and explicit variable modes by id, re-links paint bindings when their variables still exist, and reports bindingsRestored / bindingsMissing / modesRestored / modesMissing; a missing link leaves the recorded value in place. Non-empty layout grids now travel with the checkpoint too, including their variable bindings. Per-range text bindings stay a named loss. The save summary counts variable bindings and modes separately

0.4.2

Build specs carry frame/component layoutGrids (columns, rows, grid) and an exact LINE length; the reverse export emits both, so layout grids and rotated rulers no longer become warnings or default-length lines

0.4.1

Build specs carry rotation, individual corner radii, stroke cap/join/miter, masks, layout inclusion flags, and text case/decoration/paragraph spacing; figma_batch_edit gains set_rotation. figma_export_build_spec emits these instead of naming them as losses; a line still reports that its length is not representable

0.4.0

The declarative editing layer: figma_build and figma_patch (a keyed tree, resolved before writing, content planId, fingerprint-guarded apply, rollback and post-apply verification), figma_batch_edit (many targets in one transaction with preflight and per-target fingerprints), figma_audit (stable keys, hardcoded paints, text styles, placeholders, detached instances, WCAG contrast, opt-in spacing/radius), and figma_index / figma_search (a compact page index and substring/regex/glob node search), plus figma_design_system (styles, variables and components as one snapshot; export with a sha256 and a stable-key diff against the live file) and figma_export_build_spec (a live subtree back into the same keyed build language, naming every field the Plugin API cannot carry). Checkpoint restores now re-link fill/stroke/effect/text style ids when the style still exists, and audit contrast composites translucent foreground and ancestor fills before measuring. Builds and patches store keys in pluginData; duplicates rekey descendants; protected nodes survive prune. All new tools are covered by unit tests and the daemon/plugin e2e chain

0.3.0

figma_compare (a render against a PNG on disk, read and diffed without graphics dependencies), figma_checkpoint (a subtree as data: save, diff, rebuild, verify — with what a rebuild does not bring back named), figma_ops and figma_cancel (a timed-out call is settled by its recorded answer; a queued write nobody waits for is stopped), the durable append-only journal, the flb snippet library for figma_use (bboxes, grid, text, clear — its write helpers refuse out-of-scope targets before writing), the export tools (figma_export_ui_atlas, figma_export_9slice, figma_export_ui_states) and the web handoff bundle: one folder per frame with previews, spec.json, tokens and a hashed manifest re-read from disk, a 24 MP preview refused before the rasteriser, a font the time-boxed check cannot confirm reported as unchecked, and per-phase timings in the reply; an MCP client that starts while the plugin is already paired now learns the daemon's state instead of reporting it offline; plus an unsaved file's session key kept on the document

0.2.0

Several Figma files at once (16 slots, an explicit fileKey on every tool), figma_get_layout_tree, opId idempotency with a replay cache, serialised mutations, per-side stroke weights and pinned variable modes, the operating guide as MCP instructions and as a resource, npm run smoke, and the panel that fits its own content — with the octopus redrawn on the panel's grid, at half the font size, answering a tap

0.1.0

The first cut: one daemon, one token, one Figma file, the tool surface, and the safety model

License

MIT — see LICENSE. Unofficial project, not affiliated with Figma, Inc.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Figma MCP server that lets agents execute arbitrary JavaScript inside a running Figma desktop app via a local development plugin, enabling real-time document inspection and manipulation while avoiding REST API rate limits.
    3
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    The Figma desktop app as an MCP server. Gives AI agents the full Figma Plugin API on your open file: read, write, tokens, components, exports, undo. Runs locally with no cloud API, no token and no quota.
    10
    76 npm
    2
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Connects an MCP client such as Claude Code to the Figma file open in the desktop app, letting the agent create frames, write text, apply styles, import images and SVGs, take screenshots, and run arbitrary Figma Plugin API code. Commands travel through a local development plugin instead of Figma's web API, so there are no tokens, quotas, or rate limits.
    59 npm
    3
    MIT