Skip to main content
Glama
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