figma-prototype-mcp 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 [project author]
4
+
5
+ Portions of this software (src/socket.ts) are derived from
6
+ cursor-talk-to-figma-mcp (https://github.com/grab/cursor-talk-to-figma-mcp),
7
+ Copyright (c) 2024 Grab Holdings Inc., MIT License.
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # figma-prototype-mcp
2
+
3
+ Local MCP server that lets Claude (or any MCP client) create real Figma prototype interactions โ€” On click โ†’ Navigate to a frame, or Scroll To a node โ€” from natural language prompts.
4
+
5
+ Why this exists: the official Figma MCP doesn't expose a write API for prototype reactions. This project fills that gap with a Figma plugin + WebSocket bridge. It's designed to run **alongside the official Figma MCP** โ€” that one *creates* screens, this one *wires* them into a working prototype.
6
+
7
+ > ๐ŸŽจ Designers: see the plain-language **[prototype-wiring cheat-sheet](docs/prototype-wiring-for-designers.md)** ("say this โ†’ get that").
8
+
9
+ ## Architecture
10
+
11
+ ```
12
+ MCP client (Claude) <-- SSE/HTTP --> unified server (Express) <-- ws --> Figma plugin
13
+ ```
14
+
15
+ Since v0.18.0 the MCP server, the WebSocket relay, and the HTTP layer are a **single Express process** on one port (default 3000) โ€” `/sse` for the MCP client, `/ws` for the plugin. The earlier stdio-MCP + standalone-relay split was removed.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install
21
+ npm run build
22
+ ```
23
+
24
+ **Or from npm (no clone):**
25
+
26
+ ```bash
27
+ npx figma-prototype-mcp # starts the server on :3000
28
+ ```
29
+
30
+ The server prints the path to the bundled Figma plugin manifest on startup (under `node_modules/figma-prototype-mcp/dist/figma-plugin/manifest.json`) โ€” import it in Figma via **Plugins โ†’ Development โ†’ Import plugin from manifestโ€ฆ**.
31
+
32
+ ## Run (one process)
33
+
34
+ Phase A (v0.18.0+) ships a single unified server: Express + MCP SSE + Figma plugin WebSocket on the same port.
35
+
36
+ **1. Start the server** (one terminal, leave it running):
37
+
38
+ ```bash
39
+ npm start
40
+ # [server] listening on http://localhost:3000
41
+ # [server] MCP SSE endpoint: GET /sse
42
+ # [server] Plugin WebSocket: ws://localhost:3000/ws
43
+ ```
44
+
45
+ The server is designed to run 24/7. Wrap with PM2/systemd if you want it to auto-restart.
46
+
47
+ `PORT` can be overridden: `PORT=4000 npm start`. **Note:** the Figma plugin connects to `ws://localhost:3000` (hard-coded in its manifest's `networkAccess.allowedDomains`), so if you change the port you must also update `src/figma-plugin/manifest.json` and rebuild (`npm run build`) for the plugin to reach the server.
48
+
49
+ Requires **Node โ‰ฅ 18**.
50
+
51
+ **2. Figma plugin**:
52
+
53
+ - Open Figma desktop app.
54
+ - Plugins โ†’ Development โ†’ Import plugin from manifest...
55
+ - Choose `dist/figma-plugin/manifest.json`.
56
+ - Run the plugin. It auto-connects to `ws://localhost:3000/ws` (single-active session โ€” only one plugin at a time, latest connection wins). Click **Connect** if it doesn't auto-connect on launch.
57
+
58
+ **3. MCP client** (Claude Desktop or Claude Code):
59
+
60
+ Configure your client to connect to the SSE endpoint:
61
+
62
+ ```json
63
+ {
64
+ "mcpServers": {
65
+ "figma-prototype": {
66
+ "url": "http://localhost:3000/sse"
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ No `command` / `args` / env vars needed โ€” the URL is enough.
73
+
74
+ Connecting a new MCP client automatically replaces any previous one (single-active, newest-wins โ€” symmetric with the plugin side). A stale/backgrounded client never blocks a fresh connection; no need to kill it first.
75
+
76
+ > **Keep a single MCP client per server.** Newest-wins is built for *replacing* a dead/stale connection (e.g. a client reconnecting), not for running two clients at once. If a second live client connects, the first is evicted: its next call fast-fails with HTTP 400 "unknown session" and it should reconnect. A well-behaved client surfaces this immediately, but a stdioโ†”SSE bridge such as **supergateway** may not propagate the eviction to its stdio side, so the client appears to hang until its own timeout (then may silently fall back to another tool). The server logs `a second MCP client connected โ€” evicted the prior SSE connection` when this happens. Practical rule: when driving live validation through Claude Desktop, don't point an ad-hoc SSE probe at the same server mid-session.
77
+
78
+ ## Your first wire
79
+
80
+ A 60-second end-to-end check once the server is running, the plugin is connected ("Connected" in the plugin UI), and your MCP client points at `http://localhost:3000/sse`:
81
+
82
+ 1. **Open a Figma file with at least two frames** on the current page โ€” say `Home` and `Detail` โ€” and a button (or any node) inside `Home`.
83
+ 2. **Ask your MCP client** (Claude) in plain language:
84
+ > "Home์˜ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด Detail ํ™”๋ฉด์œผ๋กœ ๊ฐ€๊ฒŒ ํ•ด์ค˜"
85
+ > *(or "When the button on Home is clicked, navigate to the Detail screen")*
86
+ 3. Claude calls `get_canvas_overview` / `find_nodes` to resolve the nodes, then `proto_wire` to create the reaction. You'll get back a success count.
87
+ 4. **Verify in Figma**: select the button โ†’ **Prototype** tab shows **On click โ†’ Navigate to โ†’ Detail**. Hit **โ–ถ Present** to try it.
88
+
89
+ That's the loop: *describe the interaction โ†’ it's wired in Figma.* From here, "๋’ค๋กœ๊ฐ€๊ธฐ ๋‹ฌ์•„์ค˜", "์ด ํ† ๊ธ€ ์ผœ์ง„ ์ƒํƒœ๋กœ ๋ฐ”๊ฟ”", "๋กœ๊ทธ์ธํ•˜๋ฉด home, ์•„๋‹ˆ๋ฉด login์œผ๋กœ" all map to the tools below. To see what's already wired on a page, ask for the prototype flow (`get_prototype_flow`).
90
+
91
+ ## High-level tools (recommended)
92
+
93
+ Ten intent-oriented `proto_*` tools (9 writers + 1 history reader) that wrap `create_reactions` with named motion presets โ€” covering navigate / change-to (component variant) / scroll / overlay / back / url / set & toggle variable / conditional (incl. one-level AND/OR compound). The lower-level tools below remain the escape hatch for multi-action conditional branches, directional transitions (`MOVE_IN` / `PUSH` / `SLIDE_*`), advanced triggers (`ON_DRAG`, `MOUSE_*`, `ON_KEY_DOWN`, media), reading the existing interaction graph, and any case the high-level surface doesn't cover.
94
+
95
+ | Tool | Purpose |
96
+ |---|---|
97
+ | `proto_wire` | Wire source nodes to destination frames with **Navigate To**. Batch input `{ wires: [{ from, to, trigger?, motion?, resetScrollPosition? }], replaceExisting? }`. Defaults: `trigger=ON_CLICK`, `motion=M3_EMPHASIZED`. |
98
+ | `proto_change_to` | Switch a component **instance** to a sibling **variant** (Figma's **Change To**) โ€” a one-shot switch to a specific state (tabs, segmented controls, selected/highlight). Batch input `{ changes: [{ from, to, trigger?, motion? }], replaceExisting? }`; `from`=instance node id, `to`=target variant component id (NOT the current variant). For a repeating on/off flip use `proto_toggle_variable` on a BOOLEAN. |
99
+ | `proto_overlay` | Open / swap / close overlays. Batch input `{ overlays: [{ mode: "open"\|"swap"\|"close", from, overlay?, trigger?, motion? }] }` โ€” `overlay` is required for `open`/`swap`, forbidden for `close`. **Note:** Figma's runtime does not accept Smart Animate on overlay/swap/close navigations (the UI hides it too); when a SMART_ANIMATE-based motion preset is supplied, the compile step substitutes `DISSOLVE` while preserving `duration` and `easing` so the M3/HIG feel survives. |
100
+ | `proto_scroll` | Wire source nodes to scroll targets (**Scroll To**). Batch input `{ scrolls: [{ from, to, trigger?, motion?, resetScrollPosition? }] }`. |
101
+ | `proto_back` | Wire source nodes to the **Back** navigation action (pops the prototype history stack). Batch input `{ backs: [{ from, trigger?, motion? }], replaceExisting? }`. Defaults: `trigger=ON_CLICK`, `motion=M3_EMPHASIZED`. |
102
+ | `proto_url` | Wire source nodes to the **Open URL** action. Batch input `{ urls: [{ from, url, openInNewTab?, trigger? }], replaceExisting? }`. No `motion` field โ€” URL is a terminal event; the reaction's transition defaults to INSTANT. |
103
+ | `proto_set_variable` | Wire source nodes to the **Set Variable** action โ€” clicking the source assigns a literal value to a local Figma variable (resolved by name). Batch input `{ sets: [{ from, variable, value, trigger? }], replaceExisting? }`. `value`: boolean / number / string; for COLOR variables, pass a hex string (`"#RRGGBB"` or `"#RRGGBBAA"`). No `motion` field โ€” variable changes are instant. |
104
+ | `proto_toggle_variable` | Wire source nodes to the **Toggle Variable** action โ€” clicking the source flips a local BOOLEAN variable. Batch input `{ toggles: [{ from, variable, trigger? }], replaceExisting? }`. Variable must be BOOLEAN; non-boolean throws at runtime. No `motion` field. |
105
+ | `proto_conditional` | Wire a **conditional reaction** (if/then/else) on a source node based on a variable comparison. Batch input `{ conditions: [{ from, if, then, else?, trigger?, motion? }], replaceExisting? }`. `if` is a single comparison `{ variable, operator?, value }` OR a one-level compound `{ all: [...] }` (AND) / `{ any: [...] }` (OR) over โ‰ฅ2 comparisons. `if.operator` defaults to `"=="`. `then`/`else` each take ONE branch sugar entry (single-action). Branch keys: `navigate` / `scroll` / `overlay` / `swap` / `close` / `back` / `url` / `set`. For multi-action branches, use low-level `create_reactions`. Overlay/swap branches: SMART_ANIMATE auto-rewrites to DISSOLVE. |
106
+ | `proto_get_last_history` | Read the in-memory history of recent `proto_*` calls (FIFO ring buffer, capacity 10, server-lifetime). Input `{ count?: 1..10 }`, default 1. Returns `{ entries: HistoryEntry[] }` with entries in oldest-to-newest order. Use to support "modify the last one I made"-style requests by recovering source/target IDs and motion preset, then re-calling with `replaceExisting: true`. |
107
+
108
+ ### History stack
109
+
110
+ The server keeps an in-memory record of every successful **mutating** `proto_*` call (`proto_wire` / `proto_change_to` / `proto_overlay` / `proto_scroll` / `proto_back` / `proto_url` / `proto_set_variable` / `proto_toggle_variable` / `proto_conditional`) โ€” `historyId` (UUID), `timestamp`, `tool` name, full parsed `input`, and `result` counts โ€” up to 10 entries (FIFO ring buffer, cleared on server restart). `proto_get_last_history` exposes this so an LLM can resolve natural-language references like "the last thing I made" / "๋ฐฉ๊ธˆ ๋งŒ๋“  ๊ฑฐ" without the human re-stating nodeIds. Low-level tools (`create_reactions`, `set_frame_scroll`, etc.) are NOT recorded โ€” only the 9 mutating `proto_*` entry-points (`proto_get_last_history` itself is read-only and not recorded).
111
+
112
+ ### Motion presets
113
+
114
+ `motion` accepts a preset name (string) or a full `TransitionInput` object. The 10 presets cover the common design-system tones:
115
+
116
+ | Preset | Compiled transition |
117
+ |---|---|
118
+ | `M3_EMPHASIZED` *(default)* | SMART_ANIMATE, 500ms, cubic-bezier(0.2, 0, 0, 1) |
119
+ | `M3_EMPHASIZED_DECELERATE` | SMART_ANIMATE, 400ms, cubic-bezier(0.05, 0.7, 0.1, 1) |
120
+ | `M3_EMPHASIZED_ACCELERATE` | SMART_ANIMATE, 200ms, cubic-bezier(0.3, 0, 0.8, 0.15) |
121
+ | `M3_STANDARD` | SMART_ANIMATE, 300ms, cubic-bezier(0.2, 0, 0, 1) |
122
+ | `M3_STANDARD_DECELERATE` | SMART_ANIMATE, 250ms, cubic-bezier(0, 0, 0, 1) |
123
+ | `M3_STANDARD_ACCELERATE` | SMART_ANIMATE, 200ms, cubic-bezier(0.3, 0, 1, 1) |
124
+ | `HIG_DEFAULT` | SMART_ANIMATE, named spring GENTLE |
125
+ | `HIG_SMOOTH` | SMART_ANIMATE, named spring SLOW |
126
+ | `HIG_SNAPPY` | SMART_ANIMATE, named spring QUICK |
127
+ | `HIG_BOUNCY` | SMART_ANIMATE, named spring BOUNCY |
128
+
129
+ For `proto_overlay`, the `SMART_ANIMATE` type is rewritten to `DISSOLVE` at compile time per the Figma constraint noted above.
130
+
131
+ To bypass the preset system (e.g. for `MOVE_IN`/`PUSH`/`SLIDE_*` directional transitions or fully custom timing), pass `motion` as a raw `TransitionInput` object instead of a preset name.
132
+
133
+ ## Tools (low-level escape hatch)
134
+
135
+ | Tool | Purpose |
136
+ |---|---|
137
+ | `get_canvas_overview` | One-shot context primer: current page, frames, selection |
138
+ | `find_nodes` | Search nodes by name (and optional type) |
139
+ | `list_variables` | List Figma variables usable by name in set/toggle/conditional tools (local + library/remote; `resolvedType` filter optional) |
140
+ | `create_reactions` | **Write**: batch create prototype reactions. Each connection's `action` picks between Navigate To (action.type=navigate, targetFrameId), Scroll To (scroll, targetNodeId), Open Overlay (overlay, targetFrameId), Close Overlay (close, no destination), Back (back, no destination), Open URL (url, url, openInNewTab?), Swap Overlay (swap_overlay, targetFrameId), and Change To (change_to, targetVariantId โ€” switch a component instance to a sibling variant). Triggers: string shortcuts `ON_CLICK` (default) / `ON_HOVER` / `ON_PRESS` / `AFTER_TIMEOUT` (with top-level `afterTimeoutSeconds`); object form additionally supports `{type:"ON_DRAG"}`, `{type:"MOUSE_UP"\|"MOUSE_DOWN", delay?}`, `{type:"MOUSE_ENTER"\|"MOUSE_LEAVE", delay?, deprecatedVersion?}`, `{type:"ON_KEY_DOWN", device, keyCodes}`, `{type:"ON_MEDIA_HIT", mediaHitTime}`, `{type:"ON_MEDIA_END"}`, and a self-contained `{type:"AFTER_TIMEOUT", timeout}`. Transitions: string shortcuts `INSTANT` / `DISSOLVE` / `SMART_ANIMATE`, simple object form (DISSOLVE/SMART_ANIMATE/SCROLL_ANIMATE + duration + easing), and directional form (`MOVE_IN`/`MOVE_OUT`/`PUSH`/`SLIDE_IN`/`SLIDE_OUT` ร— `direction` LEFT/RIGHT/TOP/BOTTOM ร— optional `matchLayers`). NODE actions (navigate / scroll / overlay / swap_overlay) also accept optional `resetScrollPosition?: boolean` โ€” `false` to keep the destination frame's previous scroll position, `true` to reset to top. Omit to use Figma's runtime default. Each succeeds or fails independently; scroll targets without a scrollable ancestor return a `warning`. A `conditional` action wraps an IF/ELSE: `{ type: "conditional", condition, then: [action, ...], else?: [action, ...] }` where `condition` is a single comparison `{ variable, operator: "==" \| "!=" \| "<" \| "<=" \| ">" \| ">=", value }` or a one-level compound `{ all: [comparison, ...] }` (AND) / `{ any: [comparison, ...] }` (OR) over โ‰ฅ2 comparisons. The `variable` is the name of a local Figma variable (BOOLEAN/FLOAT/STRING); plugin resolves to id. Nested conditionals are rejected. Branches use any of the 7 non-conditional action types. Variable mutations: `set_variable` action assigns a literal (`{ type: "set_variable", variable, value }`; value is boolean/number/string matching the variable's resolvedType; valid both at top-level and inside conditional then/else); `toggle_variable` action flips a BOOLEAN variable (`{ type: "toggle_variable", variable }`; top-level only โ€” desugars to CONDITIONAL+2 SET_VARIABLE; nested-rejected to preserve the no-nesting rule). Both reference local Figma variables by name. `list_reactions` round-trips toggle_variable via pattern detection on the stored CONDITIONAL. COLOR variables accept hex string values (`"#RRGGBB"` or `"#RRGGBBAA"` โ€” case insensitive); the plugin validates format and parses to Figma's RGB(A) shape internally. `list_reactions` echoes COLOR `value` back as a hex string. Conditional comparison against COLOR variables is rejected (use BOOLEAN/FLOAT/STRING for conditions). |
141
+ | `list_reactions` | Inspect existing reactions on a node |
142
+ | `get_prototype_flow` | **Read** the whole prototype interaction graph of a page in one call: frames (with `isStartFrame`) + every wired interaction (`frameId`, `sourceNodeId`, `trigger`, decoded `action` โ€” same shape as `list_reactions`). Page-scoped (optional `pageId`); `limit` caps results. Use to see what is already wired before adding more. |
143
+ | `clear_reactions` | Remove reactions from one or more nodes |
144
+ | `set_frame_scroll` | **Write**: configure scroll-related properties on one or more FRAME nodes. Each entry accepts optional `direction` (`NONE` / `HORIZONTAL` / `VERTICAL` / `BOTH`) and/or optional `fixedChildren` (number of top-most children to fix when scrolling โ€” Figma's sticky-header model fixes the first N children in z-order; layer panel order matters). At least one of `direction` or `fixedChildren` must be provided per entry. Each frame succeeds or fails independently; response includes `applied` array naming which fields were set. |
145
+
146
+ ## Troubleshooting
147
+
148
+ | Symptom | Cause / fix |
149
+ |---|---|
150
+ | A tool returns `ํ”ผ๊ทธ๋งˆ ํ”Œ๋Ÿฌ๊ทธ์ธ ์—ฐ๊ฒฐ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”` (check the plugin connection) | The plugin isn't connected. Make sure the server is running, the plugin is open in Figma, and its UI shows **Connected** (click **Connect** if not). The server waits ~3s for the plugin before returning this. |
151
+ | Plugin UI won't connect / keeps retrying | The server must be running first (`npm start`) and reachable at `ws://localhost:3000`. The port is **hard-coded in the plugin manifest** โ€” if you ran on a non-default `PORT`, update `src/figma-plugin/manifest.json` and `npm run build`, then reload the plugin. |
152
+ | Server won't start: `EADDRINUSE :3000` | Another process holds port 3000. Stop it, or run on another port (`PORT=4000 npm start`) โ€” and update the plugin manifest as above. |
153
+ | MCP client shows no tools | Confirm the client is configured with `{"url": "http://localhost:3000/sse"}` and the server is up. Re-open the connection after starting the server. |
154
+ | A tool call hangs, then the client falls back to another tool | A **second MCP client** connected and evicted the first (single-active, newest-wins). Keep one client per server; reconnect the one you want to use. A stdioโ†”SSE bridge (e.g. supergateway) may not surface the eviction โ€” the server logs `a second MCP client connected โ€” evicted the prior SSE connection`. |
155
+ | `get_canvas_overview` shows `frames: []` but the page clearly has frames | `get_canvas_overview` lists only **top-level** frames, so frames nested inside a **Section** don't appear. `get_prototype_flow` lists frames recursively (Sections included) and is the better read for a populated page; pass `pageId` if you're not on the intended page. |
156
+ | Cryptic crash on startup (syntax / module errors) | Check your Node version โ€” this needs **Node โ‰ฅ 18** (`node -v`). |
157
+
158
+ ## Known limitations
159
+
160
+ - Reaction actions: **Navigate To**, **Scroll To**, **Open Overlay**, **Close Overlay**, **Back**, **Open URL**, **Swap Overlay**, **Change To** (component variant switch), **Set Variable** (boolean / number / string / COLOR-via-hex), **Toggle Variable** (BOOLEAN), **Conditional** (single comparison, or a one-level AND/OR compound over โ‰ฅ2 comparisons, IF/ELSE). Not supported: NOT/negation, mixing AND with OR, nested compound conditions, nested conditionals, else-if chains, media-runtime triggers.
161
+ - **Conditional is single-level IF/ELSE only โ€” no `else-if` chains.** Figma's prototype conditional has no "Else if" in the product UI, and the plugin API silently collapses a multi-block conditional to a single if/else on write (verified 2026-06-01). Express multi-way branching with separate reactions/variables instead.
162
+ - Default transition is **Instant**. Smart Animate is available as an option but requires matching layer designs.
163
+ - **Figma desktop/web app must be open and the plugin running** โ€” no headless execution.
164
+ - Single-page scope (cross-page navigation untested).
165
+ - MCP server and plugin run on **localhost** (no remote).
166
+
167
+ ## License
168
+
169
+ MIT. Includes code derived from [grab/cursor-talk-to-figma-mcp](https://github.com/grab/cursor-talk-to-figma-mcp) (MIT) โ€” see `LICENSE`.