figma-prototype-mcp 0.30.3 → 0.32.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/README.md CHANGED
@@ -172,9 +172,14 @@ To bypass the preset system (e.g. for `MOVE_IN`/`PUSH`/`SLIDE_*` directional tra
172
172
  | `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). |
173
173
  | `list_reactions` | Inspect existing reactions on a node |
174
174
  | `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. |
175
+ | `export_interactions` | Export the wired interactions of designated **completed screens** as a canonical, framework-agnostic **JSON spec** for developer handoff. Input `{ screens: string[] (frame node IDs), pageId? }`. Each interaction is a typed action (navigate / scrollTo / openOverlay / swapOverlay / closeOverlay / back / openUrl / setVariable / toggleVariable / changeVariant / conditional); unmappable actions are flagged in `unsupported[]`, unknown screen IDs in `missingScreens[]`. Read-only — developers (or Claude) derive framework code from the JSON. |
175
176
  | `clear_reactions` | Remove reactions from one or more nodes |
176
177
  | `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. |
177
178
 
179
+ ### Developer handoff: export interactions as JSON
180
+
181
+ `export_interactions` turns the prototype interactions you wired into a **language-neutral JSON spec** — a faithful map of "what each control does" (trigger → actions) for the screens you designate as done. It is intentionally framework-agnostic: it describes the behavior (navigate, set variable, conditional, …) using Figma's own vocabulary, and a developer (or Claude, on request) generates React/Vue/state-machine code from it. It does NOT emit framework code or visual UI — pair it with Figma Dev Mode / Code Connect for the UI.
182
+
178
183
  ## Troubleshooting
179
184
 
180
185
  | Symptom | Cause / fix |
@@ -510,6 +510,10 @@
510
510
  resetScrollPosition: action.resetScrollPosition
511
511
  };
512
512
  }
513
+ async function encodeReactionActions(reaction, resolvers) {
514
+ const raw = Array.isArray(reaction == null ? void 0 : reaction.actions) && reaction.actions.length > 0 ? reaction.actions : (reaction == null ? void 0 : reaction.action) ? [reaction.action] : [];
515
+ return Promise.all(raw.map((a) => encodeActionForListEcho(a, resolvers)));
516
+ }
513
517
  async function decodeConditionForEcho(condition, resolvers) {
514
518
  const decoded = decodeConditionExpression(condition);
515
519
  if ("raw" in decoded) return { raw: decoded.raw };
@@ -875,7 +879,7 @@
875
879
  return { page: { id: page.id, name: page.name }, frames, selection };
876
880
  }
877
881
  async function handleGetPrototypeFlow(params) {
878
- var _a, _b, _c, _d, _e, _f;
882
+ var _a, _b, _c;
879
883
  const page = await loadPage(params.pageId);
880
884
  const limit = (_a = params.limit) != null ? _a : 500;
881
885
  const frames = page.findAll((n) => n.type === "FRAME").map((f) => {
@@ -897,13 +901,12 @@
897
901
  const frameId = node.type === "FRAME" ? node.id : findEnclosingFrameId(node);
898
902
  const reactions = (_b = node.reactions) != null ? _b : [];
899
903
  for (const r of reactions) {
900
- const firstAction = (_e = (_d = (_c = r.actions) == null ? void 0 : _c[0]) != null ? _d : r.action) != null ? _e : {};
901
904
  interactions.push({
902
905
  frameId,
903
906
  sourceNodeId: node.id,
904
907
  sourceNodeName: node.name,
905
- trigger: (_f = r.trigger) != null ? _f : { type: "UNKNOWN" },
906
- action: await encodeActionForListEcho(firstAction, echoResolvers)
908
+ trigger: (_c = r.trigger) != null ? _c : { type: "UNKNOWN" },
909
+ actions: await encodeReactionActions(r, echoResolvers)
907
910
  });
908
911
  }
909
912
  }
@@ -1131,12 +1134,11 @@
1131
1134
  nodeId: node.id,
1132
1135
  nodeName: node.name,
1133
1136
  reactions: await Promise.all(reactions.map(async (r, i) => {
1134
- var _a2, _b, _c, _d;
1135
- const firstAction = (_c = (_b = (_a2 = r.actions) == null ? void 0 : _a2[0]) != null ? _b : r.action) != null ? _c : {};
1137
+ var _a2;
1136
1138
  return {
1137
1139
  index: i,
1138
- trigger: (_d = r.trigger) != null ? _d : { type: "UNKNOWN" },
1139
- action: await encodeActionForListEcho(firstAction, echoResolvers)
1140
+ trigger: (_a2 = r.trigger) != null ? _a2 : { type: "UNKNOWN" },
1141
+ actions: await encodeReactionActions(r, echoResolvers)
1140
1142
  };
1141
1143
  }))
1142
1144
  };
@@ -265,8 +265,12 @@ var GetCanvasOverviewInput = z.object({
265
265
  });
266
266
  var GetPrototypeFlowInput = z.object({
267
267
  pageId: z.string().optional(),
268
- limit: z.number().int().positive().max(2e3).default(500)
268
+ limit: z.number().int().positive().max(5e3).default(500)
269
269
  });
270
+ var ExportInteractionsInput = z.object({
271
+ screens: z.array(z.string()).min(1),
272
+ pageId: z.string().optional()
273
+ }).strict();
270
274
  var FindNodesInput = z.object({
271
275
  query: z.string().min(1),
272
276
  nodeTypes: z.array(z.string()).optional(),
@@ -882,6 +886,102 @@ function compileProtoConditional(input) {
882
886
  return { connections, replaceExisting: input.replaceExisting };
883
887
  }
884
888
 
889
+ // src/server/interaction-spec.ts
890
+ function mapCondition(c) {
891
+ if (c && typeof c === "object") {
892
+ if (Array.isArray(c.all)) return { all: c.all.map(mapCondition) };
893
+ if (Array.isArray(c.any)) return { any: c.any.map(mapCondition) };
894
+ if ("variable" in c && "operator" in c) {
895
+ return { variable: c.variable, operator: c.operator, value: c.value };
896
+ }
897
+ if ("raw" in c) return { raw: c.raw };
898
+ }
899
+ return { raw: c };
900
+ }
901
+ function mapAction(a, source, unsupported) {
902
+ if (!a || typeof a !== "object") {
903
+ unsupported.push({ source, reason: "non-object action", raw: a });
904
+ return null;
905
+ }
906
+ switch (a.type) {
907
+ case "BACK":
908
+ return { type: "back" };
909
+ case "CLOSE":
910
+ return { type: "closeOverlay" };
911
+ case "URL":
912
+ return { type: "openUrl", url: a.url, openInNewTab: a.openInNewTab };
913
+ case "set_variable":
914
+ return { type: "setVariable", variable: a.variable, value: a.value };
915
+ case "toggle_variable":
916
+ return { type: "toggleVariable", variable: a.variable };
917
+ case "NODE": {
918
+ const to = { id: a.destinationId ?? null, name: a.destinationName ?? null };
919
+ switch (a.navigation) {
920
+ case "NAVIGATE":
921
+ return { type: "navigate", to, transition: a.transition };
922
+ case "SCROLL_TO":
923
+ return { type: "scrollTo", to, transition: a.transition };
924
+ case "OVERLAY":
925
+ return { type: "openOverlay", to, transition: a.transition };
926
+ case "SWAP":
927
+ return { type: "swapOverlay", to, transition: a.transition };
928
+ case "CHANGE_TO":
929
+ return { type: "changeVariant", to };
930
+ default:
931
+ unsupported.push({ source, reason: `unknown navigation: ${String(a.navigation)}`, raw: a });
932
+ return null;
933
+ }
934
+ }
935
+ case "CONDITIONAL": {
936
+ if (a.condition === void 0 || a.then === void 0) {
937
+ unsupported.push({ source, reason: "non-standard conditional", raw: a });
938
+ return null;
939
+ }
940
+ const then = (Array.isArray(a.then) ? a.then : []).map((x) => mapAction(x, source, unsupported)).filter((x) => x !== null);
941
+ const elseActions = a.else !== void 0 ? (Array.isArray(a.else) ? a.else : []).map((x) => mapAction(x, source, unsupported)).filter((x) => x !== null) : void 0;
942
+ return { type: "conditional", if: mapCondition(a.condition), then, else: elseActions };
943
+ }
944
+ default:
945
+ unsupported.push({ source, reason: `unknown action type: ${String(a.type)}`, raw: a });
946
+ return null;
947
+ }
948
+ }
949
+ function buildInteractionSpec(flow, screens) {
950
+ const frameById = new Map((flow.frames ?? []).map((f) => [f.id, f]));
951
+ const byFrame = /* @__PURE__ */ new Map();
952
+ for (const it of flow.interactions ?? []) {
953
+ if (it.frameId == null) continue;
954
+ const arr = byFrame.get(it.frameId) ?? [];
955
+ arr.push(it);
956
+ byFrame.set(it.frameId, arr);
957
+ }
958
+ const unsupported = [];
959
+ const screensOut = [];
960
+ const missingScreens = [];
961
+ for (const id of screens) {
962
+ const frame = frameById.get(id);
963
+ if (!frame) {
964
+ missingScreens.push(id);
965
+ continue;
966
+ }
967
+ const interactions = (byFrame.get(id) ?? []).map((it) => {
968
+ const source = { id: it.sourceNodeId, name: it.sourceNodeName ?? null };
969
+ const actions = (Array.isArray(it.actions) ? it.actions : []).map((a) => mapAction(a, source, unsupported)).filter((x) => x !== null);
970
+ return { source, trigger: it.trigger, actions };
971
+ });
972
+ screensOut.push({ id, name: frame.name ?? null, interactions });
973
+ }
974
+ return {
975
+ schemaVersion: "1.0",
976
+ page: flow.page ?? { id: "", name: "" },
977
+ screens: screensOut,
978
+ requestedScreens: screens,
979
+ missingScreens,
980
+ unsupported,
981
+ truncated: Boolean(flow.truncated)
982
+ };
983
+ }
984
+
885
985
  // src/server/tools.ts
886
986
  async function recordedHandler(store, tool, parsedInput, send) {
887
987
  const result = await send();
@@ -898,10 +998,21 @@ function makeTools(historyStore) {
898
998
  },
899
999
  {
900
1000
  name: "get_prototype_flow",
901
- description: "Return the whole prototype interaction graph of a page in ONE call: its frames (each with `isStartFrame`) and every wired interaction \u2014 `{ frameId, frameName, sourceNodeId, sourceNodeName, trigger, action }`. `action` is decoded exactly as `list_reactions` returns it (navigate / scroll / overlay / swap / close / back / url / change_to / set_variable / toggle_variable / conditional incl. all/any compound). Use this to see what is ALREADY wired before adding more (avoid duplicates, check what a screen connects to); for a single node use list_reactions. Page-scoped \u2014 optional `pageId` (defaults to current page); `limit` caps interactions (default 500) and sets `truncated`.",
1001
+ description: "Return the whole prototype interaction graph of a page in ONE call: its frames (each with `isStartFrame`) and every wired interaction \u2014 `{ frameId, frameName, sourceNodeId, sourceNodeName, trigger, actions }`. Each entry in `actions` is decoded exactly as `list_reactions` returns it (navigate / scroll / overlay / swap / close / back / url / change_to / set_variable / toggle_variable / conditional incl. all/any compound). Use this to see what is ALREADY wired before adding more (avoid duplicates, check what a screen connects to); for a single node use list_reactions. Page-scoped \u2014 optional `pageId` (defaults to current page); `limit` caps interactions (default 500) and sets `truncated`.",
902
1002
  schema: GetPrototypeFlowInput,
903
1003
  command: "GET_PROTOTYPE_FLOW"
904
1004
  },
1005
+ {
1006
+ name: "export_interactions",
1007
+ description: "\uC774 \uB3C4\uAD6C\uB294 \uC624\uC9C1 \uD53C\uADF8\uB9C8\uC758 \uD504\uB85C\uD1A0\uD0C0\uC785 \uC778\uD130\uB799\uC158/\uC560\uB2C8\uBA54\uC774\uC158 \uC0DD\uC131\xB7\uC218\uC815 \uBAA9\uC801\uC73C\uB85C\uB9CC \uC0AC\uC6A9\uD569\uB2C8\uB2E4. Export the wired prototype interactions of the given completed screens as a canonical, framework-agnostic JSON spec for developer handoff. Input `{ screens: string[] (frame node IDs), pageId? }`. Returns `{ schemaVersion, page, screens:[{id,name,interactions:[{source,trigger,actions}]}], requestedScreens, missingScreens, unsupported, truncated }`. Each action is a typed entry (navigate / scrollTo / openOverlay / swapOverlay / closeOverlay / back / openUrl / setVariable / toggleVariable / changeVariant / conditional). This is a READ/handoff tool \u2014 developers (or you) derive framework code (React, etc.) from the JSON; it does NOT generate framework or UI code.",
1008
+ schema: ExportInteractionsInput,
1009
+ handler: async (input, session) => {
1010
+ const { screens, pageId } = input;
1011
+ const params = pageId ? { pageId, limit: 5e3 } : { limit: 5e3 };
1012
+ const flow = await session.sendCommand("GET_PROTOTYPE_FLOW", params);
1013
+ return buildInteractionSpec(flow, screens);
1014
+ }
1015
+ },
905
1016
  {
906
1017
  name: "find_nodes",
907
1018
  description: "Search nodes on the current page (or document) by name substring, with optional type filter.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figma-prototype-mcp",
3
- "version": "0.30.3",
3
+ "version": "0.32.0",
4
4
  "description": "MCP server for creating Figma prototype interactions via natural language",
5
5
  "license": "MIT",
6
6
  "author": "smooeach",