# figpea-mcp [Health: Active]

**Category:** 📐 Architecture & Design  
**Repository:** https://github.com/figpea/figpea-mcp  
**GitHub Stars:** 0  
**npm Downloads (last month):** 1143  
**Views:** 0  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/figpea-mcp

## Description
MCP server for AI agents to open, inspect, and export PSD, Adobe XD, Figma, SVG & PDF files in a live Figpea editor — runs locally over stdio, files never leave your machine.

## Tools
Capabilities this server exposes over MCP:

- **open_editor** — Opens a Figpea editor tab wired to connect back to this bridge. Returns {port, token, url} -- open `url` in a browser to connect an editor tab; the returned port/token also let a caller compose its own URL.
- **status** — Reports the bridge's port, whether an editor tab is connected, the connected tab's contract version (null if none), and how many contract tools are currently registered. Also returns token and url so an LLM can construct the paste-ready pairing string without re-launching (REQ-1035). Returns a `connection` block naming WHY a tab is not connected — `lastEvent` (no_attempt, transport_only, hello_timeout, hello_rejected, hello_accepted, slot_refused, tab_superseded, disconnected) with the `nextStep` it implies, plus per-process counters and the run's `startedAt`; read it instead of re-trying a pairing blindly. Returns `document` — the active design's `{id, name}`, or null with no tab, with an older tab, or when the tab does not answer — so you can tell WHICH design you are pointed at, and notice that it changed between two calls, without issuing a mutation first. Pair it with the tab's `session.expectDocument` to make a mutation refuse itself against the wrong design. `slot_refused` means a second tab asked for this bridge's single slot and did not get it — the tab already paired is untouched and still serving; open it with --bridge-slots=multi (or FIGPEA_BRIDGE_SLOTS=multi) to pair both at once. Also returns WHICH BUILD is answering: a `build` block (`version`, `buildId`, `builtAt`, `servedAt`, `root`) and a top-level `buildStale` boolean. Read `buildStale` BEFORE spending time on a call that fails in a way the current source would not — your MCP host owns this process and does not restart it when a newer build lands on disk, so a stale process answers with code from before the fix and the failure reads as a bug in the design file. `buildStale: true` means restart the MCP server; when it is false, `build.buildId` is provably the build this process loaded, so match it against the commit you are reading the source of. It says nothing about the connected editor tab's build (REQ-1457). REQ-1492 adds WHICH TAB it is attached to: a `tab` block (`connectionId`, `origin`, `originSource`, `contractVersion`, `pairedAt`) naming the tab this call is answered for, a `connections[]` list of every paired tab with its own id/origin/contract version and `active` flag, `activeConnectionId`, and `bridgeSlots` ('single' or 'multi' — how this bridge serves tabs). Read `tab.origin` and `tab.contractVersion` BEFORE a destructive write: assert you are on the document you expect. `originSource` is 'handshake' when the tab's connection carried an Origin header and 'absent' when it did not, so origin null means the browser declined to send one, never a value this server guessed. REQ-1503 adds a `liveness` block beside `tabConnected`, naming whether the TAB is ANSWERING: `state` (unpaired, unknown, responsive, unresponsive) with the `nextStep` that token implies, plus `connectionId`, `inFlight`, `oldestInFlightMs`, `consecutiveTimeouts`, `lastAnswerAt` and `lastTimeoutAt`. `tabConnected` keeps its own meaning — the socket is open — and cannot see a wedged tab, because a frozen tab holds its socket open forever while every call times out; read `liveness` BEFORE retrying a timed-out call. REQ-1522 names that call's code: a timed-out call comes back as `bridge_timeout_maybe_applied`, meaning the deadline fired and the change MAY have landed (it is not a failure report), and re-issuing an identical non-idempotent call is then REFUSED with `bridge_previous_call_unresolved` rather than applied twice — so the state check the timeout message names is the route forward, not a retry. A `bridge_error` is the different case: the call was never delivered at all. It reports what this bridge OBSERVED, never why: `unresponsive` is two calls in a row going unanswered, or one on a tab that had never answered, and it is NOT proof the tab failed — a large project mid-render and a frozen tab look identical from here, and a timed-out call may still land. `unknown` means nothing has been observed yet, not that the tab is healthy; in-flight age is published as `inFlight`/`oldestInFlightMs` rather than folded into the state, because a slow `session.openFile` runs 120000ms by its own documented default. It describes the tab calls are ADDRESSED TO — read `activeConnectionId` to know which one that is, and `select_tab` to change it. With the MCP channel gone, the same facts are readable over the bridge's own `GET /state` and a call can be relayed over its token-gated `POST /call`, so a run can be finished without this server: the bridge keeps serving without the MCP channel (README → Recovering a lost session). REQ-1516 adds `pairingWarning`: a one-line warning when FIGPEA_EDITOR_URL is unset, naming that the pairing URL therefore targets https://editor.figpea.com (production) — null when the env names an origin explicitly, because that is a choice, not an accident.
- **figpea_skill** — Returns Figpea's agent skill reference -- the craft guidance for using window.figpea well (the authoring loop, recreating a reference faithfully, wiring interactions, the screenshot feedback loop, undo etiquette, entitlement boundaries, and the canonical Tier-1 recipe). Sourced from the editor origin's /agent/skill.md at startup, and the answer NAMES that origin: the body is that origin's own build, so if a tab is paired from a different origin or build, get the tab's skill with figpea.SKILL() in the tab or GET <the tab's origin>/agent/skill.md.
- **figpea_describe** — Returns the agent contract surface for a group or method — the same doc/params/result the editor's own describe() returns, served from the manifest this server already holds in memory (no round trip to the tab). Call it with no arguments for the group index, {group} for one group's methods, or {group, method} for one method's wire shape. In compact mode this is how you learn a method's argument shape instead of probing: e.g. figpea_describe({group:'layer', method:'batch'}) returns the ops shape, whose args is a POSITIONAL array of {method, args} ops. A per-method response also carries stringJsonParams: the names of THIS method's params you may send as a JSON string instead of a real object/array, because the server parses them before the round trip (e.g. ["[{\"method\":\"create\",\"args\":[\"rect\",{\"rwidth\":100}]}]"] for ops). The key is absent when the method declares no such param, and it is derived from the manifest, so it is right for any method without this description being updated. A per-method response also carries wire: this method's parameters as the POSITIONAL slots they occupy — encoding, a callAs line showing e.g. layer.stylePatch(id, patchContents), and each slot's index, name, type, an object slot's legal contents, and the one envelope not to send. Read it because params is the DECLARATION and not the encoding: an object-valued parameter IS its positional slot, so its contents go flat and never inside an envelope keyed by the parameter's name. A group listing states the same thing once for every method it holds, as encodingNote. The wire key is absent when the method declares no positional arguments, and it is derived from the same manifest params is read from, so it is right for any method without this description being updated.
- **figpea_call** — Universal dispatcher — calls any group.method on the paired editor tab via bridge.callTab(group, method, args, _timeoutMs?). In compact mode this is the only way to reach contract methods; in full mode the individual tools are also available. group/method are the describe() surface names, and args is the POSITIONAL argument array for that method, in that method's own parameter order. FLAT example: ["rect", {rwidth:100}] for layer.create. NESTED example — when a parameter is itself an array (e.g. layer.batch's ops), that parameter is passed as ONE element of args, so the element is an array of {method, args} ops: {"group":"layer","method":"batch","args":[[{"method":"create","args":["page",{"name":"probe","pageWidth":100,"pageHeight":100}]}]]}. Each op's own args is likewise a positional ARRAY, never an object. Unsure of a method's shape? Call figpea_describe({group, method}) first — it returns that method's doc and params from the manifest with no round trip to the tab. Image results return MCP image content + a text summary. Pass returnAs:"path" to receive a binary result off-band as a session file path instead of inline base64 — it reaches every binary export, e.g. canvas_screenshot / export_layer / export_artboard for images and export_project for a native .fp. The default can be too low during a burst of mutations, where the editor is still settling after the relay has already given up — pass _timeoutMs deliberately (90000 is a legal value) rather than discovering the limit by timing out. Clamped to 120000, not rejected. If a call does time out you get code bridge_timeout_maybe_applied — the deadline fired and the change may have landed, so it is not a failure report — and an identical re-issue of a non-idempotent call is refused with bridge_previous_call_unresolved rather than applied twice. Run the state check the message names. Every call carries one budget: the TOTAL serialized size of args is capped (currently ~22000 characters — read the live value at describe().limits.argsChars, or figpea_describe({selector:"limits"}), and do not hardcode it). This applies to EVERY call, whatever the payload — not only image data URIs. A dense layer.batch of shape and text ops reaches the cap at well under 90 ops, and image bytes reach it far sooner. Over the cap the call is refused with arg_size_exceeded and NOTHING is applied, so there is no partial result to recover from. Chunk instead: aim for roughly two-thirds of the limit per call and split the op list across calls of that size, passing ids from earlier results literally. Each chunk is its own undo step. The limit is measured in characters (UTF-16 code units of JSON.stringify(args)), not bytes, so a payload carrying emoji or other multi-unit characters reaches the transport cap earlier than the number suggests — budget lower for those. returnAs is a sibling of group/method/args on figpea_call itself — never a member of args. The natural misspelling args:[<id>, {format:"png", returnAs:"path"}] is refused with invalid_params naming the key and its position (args[1].returnAs), before the call reaches the editor and without spending a tab round trip: a "returnAs" (or "_rawJson") nested inside args is never read from inside args. Pass it as a sibling instead: {"group":"export","method":"artboard","args":[ …, …],"returnAs":"path"}.

## Claude Desktop Quick Installation
Install path detected from listing signals. Uses `npx` (confidence: high):

```json
"mcpServers": {
  "figpea-mcp": {
    "command": "npx",
    "args": ["-y","figpea-mcp"]
  }
}
```

## Documentation

## What figpea-mcp MCP server does

The figpea-mcp MCP server connects an AI agent to a live Figpea editor running in a browser. It supports viewing, inspecting, and exporting PSD, Adobe XD, Figma, SVG, and PDF files. The bridge runs on the local machine, and the documented flow keeps design files from leaving that machine.

The server exposes a small fixed set of MCP tools plus a universal dispatcher for the editor's contract surface:

- `open_editor` returns a port, token, and pairing URL.
- `status` reports bridge details, pairing state, connected-tab metadata, and available connection slots.
- `figpea_skill` returns the agent guidance fetched from the paired editor origin, and can work without a paired tab.
- `figpea_describe` exposes documentation for editor groups and methods.
- `figpea_call` invokes a selected editor method with its group, method name, and arguments.
- `select_tab` switches the target tab when multi-slot mode is enabled.

## How it works

At startup, the bridge binds to loopback at `127.0.0.1` and creates a per-run pairing token. The server prints a pairing URL to stderr. Opening that URL in a browser causes the Figpea editor tab to connect back to the bridge over a local WebSocket. An agent can also call `open_editor` to obtain the URL programmatically.

The default bridge accepts one editor tab. If a contract call arrives before pairing, the response includes a `no_tab` error and a pairing URL. Connection status includes details such as the last connection event, the tab's contract version, and the observed origin.

For multiple tabs, start the bridge with `--bridge-slots=multi` or set `FIGPEA_BRIDGE_SLOTS=multi`. Each tab receives a separate connection ID, and `select_tab` determines which tab receives later calls. The tabs represent separate documents; this mode is not collaborative editing.

## Setup and configuration

A standard stdio configuration starts the package with `npx`:

```json
{
  "mcpServers": {
    "figpea": {
      "command": "npx",
      "args": ["-y", "figpea-mcp"]
    }
  }
}
```

The `-y` option suppresses the installation prompt for a non-interactive client process. After startup, open the printed pairing URL, or ask the agent to call `open_editor`.

The optional `FIGPEA_EDITOR_URL` variable changes the editor origin, which is useful for pointing at a local development editor. The `--port=<n>` option selects a port in the applicable mode. Running `npx figpea-mcp --http` provides a second MCP entry point over a loopback HTTP endpoint; it uses the same tools and requires the per-run token in the `x-figpea-token` header.

## Tools and capabilities

`figpea_call` is the main execution interface. It accepts an editor group, method, and argument list, and can also read a JSON file for a method's top-level array argument through `_opsFile`. `figpea_describe` can return the complete group index, one group's methods, or a single method's parameters and result documentation.

`status` identifies the connected tab and lists available connections. In multi-slot mode, `select_tab({connectionId})` directs later calls to a chosen connection. The `figpea_skill` response identifies the editor-origin URL from which its skill reference was loaded.

## Limitations and notes

The figpea-mcp MCP server is local-only in its documented bridge setup: the bridge binds to loopback and does not widen its listener to a public interface. A token belongs to one server run, so an older pairing URL becomes stale after restarting the server.

The default configuration permits one tab and refuses a second connection without displacing the existing tab. Multi-slot mode must be enabled before `select_tab` is registered. HTTP mode is an alternate transport to the same server surface, not an additional set of editor capabilities.

_Full upstream README: https://allmcps.com/mcp/figpea-mcp/readme_

