figma-bridge
README.md
# 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](https://developers.figma.com/docs/figma-mcp-server/) 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](https://datatracker.ietf.org/doc/html/rfc6455) — 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
```bash
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
<details open>
<summary><b>Claude Desktop</b></summary>
**Settings → Developer → Add local MCP server:**
```json
{
"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`.
</details>
<details>
<summary><b>Claude Code</b></summary>
```bash
claude mcp add --scope user figma-bridge \
--env FIGMA_TOKEN=figd_your_token_here \
-- node /path/to/claude-figma-bridge/server/figma-bridge.js
```
</details>
<details>
<summary><b>Any other MCP client</b></summary>
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_...
```
</details>
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:
```js
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:
```js
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:
```js
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](https://www.figma.com/plugin-docs/api/api-reference/).
---
## 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
```bash
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
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues