figma-prototype-mcp 0.30.0 β 0.30.2
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/README.md +29 -3
- package/dist/figma-plugin/code.js +13 -13
- package/dist/figma-plugin/manifest.json +3 -2
- package/dist/server/index.js +43 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,33 @@ Why this exists: the official Figma MCP doesn't expose a write API for prototype
|
|
|
6
6
|
|
|
7
7
|
> π¨ Designers: see the plain-language **[prototype-wiring cheat-sheet](docs/prototype-wiring-for-designers.md)** ("say this β get that").
|
|
8
8
|
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
You connect three local pieces: a **server**, the **Figma plugin**, and your **AI client**. ~3 minutes.
|
|
12
|
+
|
|
13
|
+
**Prerequisites:** [Node 18+](https://nodejs.org) Β· the Figma **desktop app** Β· an MCP client ([Claude Desktop](https://claude.ai/download) or Claude Code).
|
|
14
|
+
|
|
15
|
+
**1. Start the server** (terminal β leave it running):
|
|
16
|
+
```bash
|
|
17
|
+
npx figma-prototype-mcp
|
|
18
|
+
# β [server] listening on http://localhost:3000
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**2. Install & run the plugin:**
|
|
22
|
+
- Install from Figma Community: **[Prototype MCP β wire prototypes with LLM](https://www.figma.com/community/plugin/1647184714488719280/prototype-mcp-wire-prototypes-with-llm)**
|
|
23
|
+
- Open a Figma **design file** β run the plugin (Plugins β Prototype MCP) β it should show **Connected** (needs step 1 running).
|
|
24
|
+
|
|
25
|
+
**3. Point your AI client at the server** β add this to your MCP client config, then restart it:
|
|
26
|
+
```json
|
|
27
|
+
{ "mcpServers": { "figma-prototype": { "url": "http://localhost:3000/sse" } } }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**4. Wire it by talking.** In a file with β₯2 frames, ask Claude:
|
|
31
|
+
> "Homeμ λ²νΌμ λλ₯΄λ©΄ Detail νλ©΄μΌλ‘ κ°κ² ν΄μ€"
|
|
32
|
+
> *(or "when the button on Home is clicked, navigate to Detail")*
|
|
33
|
+
|
|
34
|
+
The interaction appears in Figma's **Prototype** tab. That's the loop β describe it, it's wired. (Stuck? see [Troubleshooting](#troubleshooting).)
|
|
35
|
+
|
|
9
36
|
## Architecture
|
|
10
37
|
|
|
11
38
|
```
|
|
@@ -50,9 +77,8 @@ Requires **Node β₯ 18**.
|
|
|
50
77
|
|
|
51
78
|
**2. Figma plugin**:
|
|
52
79
|
|
|
53
|
-
-
|
|
54
|
-
- Plugins β Development β Import plugin from manifest
|
|
55
|
-
- Choose `dist/figma-plugin/manifest.json`.
|
|
80
|
+
- **Easiest β install from Figma Community:** [Prototype MCP β wire prototypes with LLM](https://www.figma.com/community/plugin/1647184714488719280/prototype-mcp-wire-prototypes-with-llm) β **Open inβ¦** / **Run**.
|
|
81
|
+
- **Or load locally (for development):** Figma desktop β Plugins β Development β Import plugin from manifestβ¦ β choose `dist/figma-plugin/manifest.json` (after `npm run build`).
|
|
56
82
|
- 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
83
|
|
|
58
84
|
**3. MCP client** (Claude Desktop or Claude Code):
|
|
@@ -498,7 +498,7 @@
|
|
|
498
498
|
return { type: "set_variable", variable: varName != null ? varName : `<id:${action.variableId}>`, value };
|
|
499
499
|
}
|
|
500
500
|
const destId = action.destinationId;
|
|
501
|
-
const destName = destId ? resolvers.nodeName(destId) : void 0;
|
|
501
|
+
const destName = destId ? await resolvers.nodeName(destId) : void 0;
|
|
502
502
|
return {
|
|
503
503
|
type: (_c = action.type) != null ? _c : "UNKNOWN",
|
|
504
504
|
navigation: action.navigation,
|
|
@@ -623,14 +623,14 @@
|
|
|
623
623
|
return figma.currentPage;
|
|
624
624
|
}
|
|
625
625
|
await figma.loadAllPagesAsync();
|
|
626
|
-
const page = figma.
|
|
626
|
+
const page = await figma.getNodeByIdAsync(pageId);
|
|
627
627
|
if (!page || page.type !== "PAGE") throw new Error(`Page not found: ${pageId}`);
|
|
628
628
|
return page;
|
|
629
629
|
}
|
|
630
630
|
async function buildNonConditionalAction(action, trigger, afterTimeoutSeconds, transition, sourceNode, degradeTo) {
|
|
631
631
|
var _a;
|
|
632
632
|
if (action.type === "navigate") {
|
|
633
|
-
const target = figma.
|
|
633
|
+
const target = await figma.getNodeByIdAsync(action.targetFrameId);
|
|
634
634
|
if (!target) throw new Error(`Target frame not found: ${action.targetFrameId}`);
|
|
635
635
|
if (target.type !== "FRAME") {
|
|
636
636
|
throw new Error(`Target must be a frame: ${action.targetFrameId} (got ${target.type})`);
|
|
@@ -651,7 +651,7 @@
|
|
|
651
651
|
return { built: reaction.actions[0], warning };
|
|
652
652
|
}
|
|
653
653
|
if (action.type === "scroll") {
|
|
654
|
-
const target = figma.
|
|
654
|
+
const target = await figma.getNodeByIdAsync(action.targetNodeId);
|
|
655
655
|
if (!target) throw new Error(`Scroll target node not found: ${action.targetNodeId}`);
|
|
656
656
|
const scrollable = findScrollableAncestor(target);
|
|
657
657
|
let warning;
|
|
@@ -668,7 +668,7 @@
|
|
|
668
668
|
return { built: reaction.actions[0], warning };
|
|
669
669
|
}
|
|
670
670
|
if (action.type === "overlay") {
|
|
671
|
-
const target = figma.
|
|
671
|
+
const target = await figma.getNodeByIdAsync(action.targetFrameId);
|
|
672
672
|
if (!target) throw new Error(`Overlay target frame not found: ${action.targetFrameId}`);
|
|
673
673
|
if (target.type !== "FRAME") {
|
|
674
674
|
throw new Error(`Overlay target must be a frame: ${action.targetFrameId} (got ${target.type})`);
|
|
@@ -710,7 +710,7 @@
|
|
|
710
710
|
return { built, warning };
|
|
711
711
|
}
|
|
712
712
|
if (action.type === "swap_overlay") {
|
|
713
|
-
const target = figma.
|
|
713
|
+
const target = await figma.getNodeByIdAsync(action.targetFrameId);
|
|
714
714
|
if (!target) throw new Error(`Swap overlay target frame not found: ${action.targetFrameId}`);
|
|
715
715
|
if (target.type !== "FRAME") {
|
|
716
716
|
throw new Error(`Swap overlay target must be a frame: ${action.targetFrameId} (got ${target.type})`);
|
|
@@ -725,7 +725,7 @@
|
|
|
725
725
|
return { built: reaction.actions[0] };
|
|
726
726
|
}
|
|
727
727
|
if (action.type === "change_to") {
|
|
728
|
-
const target = figma.
|
|
728
|
+
const target = await figma.getNodeByIdAsync(action.targetVariantId);
|
|
729
729
|
if (!target) throw new Error(`Change-to target not found: ${action.targetVariantId}`);
|
|
730
730
|
if (target.type !== "COMPONENT") {
|
|
731
731
|
throw new Error(`Change-to target must be a component variant: ${action.targetVariantId} (got ${target.type})`);
|
|
@@ -993,7 +993,7 @@
|
|
|
993
993
|
let warningCount = 0;
|
|
994
994
|
for (const conn of params.connections) {
|
|
995
995
|
try {
|
|
996
|
-
const source = figma.
|
|
996
|
+
const source = await figma.getNodeByIdAsync(conn.sourceNodeId);
|
|
997
997
|
if (!source) throw new Error(`Source node not found: ${conn.sourceNodeId}`);
|
|
998
998
|
if (!("setReactionsAsync" in source) || typeof source.setReactionsAsync !== "function") {
|
|
999
999
|
throw new Error(`Node cannot have reactions: ${source.name} (type: ${source.type})`);
|
|
@@ -1115,15 +1115,15 @@
|
|
|
1115
1115
|
return void 0;
|
|
1116
1116
|
}
|
|
1117
1117
|
},
|
|
1118
|
-
nodeName: (id) => {
|
|
1118
|
+
nodeName: async (id) => {
|
|
1119
1119
|
var _a, _b;
|
|
1120
|
-
return (_b = (_a = figma.
|
|
1120
|
+
return (_b = (_a = await figma.getNodeByIdAsync(id)) == null ? void 0 : _a.name) != null ? _b : void 0;
|
|
1121
1121
|
}
|
|
1122
1122
|
};
|
|
1123
1123
|
async function handleListReactions(params) {
|
|
1124
1124
|
var _a;
|
|
1125
1125
|
await figma.loadAllPagesAsync();
|
|
1126
|
-
const node = figma.
|
|
1126
|
+
const node = await figma.getNodeByIdAsync(params.nodeId);
|
|
1127
1127
|
if (!node) throw new Error(`Node not found: ${params.nodeId}`);
|
|
1128
1128
|
if (!("reactions" in node)) throw new Error(`Node has no reactions field: ${node.name}`);
|
|
1129
1129
|
const reactions = (_a = node.reactions) != null ? _a : [];
|
|
@@ -1147,7 +1147,7 @@
|
|
|
1147
1147
|
const results = [];
|
|
1148
1148
|
for (const nodeId of params.nodeIds) {
|
|
1149
1149
|
try {
|
|
1150
|
-
const node = figma.
|
|
1150
|
+
const node = await figma.getNodeByIdAsync(nodeId);
|
|
1151
1151
|
if (!node) throw new Error(`Node not found: ${nodeId}`);
|
|
1152
1152
|
if (!("setReactionsAsync" in node)) throw new Error(`Node cannot have reactions: ${node.name}`);
|
|
1153
1153
|
const existing = (_a = node.reactions) != null ? _a : [];
|
|
@@ -1178,7 +1178,7 @@
|
|
|
1178
1178
|
for (const { frameId, direction, fixedChildren } of params.frames) {
|
|
1179
1179
|
const applied = [];
|
|
1180
1180
|
try {
|
|
1181
|
-
const node = figma.
|
|
1181
|
+
const node = await figma.getNodeByIdAsync(frameId);
|
|
1182
1182
|
if (!node) throw new Error(`Frame not found: ${frameId}`);
|
|
1183
1183
|
if (node.type !== "FRAME") {
|
|
1184
1184
|
throw new Error(`Node is not a FRAME: ${node.name} (type: ${node.type})`);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "Figma Prototype MCP",
|
|
3
|
-
"id": "
|
|
3
|
+
"id": "1647184714488719280",
|
|
4
4
|
"api": "1.0.0",
|
|
5
5
|
"main": "code.js",
|
|
6
6
|
"ui": "ui.html",
|
|
7
7
|
"editorType": ["figma"],
|
|
8
|
+
"documentAccess": "dynamic-page",
|
|
8
9
|
"permissions": ["teamlibrary"],
|
|
9
10
|
"networkAccess": {
|
|
10
11
|
"allowedDomains": ["ws://localhost:3000"],
|
|
11
|
-
"reasoning": "
|
|
12
|
+
"reasoning": "Connects only to a local companion server (ws://localhost:3000) that the user runs themselves; no external network access. The server bridges this plugin to an MCP client. https://github.com/smooeach/figma-prototype-mcp"
|
|
12
13
|
}
|
|
13
14
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -8,6 +8,45 @@ import { fileURLToPath } from "url";
|
|
|
8
8
|
|
|
9
9
|
// src/server/sessions.ts
|
|
10
10
|
import { randomUUID } from "crypto";
|
|
11
|
+
|
|
12
|
+
// src/server/messages.ts
|
|
13
|
+
var COMMUNITY_URL = "https://www.figma.com/community/plugin/1647184714488719280";
|
|
14
|
+
var PLUGIN_NOT_CONNECTED = [
|
|
15
|
+
`Figma plugin not connected. The MCP server is running, but no Figma plugin has connected yet.`,
|
|
16
|
+
`To connect: in Figma, open your file \u2192 Plugins \u2192 run "Prototype MCP".`,
|
|
17
|
+
`If you haven't installed it, get it from Figma Community:`,
|
|
18
|
+
COMMUNITY_URL,
|
|
19
|
+
`The plugin auto-connects to ws://localhost:3000/ws \u2014 once it shows "Connected", retry your request.`,
|
|
20
|
+
``,
|
|
21
|
+
`Figma \uD50C\uB7EC\uADF8\uC778\uC774 \uC5F0\uACB0\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. MCP \uC11C\uBC84\uB294 \uC2E4\uD589 \uC911\uC774\uC9C0\uB9CC \uD50C\uB7EC\uADF8\uC778\uC774 \uC544\uC9C1 \uC5F0\uACB0\uB418\uC9C0 \uC54A\uC558\uC5B4\uC694.`,
|
|
22
|
+
`\uC5F0\uACB0 \uBC29\uBC95: Figma\uC5D0\uC11C \uD30C\uC77C\uC744 \uC5F4\uACE0 \u2192 Plugins \u2192 "Prototype MCP" \uC2E4\uD589.`,
|
|
23
|
+
`\uC124\uCE58 \uC804\uC774\uB77C\uBA74 Figma Community\uC5D0\uC11C \uBC1B\uC73C\uC138\uC694:`,
|
|
24
|
+
COMMUNITY_URL,
|
|
25
|
+
`\uD50C\uB7EC\uADF8\uC778\uC740 ws://localhost:3000/ws\uC5D0 \uC790\uB3D9 \uC5F0\uACB0\uB429\uB2C8\uB2E4 \u2014 "Connected"\uAC00 \uB728\uBA74 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.`
|
|
26
|
+
].join("\n");
|
|
27
|
+
var PLUGIN_DISCONNECTED = [
|
|
28
|
+
`The Figma plugin disconnected before the command finished. This usually means the Figma tab/file or the plugin window was closed.`,
|
|
29
|
+
`Reopen "Prototype MCP" in Figma \u2014 it auto-reconnects \u2014 then retry.`,
|
|
30
|
+
``,
|
|
31
|
+
`\uBA85\uB839\uC774 \uB05D\uB098\uAE30 \uC804\uC5D0 Figma \uD50C\uB7EC\uADF8\uC778 \uC5F0\uACB0\uC774 \uB04A\uACBC\uC2B5\uB2C8\uB2E4. \uBCF4\uD1B5 Figma \uD0ED/\uD30C\uC77C\uC774\uB098 \uD50C\uB7EC\uADF8\uC778 \uCC3D\uC744 \uB2EB\uC558\uC744 \uB54C \uBC1C\uC0DD\uD574\uC694.`,
|
|
32
|
+
`Figma\uC5D0\uC11C "Prototype MCP"\uB97C \uB2E4\uC2DC \uC2E4\uD589\uD558\uBA74 \uC790\uB3D9 \uC7AC\uC5F0\uACB0\uB429\uB2C8\uB2E4 \u2014 \uADF8 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694.`
|
|
33
|
+
].join("\n");
|
|
34
|
+
var PLUGIN_CONNECTION_REPLACED = [
|
|
35
|
+
`Your plugin connection was replaced by a newer one. Only one Figma plugin can be active at a time (newest wins) \u2014 this usually means the plugin connected from a second Figma tab or file.`,
|
|
36
|
+
`Use the most recently opened "Prototype MCP" plugin, then retry.`,
|
|
37
|
+
``,
|
|
38
|
+
`\uD50C\uB7EC\uADF8\uC778 \uC5F0\uACB0\uC774 \uB354 \uC0C8\uB85C\uC6B4 \uC5F0\uACB0\uB85C \uAD50\uCCB4\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uD55C \uBC88\uC5D0 \uD558\uB098\uC758 Figma \uD50C\uB7EC\uADF8\uC778\uB9CC \uD65C\uC131\uD654\uB429\uB2C8\uB2E4(\uCD5C\uC2E0 \uC6B0\uC120) \u2014 \uBCF4\uD1B5 \uB450 \uBC88\uC9F8 Figma \uD0ED\uC774\uB098 \uD30C\uC77C\uC5D0\uC11C \uD50C\uB7EC\uADF8\uC778\uC774 \uC5F0\uACB0\uB410\uC744 \uB54C \uBC1C\uC0DD\uD574\uC694.`,
|
|
39
|
+
`\uAC00\uC7A5 \uCD5C\uADFC\uC5D0 \uC5F0 "Prototype MCP" \uD50C\uB7EC\uADF8\uC778\uC744 \uC0AC\uC6A9\uD55C \uB4A4 \uC7AC\uC2DC\uB3C4\uD558\uC138\uC694.`
|
|
40
|
+
].join("\n");
|
|
41
|
+
var pluginCommandTimeout = (command, ms) => [
|
|
42
|
+
`The Figma plugin is connected but didn't respond within ${ms}ms (command: ${command}).`,
|
|
43
|
+
`Figma may be busy, or the plugin may be stuck. Try closing and relaunching "Prototype MCP" in Figma, then retry.`,
|
|
44
|
+
``,
|
|
45
|
+
`Figma \uD50C\uB7EC\uADF8\uC778\uC774 \uC5F0\uACB0\uB3FC \uC788\uC9C0\uB9CC ${ms}ms \uC548\uC5D0 \uC751\uB2F5\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 (\uBA85\uB839: ${command}).`,
|
|
46
|
+
`Figma\uAC00 \uBC14\uC058\uAC70\uB098 \uD50C\uB7EC\uADF8\uC778\uC774 \uBA48\uCDC4\uC744 \uC218 \uC788\uC5B4\uC694. Figma\uC5D0\uC11C "Prototype MCP"\uB97C \uB2EB\uC558\uB2E4\uAC00 \uB2E4\uC2DC \uC2E4\uD589\uD55C \uB4A4 \uC7AC\uC2DC\uB3C4\uD558\uC138\uC694.`
|
|
47
|
+
].join("\n");
|
|
48
|
+
|
|
49
|
+
// src/server/sessions.ts
|
|
11
50
|
var PluginSession = class {
|
|
12
51
|
active = null;
|
|
13
52
|
pending = /* @__PURE__ */ new Map();
|
|
@@ -31,7 +70,7 @@ var PluginSession = class {
|
|
|
31
70
|
this.active.close();
|
|
32
71
|
} catch {
|
|
33
72
|
}
|
|
34
|
-
this.failAllPending(new Error(
|
|
73
|
+
this.failAllPending(new Error(PLUGIN_CONNECTION_REPLACED));
|
|
35
74
|
}
|
|
36
75
|
this.active = ws;
|
|
37
76
|
try {
|
|
@@ -43,7 +82,7 @@ var PluginSession = class {
|
|
|
43
82
|
clearActive(ws) {
|
|
44
83
|
if (this.active === ws) {
|
|
45
84
|
this.active = null;
|
|
46
|
-
this.failAllPending(new Error(
|
|
85
|
+
this.failAllPending(new Error(PLUGIN_DISCONNECTED));
|
|
47
86
|
}
|
|
48
87
|
}
|
|
49
88
|
handleResponse(msg) {
|
|
@@ -58,14 +97,14 @@ var PluginSession = class {
|
|
|
58
97
|
if (!this.isConnected()) {
|
|
59
98
|
await this.waitForConnection(this.connectWaitMs);
|
|
60
99
|
if (!this.isConnected()) {
|
|
61
|
-
throw new Error(
|
|
100
|
+
throw new Error(PLUGIN_NOT_CONNECTED);
|
|
62
101
|
}
|
|
63
102
|
}
|
|
64
103
|
const id = randomUUID();
|
|
65
104
|
return new Promise((resolve, reject) => {
|
|
66
105
|
const timer = setTimeout(() => {
|
|
67
106
|
this.pending.delete(id);
|
|
68
|
-
reject(new Error(
|
|
107
|
+
reject(new Error(pluginCommandTimeout(command, this.commandTimeoutMs)));
|
|
69
108
|
}, this.commandTimeoutMs);
|
|
70
109
|
this.pending.set(id, { resolve, reject, timer });
|
|
71
110
|
try {
|