# Figma Desktop Bridge

A Figma plugin that bridges the Variables API and Component descriptions to MCP (Model Context Protocol) clients without requiring an Enterprise plan. Supports both local MCP servers and cloud relay connections from web-based AI clients.

## Overview

This plugin enables AI assistants to access your Figma variables AND component descriptions through the MCP protocol. It serves two purposes:

- **Local MCP bridge** — Connects to a local MCP server via WebSocket on localhost (Claude Code, Claude Desktop, Cursor)
- **Cloud relay bridge** — Connects to a cloud relay via WebSocket over TLS, enabling web-based AI clients (Claude.ai, v0, Replit, Lovable) to send write commands to Figma

Both modes bypass Figma's plugin sandbox restrictions and the REST API's component description bug.

## Architecture

The plugin communicates with MCP servers via WebSocket through two paths:

```
LOCAL:  MCP Server ←WebSocket (ports 9223–9232)→ Plugin UI ←postMessage→ Plugin Worker → Figma API
CLOUD:  Cloud MCP Server → Relay DO ←WebSocket (wss://)→ Plugin UI ←postMessage→ Plugin Worker → Figma API
```

For local mode, the server supports multi-instance operation — if port 9223 is already in use, it automatically falls back through ports 9224–9232. The plugin scans all ports and connects to every active server.

For cloud mode, the plugin connects to a Cloudflare Durable Object relay via a one-time pairing code. Web-based AI clients send write commands through the relay to the plugin.

**Key Features:**
- ✅ No Enterprise plan required for variables
- ✅ Access all local variables and collections
- ✅ Reliable component descriptions (bypasses REST API bug)
- ✅ Supports multiple variable modes
- ✅ On-demand component data retrieval
- ✅ Persistent connection (stays open until closed)
- ✅ Clean, minimal UI
- ✅ Real-time data updates
- ✅ WebSocket transport — no debug flags needed
- ✅ Auto-reconnect on connection loss
- ✅ Multi-instance: connects to all active MCP servers simultaneously (v1.10.0)
- ✅ Cloud Mode: pair with web-based AI clients (Claude.ai, v0, Replit, Lovable)
- ✅ Full write access from any MCP-capable web platform

## Installation

### Quick Install (Recommended)

1. **Open Figma Desktop**
2. **Go to Plugins → Development → Import plugin from manifest...**
3. **Navigate to:** `/path/to/figma-console-mcp/figma-desktop-bridge/manifest.json`
4. **Click "Open"**

The plugin will appear in your Development plugins list as "Figma Desktop Bridge".

### Manual Installation

Alternatively, you can install from the plugin directory:

```bash
# From the figma-console-mcp directory
cd figma-desktop-bridge

# Figma will use these files:
# - manifest.json (plugin configuration)
# - code.js (plugin worker logic)
# - ui.html (plugin UI interface)
```

## Usage

### Running the Plugin

1. **Open your Figma file** with variables and/or components
2. **Run the plugin:** Right-click → Plugins → Development → Figma Desktop Bridge
3. **Wait for confirmation:** the status strip turns green and shows `READY`

The plugin will:
- Fetch all local variables and collections on startup
- Display counts in the UI (e.g., "Variables: 404 in 2 collections")
- Store variables in `window.__figmaVariablesData`
- Provide on-demand component data via `window.requestComponentData(nodeId)`
- Keep running until manually closed

### Accessing Data via MCP

Once the plugin is running, MCP clients can access both variables and components:

**Variables (pre-loaded):**
```typescript
// From Claude Code or Claude Desktop
figma_get_variables({
  format: "summary"  // or "filtered" or "full"
})
```

**Components (on-demand):**
```typescript
// Request component with description
figma_get_component({
  fileUrl: "https://figma.com/design/YOUR_FILE_KEY",
  nodeId: "279:2861"
})
```

**Important:** Keep the plugin running while querying. Variables are pre-loaded, but component data is fetched on-demand when requested.

## Plugin Interface

The plugin is a compact, collapsible status panel designed to stay out of the way of the canvas. It opens as a small strip and expands only when you ask it to.

### Status strip (always visible)

- **Status** — a coloured dot + label (`READY`, `CONNECTING`, disconnected) showing whether the bridge is live.
- **Pause / Reconnect** — the action button:
  - `Pause` (when connected) — disconnects and stops auto-reconnect until you resume.
  - `Reconnect` (when not connected) — runs a fresh port scan and connects on the spot, with no need to close and reopen the plugin.
- **Cloud icon** — opens cloud pairing (see [Cloud Mode](#cloud-mode)).
- **`+` / `−`** — expand/collapse the options and panels.

### Options (under `+`)

- **Info** — shows the connected **File** and **Page**, confirming which file the bridge is bound to (useful when several MCP servers or files are in play).
- **Log** — opens the activity log (below).
- **`N server(s)` badge** — how many MCP servers the plugin is currently connected to. A surprisingly high count is a useful signal that stale/zombie server instances are lingering (see [Multi-Instance Support](#multi-instance-support-v1100)).

### Activity log

The log tells the story of what the AI did, rather than listing raw API traffic:

- Each entry is a human-readable label taken from a leading `// comment` in executed code (e.g. `// Apply drop shadow` → "Apply drop shadow"), a recognised Figma API call, or a `<Code>` prefix for unrecognised raw JS.
- Errors are shown in red and prefixed `[!]`.
- Consecutive duplicate actions collapse to one line with a `×N` repeat count.
- The ⚠ control filters to errors only; the copy button exports the full session log as plain text (handy for bug reports and demos).

The log records actions as they happen while the panel is in memory — it reflects activity since the plugin connected.

### Theme

The plugin follows Figma's light/dark theme automatically — there is no separate theme control to manage.

### Connection & auto-reconnect

- On launch the plugin scans ports 9223–9232 and connects to every MCP server it finds.
- If no server is running yet (for example, you opened the plugin before starting your AI client), a background watchdog keeps probing while disconnected and connects automatically as soon as a server appears — **no plugin restart needed**. The `Reconnect` button forces an immediate retry.
- On the server side, stale/zombie MCP instances that would otherwise squat the port range are reaped automatically (SIGTERM, escalating to SIGKILL), so a freshly launched server can claim its preferred port instead of being pushed onto a fallback the plugin then has to chase.

## Cloud Mode

Cloud Mode lets web-based AI clients (Claude.ai, v0, Replit, Lovable) send write commands to your Figma file through a cloud relay. This enables the full suite of design creation and variable management tools from any MCP-capable web platform — no local Node.js required.

### How to Connect

1. **Start pairing from the AI client.** The AI client calls `figma_pair_plugin`, which generates a 6-character pairing code (valid for 5 minutes).
2. **Open the Desktop Bridge plugin** in Figma Desktop.
3. **Expand Cloud Mode.** Click the "Cloud Mode" toggle (chevron) below the status bar.
4. **Enter the pairing code** in the text input and click **Connect**.
5. The plugin establishes a WebSocket connection to the cloud relay. The status indicator changes to "Connected to cloud relay."

### Disconnecting

Click the **Disconnect** button in the Cloud Mode section, or close the plugin. The cloud relay session is terminated immediately.

### Notes

- **Both local and cloud connections can be active simultaneously.** Local MCP servers continue to work while Cloud Mode is connected.
- **Pairing codes are single-use.** Each code can only be used once. If connection fails, generate a new code from the AI client.
- **Codes expire in 5 minutes.** If the code has expired, ask the AI client to generate a fresh one.
- **One plugin per relay session.** Each pairing code creates a dedicated relay instance scoped to that session.

## How It Works

### Plugin Worker (code.js)

**On Startup (Variables):**
1. Uses Figma's Variables API to fetch all local variables
2. Formats data with full mode values
3. Sends to UI via `postMessage`

**On Request (Components):**
1. Listens for component requests via `figma.ui.onmessage`
2. Uses `figma.getNodeByIdAsync(nodeId)` to fetch component
3. Extracts description, descriptionMarkdown, and metadata
4. Sends response back to UI via `postMessage` with requestId

### Plugin UI (ui.html)

**Variables Flow:**
1. Listens for `VARIABLES_DATA` message from worker
2. Stores data on `window.__figmaVariablesData`
3. Sets `window.__figmaVariablesReady = true`
4. Displays status to user

**Components Flow:**
1. Exposes `window.requestComponentData(nodeId)` function
2. Returns a Promise that resolves when worker responds
3. Sends request to worker via `parent.postMessage()`
4. Resolves promise when `COMPONENT_DATA` message received
5. Includes 10-second timeout and error handling

### MCP Desktop Connector

**WebSocket Path (Preferred):**
1. MCP server starts WebSocket server on port 9223 (or next available port in range 9223–9232)
2. Plugin UI connects as WebSocket client
3. MCP server sends commands as JSON `{ id, method, params }`
4. Plugin UI routes to the same `window.*` handlers
5. Results sent back as `{ id, result }` or `{ id, error }`

## Troubleshooting

### Plugin doesn't appear in menu
- Make sure Figma Desktop is running (not browser)
- Check that manifest.json path is correct
- Try **Plugins → Development → Refresh plugin list**

### "No plugin UI found with variables data" or "No plugin UI found with requestComponentData"
- Ensure plugin is running (check for open plugin window showing "✓ Desktop Bridge active")
- Try closing and reopening the plugin
- Check browser console for errors (Plugins → Development → Open Console)

### Variables not updating
- Close and reopen the plugin to refresh data
- Use `refreshCache: true` parameter in MCP call
- Check that you're viewing the correct Figma file

### Component descriptions are empty or missing
- **First, verify in Figma:** Check if the component actually has a description set
- If using REST API fallback (not Desktop Bridge), descriptions may be missing due to known Figma API bug
- Ensure the plugin is running - component data requires active plugin connection
- Check that the nodeId is correct (format: "123:456")

### Component request times out
- Ensure plugin is running and shows "Desktop Bridge active"
- Check that the component exists in the current file
- Verify nodeId format is correct
- Timeout is set to 10 seconds - complex files may take longer

### WebSocket connection not working
- Verify the MCP server is running (it starts the WebSocket server on port 9223)
- Check that the plugin is open in Figma — the WebSocket client is in the plugin UI
- Check the browser console (Plugins > Development > Open Console) for `[MCP Bridge] WebSocket connected to port 9223`
- **You usually don't need to restart the plugin.** If you opened it before starting the MCP server, the background watchdog reconnects automatically within ~12s once a server appears; the `Reconnect` button forces it immediately. The status strip and the `N server(s)` badge reflect the live connection state.
- As of v1.10.0, multiple MCP servers can run simultaneously on different ports (9223–9232). If tools aren't working on a fallback port, re-import the plugin manifest to enable multi-port scanning.
- **Custom ports:** the plugin scans ports 9223–9232 automatically. The `FIGMA_WS_PORT` env var sets the preferred starting port. Multi-instance support works out of the box within this range.

### Empty or outdated data
- Plugin fetches variables on load - rerun plugin after making variable changes
- Component data is fetched on-demand - always returns current state
- Cache TTL is 5 minutes for variables - use `refreshCache: true` for immediate updates
- Ensure you're in the correct file (plugin reads current file's data)

### Cloud Mode shows "Connection failed"
- The pairing code may have expired (codes are valid for 5 minutes). Generate a new one from the AI client.
- Verify the code was entered correctly (6 characters, case-sensitive).

### Cloud Mode disconnect button doesn't work
- Close and reopen the Cloud Mode section, then try again.

### Cloud connection drops between AI turns
- Re-pair by generating a new pairing code from the AI client. The relay uses hibernation-safe patterns, but extended idle periods may cause reconnection. If it persists, generate a fresh code.

## Multi-Instance Support (v1.10.0)

The Desktop Bridge plugin supports connecting to **multiple MCP server instances** simultaneously. This is useful when:

- **Claude Desktop** runs both Chat and Code tabs (each spawns a separate MCP server)
- **Multiple CLI terminals** are running different projects with the MCP
- **Claude Desktop + Claude Code CLI** are used together

### How It Works

1. The MCP server tries port 9223 first. If it's taken, it falls back to 9224, 9225, etc. (up to 9232)
2. The plugin scans **all 10 ports** on startup and connects to every active server
3. All events (selection changes, document changes, variables, console logs) are **broadcast to every connected server**
4. Each server instance independently receives real-time data from Figma

### Automatic cleanup of stale servers

When an AI client closes, its MCP server should exit. If a server's shutdown hangs (e.g. a network close blocks), it can be left behind as a **zombie** still holding a port — and enough zombies exhaust the 9223–9232 range, forcing new servers onto fallback ports the plugin then connects to instead of the one your client is using. This is the classic *"server running but no plugin connected — restart the plugin"* symptom.

The server guards against this on two fronts:

- **On shutdown**, a hard backstop forces the process to exit even if graceful close hangs, so zombies aren't created in the first place.
- **On startup and periodically**, each server reaps orphaned instances that hold a port without a live advertisement file — escalating from `SIGTERM` to `SIGKILL` so a process that ignores `SIGTERM` is still cleared.

The `N server(s)` badge in the plugin is a quick health check: if it's higher than the number of AI clients you actually have open, stale instances are present (and will be reaped on the next server launch).

### Important: One-Time Plugin Update

If you imported the Desktop Bridge plugin **before v1.10.0**, you need to re-import the manifest once to enable multi-port scanning:

1. In Figma: **Plugins → Development → Import plugin from manifest...**
2. Select the `manifest.json` file from the `figma-desktop-bridge` directory
3. Run the plugin — it will now scan all ports and connect to all servers

> **Why?** Figma caches plugin files at the application level. Simply restarting the plugin does NOT reload the code from disk. You must re-import the manifest to force Figma to pick up the new multi-port scanning logic.

Without re-importing, the old plugin code only connects to port 9223. If your server fell back to a different port, the plugin won't find it.

## Development

### File Structure
```
figma-desktop-bridge/
├── manifest.json    # Plugin configuration
├── code.js          # Plugin worker (accesses Figma API)
├── ui.html          # Plugin UI (stores/requests data for MCP access)
└── README.md        # This file
```

### Console Logging

The plugin logs to Figma's console:

**Variables (startup):**
```
🌉 [Desktop Bridge] Plugin loaded and ready
🌉 [Desktop Bridge] Fetching variables...
🌉 [Desktop Bridge] Found 404 variables in 2 collections
🌉 [Desktop Bridge] Variables data sent to UI successfully
🌉 [Desktop Bridge] UI iframe now has variables data accessible via window.__figmaVariablesData
```

**Components (on-demand):**
```
🌉 [Desktop Bridge] Fetching component: 279:2861
🌉 [Desktop Bridge] Component data ready. Has description: true
```

**Ready state:**
```
🌉 [Desktop Bridge] Ready to handle component requests
🌉 [Desktop Bridge] Plugin will stay open until manually closed
```

View logs: **Plugins → Development → Open Console** (Cmd+Option+I on Mac)

## Security

**Local Mode:**
- Plugin network access limited to `localhost` only (for WebSocket bridge)
- Data never leaves the local machine
- WebSocket bridge is local-only and unauthenticated — it relies on `localhost` binding for security. Multiple clients may be connected concurrently (one per Figma file). Do not expose the WebSocket port outside `localhost` (e.g., via port forwarding) on untrusted machines

**Cloud Mode:**
- Cloud relay connection uses TLS (`wss://`) for all traffic
- Pairing codes are single-use and expire in 5 minutes — no unauthenticated access is possible
- Each relay Durable Object is scoped per session (one plugin per relay instance)
- The relay passes commands through without storing design data persistently

**Both Modes:**
- Uses standard Figma Plugin API (no unofficial APIs)
- Component requests are scoped to current file only

## Why Desktop Bridge for Components?

Figma's REST API has a known bug where component `description` and `descriptionMarkdown` fields are often missing or outdated. This is particularly problematic for:

- **Local project components** (not published to team libraries)
- **Unpublished components** in active development
- **Team collaboration** where descriptions contain important usage guidelines

The Desktop Bridge plugin bypasses this limitation by using the Figma Plugin API (`figma.getNodeByIdAsync()`), which has reliable, real-time access to all component fields including descriptions. This makes it ideal for teams working with local components in shared project files.

## License

Part of the figma-console-mcp project.
