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 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
- - Open Figma desktop app.
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.getNodeById(pageId);
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.getNodeById(action.targetFrameId);
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.getNodeById(action.targetNodeId);
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.getNodeById(action.targetFrameId);
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.getNodeById(action.targetFrameId);
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.getNodeById(action.targetVariantId);
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.getNodeById(conn.sourceNodeId);
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.getNodeById(id)) == null ? void 0 : _a.name) != null ? _b : void 0;
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.getNodeById(params.nodeId);
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.getNodeById(nodeId);
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.getNodeById(frameId);
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": "figma-prototype-mcp-local",
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": "Local WebSocket bridge to the figma-prototype-mcp unified server (Phase A v1.19+)"
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
  }
@@ -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("Plugin connection replaced by newer connection"));
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("Plugin disconnected"));
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("\uD53C\uADF8\uB9C8 \uD50C\uB7EC\uADF8\uC778 \uC5F0\uACB0\uC744 \uD655\uC778\uD574\uC8FC\uC138\uC694");
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(`Command ${command} timed out after ${this.commandTimeoutMs}ms`));
107
+ reject(new Error(pluginCommandTimeout(command, this.commandTimeoutMs)));
69
108
  }, this.commandTimeoutMs);
70
109
  this.pending.set(id, { resolve, reject, timer });
71
110
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-prototype-mcp",
3
- "version": "0.30.0",
3
+ "version": "0.30.2",
4
4
  "description": "MCP server for creating Figma prototype interactions via natural language",
5
5
  "license": "MIT",
6
6
  "author": "smooeach",