figma-prototype-mcp 0.30.3 → 0.31.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 |
@@ -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 mapped = mapAction(it.action, source, unsupported);
970
+ return { source, trigger: it.trigger, actions: mapped ? [mapped] : [] };
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();
@@ -902,6 +1002,17 @@ function makeTools(historyStore) {
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.31.0",
4
4
  "description": "MCP server for creating Figma prototype interactions via natural language",
5
5
  "license": "MIT",
6
6
  "author": "smooeach",