Figma Local Bridge
Local MCP bridge that drives the real Figma Desktop plugin API, letting agents read and edit the open Figma file, take screenshots, export/import assets, inspect variables and design-system components, search libraries, and run arbitrary Plugin API snippets under scope protection — no Dev/Full seat required.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
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 |
|
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 |
|
Files at once | Up to 16 open Figma files, routed by |
Tool surface | 17 parity + 33 local = 50 tools; +5 optional cloud tools = 55 when enabled |
Writes | Local writes guarded: dry run, |
Verification |
|
Runtime | Node.js 20+ |
Installer | macOS 0.35.1: double-click |
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 tokenThen 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." |
|
"Inspect this frame / find its node id." |
|
"Take a screenshot or export a PNG/SVG." |
|
"Build or update this UI from a keyed spec." |
|
"Make many small edits in one transaction." |
|
"Apply or remove an animation style, keyframes, or timeline duration." |
|
"Audit the design system and suggest tokens." |
|
"Compare the render with a mockup." |
|
"Save a baseline, edit, then tell me what changed." |
|
"Export, diff, or import design tokens." |
|
"Swap a component or build a variant matrix." |
|
"Save/restore a subtree without an undo stack." |
|
"Export a sprite atlas, 9-slice, states, or web handoff." |
|
"Search the design system or file." |
|
"Run a raw Plugin API snippet." |
|
"A write timed out — did it run?" |
|
"Read a file that is not open in Figma." |
|
"Give me render URLs for a remote Figma node." |
|
"Download remote Figma renders to disk." |
|
"Check whether my Figma REST token works." |
|
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 / CursorThe 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 |
| stdio MCP server, daemon (WebSocket hub), link state, tool implementations |
| Figma plugin: |
| macOS installer, uninstaller, double-click wrappers and the pure config/plist helpers |
| builds the self-contained macOS zip ( |
| the installable agent skill: a short playbook with frontmatter, safe to copy into |
| the full operating guide, served both as MCP |
| 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 |
|
|
| short plain-text project summary |
| unit tests ( |
| 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 .githooksThe 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.
Build the zip from a checkout with
npm run build:release(or use a release artifact attached by the maintainer). The archive isrelease/figma-local-bridge-<version>-macos.zip.Unzip it and double-click
Install.command. If macOS Gatekeeper blocks an unsigned download, use right-click → Open once. The installer is unsigned in0.35.1; signing and notarization are a later step.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.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.Restart the MCP client.
figma_pingshould 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.logImport the plugin (once): in Figma → Plugins → Development → Import plugin from manifest… →
<repo>/plugin/manifest.json. (npm run buildmust have run first —plugin/code.jsis generated and not committed.)Run the plugin: Plugins → Development → Figma Local Bridge.
The panel asks for the daemon token — copy it from
~/.local/share/opencode/figma-bridge-daemon.jsonand paste it once. It is kept infigma.clientStorage, and the plugin reconnects by itself on every launch. The port is fixed byplugin/manifest.json(networkAccess.devAllowedDomains) — the panel cannot change it.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 |
| from | Daemon port |
|
| Interfaces to listen on |
|
| State file: pid, port, |
|
| Persistent plugin token ( |
|
|
|
|
|
|
|
| Where |
|
| Where |
|
| Where the journal keeps |
|
|
|
| unset |
|
| unset | PAT ( |
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_pinglists 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 withoutfileKeyis refused before anything is sent, listing the candidates. Guessing here would edit the wrong file. The optional cloud tools use a requiredfileKeynaming 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. |
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 |
|
chips: | 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: | 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 |
| User, open file, pages, viewport |
| Sparse XML skeleton of a node — cheap, use it to find ids |
| Full design description of a node for implementing it in code |
| PNG / JPG / SVG of a node (the caption carries |
| Variables used in a subtree |
| Variable collections available from libraries |
| Search components, styles, variables |
| FigJam board: stickies, shapes, connectors, sections |
| Local Code Connect mappings |
| Export / import assets |
| Switch the active page |
| Select nodes and pan the viewport to them — the only way to move the user's cursor |
| Fonts actually used by the file, with per-text-node counts |
| Inventory of every component in the file |
| Arbitrary Plugin API snippet under scope protection; the answer carries |
Local additions:
Tool | What it does |
| Health check and capability report: verifies the plugin is paired, reports the active page and connected files ( |
| Current selection, answered from memory — the plugin pushes selection changes, so this costs no round trip |
| Absolute geometry of a whole subtree in one call: per node |
| Compact structure + semantic fingerprint; |
| 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 |
| Sparse update of an existing keyed tree: |
| Many small edits on many targets in one transaction. Ops: |
| 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 |
| Author a |
| Read-only design-system audit ( |
| 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 |
| Find nodes by name, stable key or text without fetching the whole tree: substring/regex/glob, node-type and key-prefix filters, |
| 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. |
| Design-token sync for variables. |
| Read-only Figma Motion (Beta) catalogue: |
| 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. |
| Apply a Figma or custom animation 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. |
| Set one manual keyframe track: a property ( |
| 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. |
| Set a Motion timeline duration in seconds from a |
| The reverse of |
| Diff a live render against a PNG on disk — a mockup, an earlier export, or another node written out with |
| The iteration harness for one subtree. |
| Replaces repeated identical nodes with instances of one master component, in a single transactional call. Refuses unless every target is provably equivalent; try |
| Undo, without an undo stack: |
| What the plugin is doing per file: the call that is |
| Stop an operation whose state is |
| The durable half of |
| 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 |
| One sprite with its 9-slice insets: a PNG plus a descriptor — insets in the file's pixels, the stretchable middle, and CSS |
| The states of a button/component as one file per state, plus a manifest naming them. A |
| One folder for a web developer: |
Optional cloud tools (enabled only by FIGMA_CLOUD=1 plus a token):
Tool | What it does |
| Validate |
| Read a file's JSON with optional |
| Read specific nodes from a file by |
| 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 |
| Download nodes as PNG/JPG/SVG/PDF files to |
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 |
| Explicit opt-in. |
| Personal access token, sent as |
| OAuth access token, sent as |
| Request timeout, 1000–120000 ms (default 30000) |
| Defaults to |
| Allows plain HTTP only for |
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_patchandfigma_batch_editdefault todryRun:true. The dry run resolves every resource and returns a contentplanIdplus a fingerprint; an apply with a differentplanIdis 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 undernewKeyinstead of inheriting colliding keys. A batch move resolves both ends: the target by key and, when given, the destination byparentKey. 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_editresolves 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, withstopOnError:false, stops pretending: it reports the exact op that failed and what succeeded).A component swap is a migration, not a relink.
figma_swap_componentrecords 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 theplanIdand per-target fingerprints that cover the instance and the target component; a lossy swap needsallowLoss, a library import needsallowUnverified, a target inside anotherINSTANCEis refused, and an unexpected loss during apply rolls the whole transaction back. The report keeps predicted losses separate from restored overrides, soswapComponent's editor heuristics are never presented as a contract.A variant set is a matrix, not a folder.
figma_variant_setvalidates 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 readsvariantPropertiesandvariantGroupPropertiesback 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 saysROLLBACK FAILEDinstead of claiming a rollback.A token import is a plan, not a paste.
figma_tokensexports 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 theplanId, 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 saysROLLBACK FAILEDrather than claiming success.An iteration is measured against a baseline.
figma_reviewsaves 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 resultregressed, resolved problems make itimproved, and a pixel/structure change without either is a neutralchanged— 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_auditonly reads; its findings are claims about the file, not a cleanup that might have changed it. The same is true offigma_index,figma_searchandfigma_design_system.The cloud adapter can only read Figma. The optional REST tools are registered only after an explicit
FIGMA_CLOUD=1plus a token, call documentedGETendpoints, carry their credential in a header (never a URL or reply), refuse redirects, cap the response size before serialising it, and keep401/403/404/429as 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_imageis 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. Whenfigma.motionor a node-level method is absent, the call fails withMOTION_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 thatplanId, 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_systemcompares two exports by Figma's stable style/variable/component keys, and drops only the volatile nodeidfrom 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.
opIdis minted once per logical call and reused by every attempt (server/src/figma.ts). The plugin keeps a boundedopId → resultcache 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 usedopIdis 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.revbumps on success and failure.Honest disconnects. An error distinguishes
disconnected before <method> was sent(safe to retry) frommay or may not have been applied(retried under the sameopId, 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_opsshows itqueued(nothing of it has run),running, or finished after the caller gave up with the answer it returned;figma_cancelstops aqueuedone, answering its caller immediately withOP_CANCELLEDand leavingrevuntouched because the document did not move. Arunningcall 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_opssettles 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_journalreads its tail and can show only the calls that did not end cleanly; asendwith 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
fileKeyis 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_useruns 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, orfigma_set_pagefirst. The snippet's own helpers (flb) make the same judgement before a write: a target outsidescopeNodeId, 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.racestops 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.
revtravels in the caption, so identical bytes with a higherrevmeans the render is stale, not that the edit did nothing.A fallback is named, not disguised.
figma_export_build_speccan 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 inwarnings, 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 becausefigma_buildrefuses 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 andhandoff.jsonname 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=0makes the bridge read-only;figma_useadditionally needsFIGMA_BRIDGE_DESTRUCTIVE=1and an explicit scope.Checkpoints are measured, not promised.
figma_checkpointhas 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 inmeta; 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 needsdryRun: falseandallowDestructive: trueand a bridge started with destructive writes; after the write the server dumps the subtree again and diffs it against the checkpoint, soverified: truemeans "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=1npm 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 |
| The plugin is not running in Figma, or the daemon is down |
| The |
| Several files are connected and no |
| The plugin is not paired; the optional cloud adapter can still work without it |
| The adapter is off. Set |
| The token is invalid/expired, or it lacks the scope: |
| Figma returned |
| The reply exceeded |
| The REST request failed before a reply; check network/proxy and |
| The image URL was not HTTPS (except localhost with |
| The downloaded bytes do not have the requested PNG/JPEG/SVG/PDF signature. Nothing is written; the URL probably returned an error page |
| The target exists and |
The daemon won't start | Port 8787 is busy — |
| 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 |
| The daemon already serves 16 files — close a panel |
| The daemon restarted with a new |
| The token in the plugin doesn't match |
| Three separate gates, each with its own message: a real rebuild needs |
| The second dump differs from the checkpoint: read |
A write timed out — did it ever run? | The timeout error names its |
The bridge died or was restarted — what was in flight? |
|
Liveness check: lsof -nP -iTCP:8787 | grep -i figma — empty output means the plugin is not paired.
Limitations
The
0.35.1installer is macOS-only and unsigned. Gatekeeper can require right-click → Open on the first launch; a signed/notarized.pkgor.dmgis 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 importingplugin/manifest.jsonand pasting the token remain manual.Motion support is Figma Motion (Beta) and depends on
figma.motionplus the node-level Motion methods being present in the running Figma build. When they are not, every Motion tool fails withMOTION_UNAVAILABLE— it never writespluginDatathat merely looks like an animation. Motion writes require a dry-runplanId, 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=1and a PAT/OAuth token; the token's scopes decide what works (file_content:readfor files/nodes/images,current_user:readforfigma_cloud_ping). It exposes no Figma writes, does not run an OAuth authorization flow, and cannot refresh an expired OAuth token.figma_cloud_get_imagereturns temporary URLs;figma_cloud_download_imageis the explicit byte-downloading tool and writes only local files. A response overmaxBytesis refused; usenodeIdsanddepthinstead of raising the cap blindly. A customFIGMA_CLOUD_API_BASEmust be HTTPS, except for a localhost test server withFIGMA_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.indexTreewalks 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;
revmakes stale renders visible, but there is no way to force a redraw from the Plugin API.figma_usehands 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
COMPONENTis a new component (its instances do not follow), and anINSTANCEshows its main component's current content. Every one of those is reported by the tool rather than hidden — see thelimitationslist in asavereply.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_editwithreparentand aparentIdorparentKeywhen a move is intended.A batch cannot target a node created by an earlier
duplicatein 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
INSTANCEor aCOMPONENT_SETis refused because that interior is an override or a variant set, not a normal layer tree. A destination inside aCOMPONENTis 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 whosenewKeyalready 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
INSTANCEis 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 requiresallowUnverified:true, then compares the imported structure and reports any loss. Variant properties are not replayed: the chosen target component or variant decides them.SLOTproperties, 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_settreats 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.combinerequires 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 withfigma_batch_editif placement matters.addclones the source variant's structure, not its instances, and offsets the clone by 24px when the set is not auto-layout.editkeeps the variant id and its instances, but changes their variant property values. A set whose variants do not all exposevariantPropertiesis refused rather than guessed at. A combine destination inside anINSTANCE, anotherCOMPONENT_SETor aCOMPONENTdefinition is refused; Figma will not reparent a component into a component definition.addandeditstill operate on an existing set inside a component and name that edit.figma_tokensmanages variables, not paint/text/effect/grid styles. Its nativefigma-tokens/v1format 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 withremote: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'sresolvedTypecannot be changed in Figma, so a type change is refused. Export skipsEASING/TIMINGvariables 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.prunedeletes 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; adimensionloses itspxunit because a Figma FLOAT is unitless; afontFamilyarray 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_reviewis 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 thefileKeyand stored review id, not to a published version; a node rebuilt with a new id needs the newnodeIdpassed tocompare. A screenshot can be stale after a document change (the storedrevmakes that visible), a missinginspectleaves the structure comparisonnull, and a truncated audit limits the new/resolved lists even though the counts are complete. Usefigma_checkpointfor restoration andfigma_comparewhen 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
allModessays 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-textandhardcoded-effectare opt-in rules, not defaults.
Versions
Version | What it carries |
| Performance pass: |
| Figma Motion (Beta) support: |
| Dynamic-page correctness and a reusable agent skill. |
| macOS double-click installer and release packaging. |
|
|
| Optional read-only cloud access. With |
|
|
|
|
|
|
|
|
|
|
| Keyed moves resolve both ends. |
|
|
| Per-run overrides now clear what they removed. When a direct instance override touched range styling, |
| Checkpoints restore component-property metadata. After adding back a |
| Build-spec shader paints close the paint gap. |
| Build-spec pattern paints survive the loop. |
| Build specs create boolean operations. |
| Checkpoints rebuild component sets. |
| Checkpoints rebuild slice nodes. |
| Build-spec video paints survive the loop. |
| Checkpoints rebuild boolean operations. |
| Build-spec paints keep the fields that decide how an image is placed and how a paint composites. |
| Checkpoints rebuild star and polygon shapes natively. |
| Per-run text styling inside direct instance overrides is no longer a named loss. |
| Build-spec export keeps the appearance of nodes it cannot express. |
| Checkpoints carry direct instance overrides. |
| Direct instance overrides survive the build-spec loop. |
| Text variable bindings survive the loop. |
| Per-run style links survive the loop. A run accepts |
| Text runs survive the whole loop. |
| Text hyperlinks survive the whole loop: |
|
|
| Build specs carry frame/component |
| Build specs carry rotation, individual corner radii, stroke cap/join/miter, masks, layout inclusion flags, and text case/decoration/paragraph spacing; |
| The declarative editing layer: |
|
|
| Several Figma files at once (16 slots, an explicit |
| 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.
This server cannot be deployed
Maintenance
Related MCP Connectors
The Figma MCP server brings Figma design context directly into your AI workflow.
Host any HTTP API as a standard MCP server; auto-distribute to 12+ mainstream agent platforms.
MCP connector that lets ChatGPT list, search, and run your Apple Shortcuts via a local Mac agent
MCP connector for iMessage & Contacts via a local Mac agent + Vercel relay
Related MCP Servers
- AlicenseBqualityCmaintenanceLocal MCP server connecting AI clients to the Figma desktop app for inspecting and editing Figma documents via the Plugin API.1616 npm1MIT
- AlicenseAqualityDmaintenanceFigma 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.3MIT
- AlicenseAqualityBmaintenanceThe 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.1076 npm2MIT
- AlicenseNot gradedqualityBmaintenanceConnects 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 npm3MIT