figma-bridge
An MCP server that connects Claude to Figma — read files, and create or edit anything the Figma Plugin API can reach.
Works with any MCP client, including ones Figma's official remote server refuses to serve.
Why this exists
Figma ships an official remote MCP server at https://mcp.figma.com/mcp. It is good, and if your client is on Figma's MCP Catalog you should use it instead of this.
The catch is that the server gates OAuth dynamic client registration by client name. Testing POST https://api.figma.com/v1/oauth/mcp/register:
client_name |
Response |
|---|---|
Claude Code |
200 OK |
Claude Desktop |
403 Forbidden |
Visual Studio Code |
403 Forbidden |
Cursor |
403 Forbidden |
Unlisted clients cannot complete the OAuth handshake, so they fail with a bare "Connection to server failed" and no explanation. Figma's docs confirm the policy: "Only clients listed in the Figma MCP Catalog like VS Code, Cursor, or Claude Code can connect to the Figma MCP Server."
figma-bridge sidesteps registration entirely. It talks to Figma two ways you already control: a plugin you install yourself, and a personal access token you issue yourself.
Architecture
┌────────┐ stdio JSON-RPC ┌─────────────────┐ WebSocket 127.0.0.1 ┌───────────────┐
│ Claude │ ◄───────────────► │ figma-bridge.js │ ◄───────────────────► │ Figma plugin │
└────────┘ └─────────────────┘ └───────────────┘
│ HTTPS + personal access token
▼
┌───────────────┐
│ api.figma.com │
└───────────────┘
Two paths, because of a hard constraint worth understanding:
The REST API cannot create nodes. It is almost entirely read-only. So writes cannot go through a token.
The Plugin API can create anything, but only exists inside a running plugin. There is no way to call it from outside Figma.
So writes are relayed to a plugin over a loopback WebSocket, and reads that REST can answer go straight to api.figma.com. The practical consequence: creating things requires the plugin window to be open; reading does not.
The plugin's main thread has the Plugin API but no network access, so the WebSocket lives in the plugin's UI iframe and messages hop across via postMessage.
Zero dependencies
No npm install. WebSocket framing is implemented directly against RFC 6455 — masking, the three payload-length encodings, continuation frames, and ping/pong. One file, Node's standard library only.
Requirements
- Node.js 18+ — for the bridge server
- Figma desktop app — plugin development is not available in the browser
- A Figma personal access token — optional, enables the REST read tools
Installation
1. Get the code
git clone https://github.com/sinahosseini379/claude-figma-bridge.git
Put it somewhere permanent — the path goes into your MCP config.
2. Create a Figma token (optional)
In Figma: Settings → Security → Personal access tokens → Generate new token.
Scopes needed: File content (read), and Variables (read) if you want design tokens.
Skip this if you only care about writes; the plugin path does not use it.
3. Register the MCP server
Claude DesktopSettings → Developer → Add local MCP server:
{
"command": "node",
"args": ["C:\\path\\to\\claude-figma-bridge\\server\\figma-bridge.js"],
"env": {
"FIGMA_TOKEN": "figd_your_token_here"
}
}
If the Developer section is missing, an administrator must enable isLocalDevMcpEnabled.
claude mcp add --scope user figma-bridge \
--env FIGMA_TOKEN=figd_your_token_here \
-- node /path/to/claude-figma-bridge/server/figma-bridge.js
Any other MCP client
It speaks MCP 2024-11-05 over stdio with newline-delimited JSON-RPC 2.0. Point your client at:
command: node
args: ["/path/to/server/figma-bridge.js"]
env: FIGMA_TOKEN=figd_...
Restart your client afterwards. MCP servers load at startup, so an already-open session will not see the new tools.
4. Install the Figma plugin
In the Figma desktop app: Menu → Plugins → Development → Import plugin from manifest… and pick plugin/manifest.json.
This is a local development plugin. Nothing is published, and nothing leaves your machine.
Usage
Open the plugin from Plugins → Development → figma-bridge. Leave the window open — closing it drops the connection. A green dot means the bridge is live.
Then ask your client for what you want. Read-only requests work with the plugin closed.
Environment variables
| Variable | Default | Purpose |
|---|---|---|
FIGMA_TOKEN |
— | Personal access token. Without it the REST tools return an error and only the plugin tools work. |
FIGMA_WS_PORT |
3055 |
Loopback port for the plugin bridge. Change it if something else owns 3055; set the same number in the plugin window. |
Tools
| Tool | Plugin open? | What it does |
|---|---|---|
figma_status |
no | Reports whether the plugin is connected and whether a token is configured. Start here when something breaks. |
figma_run |
yes | Runs JavaScript inside the plugin sandbox with the full Plugin API in scope. The main write tool. |
figma_current_selection |
yes | The current page, all pages, and what the user has selected. |
figma_get_file |
no | A file's node tree over REST, depth-limited. |
figma_get_node |
no | Full detail for specific nodes. |
figma_get_images |
no | Renders nodes to PNG/SVG/JPG/PDF and returns URLs. |
figma_get_variables |
no | Published design tokens. Enterprise plans only. |
figma_me |
no | The authenticated account — use it to verify a token. |
File arguments accept a full Figma URL or a bare file key; /file/, /design/, /board/, and /slides/ URLs all work. Node ids copied from URLs (1-23) are converted to API form (1:23) automatically.
figma_run
This is where the power is. The code argument is a function body executed with figma in scope and awaited, so return a value to get it back.
Create a flowchart in FigJam:
const nodes = [];
for (const [i, label] of ['Start', 'Validate', 'Save', 'Done'].entries()) {
const s = figma.createSticky();
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
s.text.characters = label;
s.x = i * 260;
s.y = 0;
nodes.push(s);
}
for (let i = 0; i < nodes.length - 1; i++) {
const c = figma.createConnector();
c.connectorStart = { endpointNodeId: nodes[i].id, magnet: 'AUTO' };
c.connectorEnd = { endpointNodeId: nodes[i + 1].id, magnet: 'AUTO' };
}
figma.viewport.scrollAndZoomIntoView(nodes);
return { created: nodes.length };
Build a card component with auto-layout:
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
const card = figma.createFrame();
card.name = 'Card';
card.layoutMode = 'VERTICAL';
card.primaryAxisSizingMode = 'AUTO';
card.counterAxisSizingMode = 'FIXED';
card.resize(320, 100);
card.itemSpacing = 8;
card.paddingTop = card.paddingBottom = 16;
card.paddingLeft = card.paddingRight = 16;
card.cornerRadius = 12;
card.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
const title = figma.createText();
title.fontName = { family: 'Inter', style: 'Bold' };
title.characters = 'Card title';
card.appendChild(title);
return { id: card.id, name: card.name };
Define colour variables:
const collection = figma.variables.createVariableCollection('Brand');
const modeId = collection.modes[0].modeId;
const created = [];
for (const [name, hex] of Object.entries({ primary: [0.05, 0.6, 1], danger: [0.88, 0.19, 0.19] })) {
const v = figma.variables.createVariable(name, collection, 'COLOR');
v.setValueForMode(modeId, { r: hex[0], g: hex[1], b: hex[2] });
created.push(v.name);
}
return { collection: collection.name, variables: created };
Three rules to keep in mind:
- Await async Plugin API calls.
loadFontAsync,getNodeByIdAsync, and friends. Text operations fail silently if the font is not loaded first. - Return plain JSON. Node objects are not serialisable. Return ids and names, not nodes.
- Colours are 0–1, not 0–255.
{ r: 1, g: 0, b: 0 }is red.
Reference: Figma Plugin API docs.
Troubleshooting
"Figma plugin is not connected" — the plugin window is closed, or its dot is not green. Reopen it from Plugins → Development → figma-bridge.
The plugin will not connect — the bridge server is not running. Your MCP client normally launches it; to check by hand, run node server/figma-bridge.js and look for bridge listening on 127.0.0.1:3055. You can also open http://127.0.0.1:3055 in a browser — it returns the bridge's status as JSON.
EADDRINUSE — something else has port 3055. Set FIGMA_WS_PORT in the MCP config and the matching port in the plugin window.
REST tools return 403 — the token is wrong, expired, or missing a scope. Call figma_me to tell a bad token apart from other failures.
REST tools return 404 — usually a file key parsed from an unexpected URL shape. Pass the bare key instead.
No figma_* tools at all — the client did not pick up the server. Confirm the config path is absolute, then fully restart the client (quit it, including any tray icon) and start a new conversation.
"sandbox blocks dynamic code evaluation" — some Figma builds disable new Function. figma_run cannot work there; the explicit selection and page_contents commands still do. Open an issue and more first-class commands can be added.
Security
The bridge binds to 127.0.0.1 only, so it is not reachable from the network. Two things are still worth knowing:
There is no authentication on the local port. Any process on the same machine can connect and ask the plugin to run code. This was a deliberate tradeoff for single-user local use, not an oversight. If you share the machine or run untrusted code on it, add a shared secret before using this.
figma_run executes arbitrary code in your file, including destructive operations. It can delete nodes and pages. Work on a duplicate when the file matters. Figma's own undo history covers plugin changes, but do not rely on it alone.
Your token sits in the MCP client's config. Treat that file as a secret. The bridge never logs the token and never writes it anywhere.
Development
node server/test-bridge.js
19 tests covering the MCP handshake and tool listing, error paths before the plugin connects, request/response round-trips, plugin-side error propagation, 20 concurrent in-flight calls, a 200 KB payload (exercising the 64-bit length header), multi-byte UTF-8, and disconnect handling. The harness spawns the real server and stands in for the plugin with a fake WebSocket client, so no Figma access is needed.
Not covered by automated tests: the plugin actually running inside Figma. That needs a human with Figma open.
Layout
server/
figma-bridge.js MCP server, WebSocket bridge, REST client
test-bridge.js test harness
start-bridge.ps1 manual launcher for debugging (Windows)
plugin/
manifest.json plugin manifest
code.js main thread — has the Plugin API, no network
ui.html UI iframe — has network, no Plugin API
Adding a tool
Append an entry to the TOOLS array in server/figma-bridge.js with a name, a description the model will read, an inputSchema, and an async handler. Handlers reach Figma via bridgeSend(command, params) or the REST API via restGet(path). To add a first-class plugin command, add it to COMMANDS in plugin/code.js as well.
License
MIT