# musepy/genable [Health: Active]

**Category:** 📐 Architecture & Design  
**Repository:** https://github.com/musepy/genable  
**GitHub Stars:** 1  
**npm Downloads (last month):** 233  
**Views:** 4  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/musepy-genable

## Description
Write-side MCP for Figma — complements Figma's official read-only MCP. 41 tools for building and editing Figma designs from Claude Code, Cursor, Cline: JSX-like tree creation, variables/tokens, components, cross-page navigation, and visual verification via screenshots. Install: npx -y genable-mcp.

## Tools
Capabilities this server exposes over MCP:

- **jsx** — Create design trees with nested JSX markup. One jsx call builds a complete subtree atomically — nesting is the hierarchy. Keep a single logical unit inside one call; the returned root's children are already built, not stubs to be filled in later.

Examples:
  jsx({markup: "<frame name='Card' layout='column' padding={16} fill='#FFFFFF' w='fill' />"})
  jsx({markup: "<frame name='Row' layout='row' gap={8} padding={12} w='fill'><icon name='lucide:settings' size={20} /><text name='Label' w='fill'>Account</text><icon name='lucide:chevron-right' size={16} /></frame>"})

Elements: frame, text, rect, ellipse, line, icon, image, instance, component, group, section, vector
Attributes (frame): layout, justify, items, wrap, w, h, minW, maxW, p, gap, bg, fill, rounded, stroke, shadow, blur, bgblur, opacity, layoutPositioning
Attributes (grid container): cols, rows, gap, rowGap, colGap, colSizes, rowSizes, autoFlow, autoRows — see "Grid layout" below
Attributes (grid child): colSpan, rowSpan, alignX, alignY, rowStart, colStart
Attributes (text): size, weight, lineHeight, font, fill, w (w="fill" for wrap), maxLines, textTruncation, paragraphSpacing
Multi-paragraph text: put a BLANK LINE between paragraphs inside <text> — it becomes a real paragraph break. A single newline is collapsed to a space (prose wrap); for a hard single-line break write {'\n'}. Pair multi-paragraph body copy with paragraphSpacing={N} for gap between paragraphs.
Truncation: maxLines={N} caps a wrapping text at N lines with an ellipsis (needs a bounded width, e.g. w="fill" or w={240}).
Advanced text — inline emphasis spans (**bold** *italic* ~~strike~~ in content), coloured/sized spans, vertical trim, hanging indent/punctuation, list spacing → read skill:typography (don't inline these here).
Effects: shadow="0,8,32,0,#0006" or shadow={shadow(0,8,32,0,'#0006')}; blur={10} for layer blur; bgblur={20} for frosted-glass/glassmorphism background blur. Multiple effects merge automatically.
Decoration in auto-layout: floating orbs/blobs/decorative shapes inside a row/column parent need layoutPositioning="absolute" so they don't get stacked into the main-axis flow.
Full-frame backgrounds: set the parent frame's bg directly. Don't add a separate <rect> backdrop. Supported gradient strings (CSS-like subset, not full CSS):
  - linear-gradient(<angle>deg, <#hex> <pos>%, ...)        e.g. "linear-gradient(135deg, #A 0%, #B 100%)"
  - linear-gradient(to <direction>, <stops>)               directions: top/right/bottom/left and corners (e.g. "to bottom right")
  - radial-gradient(<stops>)                               centered, ellipse-fill — no position/shape modifiers
  - radial-gradient(circle, <stops>)                       centered, circle shape only
  - conic-gradient(from <angle>deg, <stops>)
  Unsupported (will be rejected): "circle at X% Y%", "ellipse at ...", "X% Y%" position syntax, named colors (red/blue/...), hsl(), and numeric stops without a percent sign.
Text: <text size={24}>content here</text>
Rich text (mixed inline styles in one text node — runtime parses markup per character):
  **bold**            e.g. "Click **here** to continue"
  *italic*            e.g. "Read the *fine print*"
  ***bold italic***   e.g. "This is ***critical***"
  ~~strikethrough~~   e.g. "Was ~~$19.99~~"
  {color:#HEX|text}   e.g. "{color:#EF4444|Error}: something failed"
  {size:N|text}       e.g. "{size:32|Big} then normal"
  Nesting stacks: "{color:#EF4444|**$9.99**}" → red + bold on the same range. For uniform whole-text styling use the weight / fill / size props instead — markup is only for MIXED styles within one text node.
Instance: <instance ref="Button" variant="Size=Large"/>
Self-closing: <line w="fill" stroke="#E5E7EB"/> (use line for dividers/separators; rect/ellipse for SMALL pure decoration with no children — page-level backgrounds belong on the parent frame's bg)
Arc/Ring: <ellipse w={120} h={120} arc="0 270" fill="#4F46E5"/> (arc="start end innerRadius?" — innerRadius 0-1 makes a donut/ring)
Grid layout: `layout="grid"` + `cols={N}`. Omit `rows` for auto-rows (rows grow to fit children); set `rows={M}` for a fixed N×M. `gap`, or `rowGap`/`colGap` separately.
  - Per-track sizing: `colSizes="240px 1fr 1fr"` (fixed sidebar + two flexible columns) or `rowSizes="80px 1fr"` (fixed header row + flexible body). Tokens: `Npx`/`N`=fixed px, `Nfr`=flexible share (`1fr 2fr` ⇒ 1:2), `hug`=fit-content. Setting `colSizes`/`rowSizes` infers `cols`/`rows` from the token count — don't also pass a conflicting count.
  - Auto-flow: `autoFlow` packs children row-major into the next free cell (best for galleries/masonry, esp. with mixed spans). `autoRows` lets the grid manage its own row count.
  - Per-child: `colSpan`/`rowSpan` (cell spanning), `alignX`/`alignY` (start|center|end|auto within the cell), `rowStart`/`colStart` (0-based explicit cell anchor — requires manual placement, so do NOT combine with `autoFlow`).
  Sizing defaults (lean on them): rows are HUG (cells fit content) and the container HUGS its height automatically, while children default to fill-width / hug-height. So a content gallery needs only `cols` + a `w` (fixed px or `"fill"`) — no `rowSizes`, no height. For equal-height rows (KPI tiles, dashboards) set `rowSizes="1fr 1fr"` (children then FILL the rows); for fixed-height bands use px row sizes. A `rowSpan>1` child fills its span.
  Gotchas: (1) the container can hug HEIGHT but not WIDTH (columns are FLEX) — always give it a `w` (`"fill"` or px). (2) `justify`/`items` are no-ops on a grid — use per-child `alignX`/`alignY`. (3) for a 1-column stack use `layout="column"`, not grid. Prefer grid for a 2-D matrix (galleries, KPI tiles, pricing tiers, dashboards); use row/column when children differ in width or you need space-between.
Variable binding: fill/bg/stroke accept qualified bare-name token strings (e.g. bg="$Theme/Bg/Surface"). Object literals (fill={{variable_id:...}}) drop the binding silently — always use the string form.

Swap an existing subtree: jsx({replaceId: "<id>", markup: "..."}) replaces the old node at the same parent and sibling index atomically, preserving position in one call. Markup must have a single root. Use jsx for tree creation; edit for property updates on known nodes.
- **inspect** — Read design node(s) — choose what to surface with `facets`.

Default (no facets) returns a skeleton: id, name, type, role, children.
For anything else, list the facets you need — nothing else is included.

Facets:
  structure   name, type, size, layout shorthand — cheap overview
  layout      layoutMode/gap/padding/align/sizing (row/column, fill/hug, etc.)
  paint|fill  fills + Paint.boundVariables.color (see bound tokens)
  stroke      strokes, strokeWeight, strokeAlign, dashPattern
  effects     shadows, blurs
  typography|text  fontFamily, fontSize, fontWeight, lineHeight, letterSpacing
  appearance  opacity, visible, blendMode, cornerRadius, clipsContent
  variables   node-level boundVariables + explicitVariableModes (token bindings)
  lint        validation view — per-node role + visual/layout summary + issues (severity error/warning/info). Specific node only, depth-3 default; exclusive (ignores other facets).
  all         everything

Parameters:
  node    "/" for page root, or node ID from jsx/inspect results (e.g. "100:5").
  facets  array of facet names listed above.
  depth   Max tree depth (default: 5, max: 10).

Selection: when the user prompt carries a `<selected_nodes>[{id,name,type}…]</selected_nodes>` block, those IDs are authoritative (a reference, not a snapshot) — inspect them directly, don't call `get_selection`. Start shallow (depth 1–2); go deeper only when the task needs leaf access — a full-depth dump of a large tree burns thousands of tokens.

Examples:
  inspect({node: "/"})                                   → page skeleton
  inspect({node: "100:5"})                               → one-node skeleton
  inspect({node: "100:5", facets: ["variables"]})        → token bindings only
  inspect({node: "100:5", facets: ["layout", "paint"]})  → layout + fills
  inspect({node: "100:5", facets: ["all"]})              → full properties
  inspect({node: "100:5", facets: ["lint"]})             → validate subtree (roles + issues)

Use `get_screenshot` for visual verification. Use `facets: ["lint"]` to validate a subtree (roles + issues) rather than read properties.
- **edit** — Batch update properties on nodes. The universal write tool — use for sizing, radius, opacity, effects, instance component-prop overrides, or any property not covered by focused setters.

For single-property changes, prefer focused setters (they validate input more strictly):
  set_text   — text content
  set_fill   — fill / background color
  set_stroke — border
  set_layout — auto-layout (gap, padding, direction)

Use when:
- Batch fixes — multiple nodes, mixed properties in one call
- Properties not covered by setters: w/h, corner, opacity, blur, shadow, INSTANCE TEXT/BOOLEAN/INSTANCE_SWAP overrides
- Need to mix Figma props and component property overrides in the same node entry

Returns (per node): `applied: [keys]` (written), `noop: [keys]` (already that value), `rejected: [{key, reason}]`. Empty arrays are omitted.
- Single: { data: { id, name, type, applied?, noop?, rejected? } }
- Batch:  { data: { count, results: [...], errors?, partial? } }

`rejected` is TERMINAL: the property is invalid for that node type (e.g. `corner`/cornerRadius on a TEXT node, `layoutMode` on a vector) or readonly — it will NEVER apply. Do NOT retry a rejected property; choose one valid for that node type, or target a different node. If EVERY requested property is rejected, the call returns `{ error }` — do not re-issue it.
`partial: true` (batch) means some entries succeeded and some failed — check `errors[]` for which entries to retry.

Skip when:
- One property on one node — use the focused setter for clearer errors
- Color via variable token — use set_fill/set_stroke (edit silently drops bare-name tokens here)
- Need to apply auto-layout — use set_layout

Component property overrides:
  For instances, use property DISPLAY NAMES (e.g. "Label") — edit resolves them to Figma's internal keys automatically. Component props mix with Figma props in the same call.

Examples:
  edit({node: "1:2", props: {corner: 16, opacity: 0.8}})
  edit({nodes: [
    {node: "1:1", props: {w: "fill", corner: 8}},        // Figma native props
    {node: "1:2", props: {opacity: 0.6}},
    {node: "1:3", props: {Label: "Sign In"}}             // instance TEXT prop (by display name)
  ]})
- **read_jsx** — Read a Figma subtree as canonical JSX text — the same dialect the `jsx` tool accepts as input. Output is line-numbered and byte-stable: slicing a chunk and feeding it as `edit_jsx`'s `old_string` is guaranteed to match.

Each existing node carries `id="X:Y"` as the first attribute so edit_jsx can update it in place. Defaults are dropped, vocabulary uses short DSL names (w/h/p/gap/bg/rounded/layout/justify/items/content).

When to use:
- Before `edit_jsx`: read first to get the exact text to replace
- To browse a subtree as code rather than as JSON (inspect facets)
- To produce a substring for `search_jsx` to grep against

Skip when:
- You only need one property value → use inspect with facets
- You need variable bindings or style references → use inspect's variables/styles facet

Example:
  read_jsx({ node: "1:1" })
  →  0001  <frame id="1:1" name="Login" w={320} h={480} layout="column" gap={12} p={24} bg="#fff">
     0002    <text id="1:2" size={24} weight="Bold">Welcome back</text>
     0003    <frame id="1:3" w="fill" h={44} rounded={8} layout="row" justify="center" items="center" bg="#000">
     0004      <text id="1:4" size={14} fill="#fff">Sign In</text>
     0005    </frame>
     0006  </frame>

Output is wrapped in {data:{jsx:"..."}} — pass jsx (with or without line numbers) to edit_jsx.
- **edit_jsx** — Update existing nodes by editing the canonical JSX text of a subtree. Same contract as Claude Code's Edit tool: pass an `old` substring (from read_jsx output) and a `new` substring; the system locates the match, parses the new JSX, and applies property changes to every node whose `id` is preserved across old → new.

Read first, then edit. `old` must come from read_jsx output for the same scope. Uniqueness is enforced unless replace_all is true.

Chaining edits — DON'T re-read between edits. On success the response returns the re-serialized scope as `data.jsx` (same line-numbered format as read_jsx, reflecting your change). Slice your next `old` from THAT, not from a stale earlier read. Only call read_jsx again if `data.jsxTruncated` is set (scope too large to echo).

Accepted `new` shapes:
- Self-closing: `<frame id="1:5" bg="#0066FF" rounded={8} />` — prop-only edit, children untouched
- Opening tag only: `<frame id="1:5" bg="#0066FF" rounded={8}>` — auto-converted to self-closing (same semantics as above)
- Full element with closing: `<frame id="1:5" ...>...children...</frame>` — touches nested ids too

What it does (MVP scope):
- For every `<tag id="X:Y" ...>...</tag>` in `new`, set that node's props to match (multi-node, multi-prop in one call).
- For text nodes, the children content is treated as the new `characters`.
- Defaults are dropped, vocabulary uses short DSL names (w/h/p/gap/bg/rounded/layout/justify/items/content).

What it does NOT do yet:
- Creating new children (use `jsx` tool — the description tells you what's supported).
- Deleting children that disappeared from new (use `delete_node`).
- Reparenting / moving (use `move_node`).

Example — restyle a button:
  // 1. Read first
  read_jsx({ node: "1:1" })
  // 2. Edit
  edit_jsx({
    scope: "1:1",
    old: '<frame id="1:5" w="fill" h={44} rounded={8} bg="#000">',
    new: '<frame id="1:5" w="fill" h={44} rounded={12} bg="#0066FF">',
  })
  // → updates node 1:5 with new rounded + bg

Example — multi-node edit (one call):
  edit_jsx({
    scope: "1:1",
    old: '<frame id="1:5" w="fill" h={44} rounded={8} bg="#000">\n  <text id="1:6" size={14} fill="#fff">Sign In</text>\n</frame>',
    new:  '<frame id="1:5" w="fill" h={44} rounded={12} bg="#0066FF">\n  <text id="1:6" size={16} fill="#fff">Sign Up</text>\n</frame>',
  })
  // → updates node 1:5 (rounded, bg) AND node 1:6 (size, content) atomically
- **find_nodes** — Search nodes on the current page by name substring or type. Case-insensitive. Scoped to the current page — call switch_page first if your target lives elsewhere.

Match logic: a node matches if its NAME contains the query (substring) OR its TYPE equals the query (exact, lowercased). So query "frame" finds both nodes named "Frame 12" and all nodes of type FRAME.

Use when:
- User refers to a node by name without giving an ID ("the Hero button", "the Footer")
- Locating all nodes of a specific type (COMPONENT, TEXT, FRAME, INSTANCE) before bulk operations
- Looking up existing nodes before jsx() so you can wire them as <instance>/<component>

Returns: { data: { results: [{id, name, type, x, y, width, height}], total, truncated: boolean } }

Capped at 20 results — set explicit scope to narrow if truncated.

Skip when:
- You already have the node ID — call inspect directly
- You want the user's current selection — use get_selection
- Target is on a different page — call switch_page first

Examples:
  find_nodes({query: "Button"})              // by name substring
  find_nodes({query: "frame"})               // by type (FRAME)
  find_nodes({query: "Card", scope: "1:2"})  // scoped to subtree
- **discover_props** — Discover the unique values used for given properties across a subtree. For surveying a design system or auditing inconsistency.

Use when:
- Auditing token coverage — "what colors actually appear in this page?"
- Spotting outliers — "are there fontSize values that escaped the type scale?"
- Before a bulk replace_props — to know which source values you'll be replacing

Returns: { data: { <propName>: [<unique values, deduplicated>...] } }

Each requested property name becomes a key; its value is the list of distinct values found at any node in the subtree.

Searchable properties: fillColor, textColor, strokeColor, strokeWeight, opacity, cornerRadius, gap, fontSize, fontFamily, fontWeight.

Skip when:
- You want a specific node's properties — use inspect with facets
- You want to find nodes BY a value (not values used by name) — use find_nodes or replace_props
- Property you care about isn't in the searchable list — discover_props can't surface it

Examples:
  discover_props({node: "1:2", props: ["fillColor", "fontSize"]})
  discover_props({node: "/", props: ["cornerRadius", "opacity"]})    // whole page
- **replace_props** — Bulk find-and-replace property values across a subtree (target node + all descendants). Destructive batch mutation — no preview, no undo across many nodes. Returns per-rule match counts.

Use when:
- Theming pass: change every #FFF fill to #000 across a screen
- Token migration: bump every fontSize from 14 to 16
- Normalizing values left inconsistent by earlier passes
- The alternative is N targeted single-node calls (set_text / set_fill / edit)

Returns: { data: { replacements: [{ rule: 0, matched: 12 }, { rule: 1, matched: 0 }] } }

Parameters beyond schema:
- `node` is the subtree root; search recurses into all descendants (depth-first).
- Each rule's `from` is an EXACT-match string (no substring, no regex). For typed props (fontSize, opacity), pass values as strings — the executor coerces.
- Zero matches do NOT error — they return matched: 0. Sanity-check with discover_props first if you're unsure values exist.

Skip when:
- Updating a single known node — use set_text / set_fill / set_stroke / set_layout for type-aware single-intent edits, or edit for generic.
- Values are variable-bound (tokens) — replace_props bypasses bindings; use bind_variable to swap the token instead.
- You need partial / fuzzy match — replace_props is exact-only; you'll need find_nodes + a loop.

Examples:
  // single rule, white -> black
  replace_props({node: "1:2", rules: [{prop: "fillColor", from: "#FFF", to: "#000"}]})

  // batch theme update — both rules applied in one pass
  replace_props({node: "1:2", rules: [
    {prop: "fillColor", from: "#FFF", to: "#000"},
    {prop: "fontSize", from: "14", to: "16"}
  ]})
- **delete_node** — Delete a node and all its children. The node id becomes invalid after deletion — drop any cached references.

Use when:
- Removing nodes you created earlier in this session (cleanup, retry after misplacement)
- User explicitly asks to remove, delete, or "get rid of" an element

Returns: { data: { id, name, changed: true } }

If the deleted node was NOT created in the current session, the response carries an extra `warning` field. The deletion still happened — this is a signal that you removed pre-existing user work, surface it to the user if it wasn't requested.

Skip when:
- Target is the page root ("/") — use delete_page to remove a whole page
- You want to relocate rather than remove — use move_node
- The node is currently user-selected and removal wasn't explicitly requested — confirm via ask_user first

Examples:
  delete_node({node: "1:2"})
- **move_node** — Relocate a node without recreating it. Preserves IDs, bound variables, and component instances across the move, so callers tracking the node by ID never need to re-discover it. Use for: (a) changing child order within a container, (b) moving a subtree into a different parent, (c) fixing a placement mistake after jsx.

Examples:
  move_node({node: "1:3", name: "NewTitle"})         — rename in place
  move_node({node: "1:3", parent: "1:4"})            — move into parent 1:4
  move_node({node: "1:5", index: 0})                 — reorder within current parent
- **clone_node** — Deep-copy a node with optional property overrides.

Examples:
  clone_node({node: "1:2"})                                 — clone to page root, same name
  clone_node({node: "1:2", parent: "/"})                    — clone to page root explicitly
  clone_node({node: "1:2", parent: "/", name: "Hero Copy"}) — clone to root with custom name
  clone_node({node: "1:2", parent: "1:4"})                  — clone into parent node 1:4
  clone_node({node: "1:2", parent: "1:4", overrides: {"bg": "#D9D9D9"}})
- **list_variables** — List variables as a flat array with referenced collections.

Returns {data: {variables[], collections[], nextCursor?}}. Each variable carries
its full Figma shape: id, name, variableCollectionId, resolvedType, valuesByMode.
collections[] only includes collections referenced by the returned variables
(use for mode-name resolution).

Parameters:
  collection — VariableCollectionId to filter by
  filter     — substring match on variable name (case-insensitive)
  cursor     — opaque pagination cursor from a previous call
  limit      — max variables per page (default 100)

Examples:
  list_variables()
  list_variables({collection: "VariableCollectionId:1:2"})
  list_variables({filter: "bg"})
  list_variables({cursor: "100"})
- **create_collection** — Create a VariableCollection with named modes — idempotent (find-or-create).

Returns the existing collection if one with the same name + identical mode list
(same order) already exists, otherwise creates a new one. Safe to retry. Spec §3.1.

The first mode in the array becomes the default mode. Mode order is part of
identity: ["Light","Dark"] ≠ ["Dark","Light"] (Figma resolves the first-listed
mode at the root).

Omit idempotency_key — the handler computes it canonically from (name, modes).
Pass it only if you need strict concurrency-safety validation (LLMs should not
try to compute SHA-256 inline; placeholder strings are rejected).

Returns {data: {collection_id, modes: [{modeId, name}], reused?: true}} — use
those modeIds with set_variable_value and set_variable_mode.

Examples:
  create_collection({name: "Theme", modes: ["Light", "Dark"]})
  create_collection({name: "Device", modes: ["Desktop", "Tablet", "Mobile"]})
- **create_variable** — Create a variable in a collection — idempotent (find-or-create) and value-capable.

Re-running with the same args returns the EXISTING variable instead of creating a
duplicate, so this is safe to retry. Omit `values_by_mode` to create an empty
variable; provide it to populate every mode in one call.

Behavior (spec §3.1), matched by (collection, name, type) in the target collection:
  - Exactly 1 match → idempotent reuse (returns it, `reused: true`).
  - 0 in target, matches in OTHER collections → create new in target + warning
    NAME_EXISTS_OUTSIDE_TARGET_COLLECTION.
  - 0 anywhere → create new.
  - 2+ in target collection (Figma allows duplicates) → fail SAME_COLLECTION_NAME_DUPLICATE.

values_by_mode keys can be either mode NAMES (e.g. "Light") or modeIds (e.g. "1:0").
Each value must match the variable type (hex/RGBA for COLOR, number for FLOAT,
string for STRING, boolean for BOOLEAN).

Omit idempotency_key — the handler computes it canonically from
(collection, name, type, values_by_mode). Pass it only if you need strict
concurrency-safety validation (LLMs should not try to compute SHA-256 inline;
placeholder strings are rejected).

Mode coverage policy (spec §6.2):
  - mode_coverage_required: 'all' (default) — every mode in the collection
    must have an explicit value. set_fill / bind_variable will REJECT
    bindings that fall through to a missing mode (MISSING_MODE_VALUES).
  - mode_coverage_required: 'opt-in-fallback' — fallback to default mode
    is intended. Bindings emit FALLBACK_BINDING warning instead of failing.
    Caller MUST provide fallback_reason containing the structured phrase
    "fallback to <mode_name>" (machine-greppable).

Returns {data: {variable_id, name, type, collection_id, mode_coverage[],
mode_coverage_required, reused?: true}, warnings?: [...]}.

Examples:
  create_variable({collection: "VariableCollectionId:1:2", name: "Theme/bg", type: "COLOR"})  // empty
  create_variable({collection: "VariableCollectionId:1:2", name: "Text/Primary", type: "COLOR", values_by_mode: {Light: "#111", Dark: "#EEE"}})
  create_variable({collection: "VariableCollectionId:1:2", name: "Spacing/desktop", type: "FLOAT", values_by_mode: {Desktop: 24}, mode_coverage_required: "opt-in-fallback", fallback_reason: "Desktop-only metric; fallback to Desktop in Mobile mode."})
- **delete_collection** — Delete a VariableCollection. Cascades — all variables in the collection are removed too, and any node bindings to those variables become unbound (the node keeps its concrete value at deletion time).

Returns: {ok: true, removedVariables: <count>}.

Examples:
  delete_collection({collection: "VariableCollectionId:1:2"})
- **delete_variable** — Delete a single Variable. Any node bindings to it become unbound (the node keeps its concrete value at deletion time). The parent VariableCollection survives even if this was its last variable — use delete_collection separately to remove the collection.

Returns: {ok: true}.

Examples:
  delete_variable({variable: "VariableID:1:5"})
- **set_variable_value** — Set a variable's value for a specific mode. Thin wrapper over Figma's variable.setValueForMode(modeId, value) — call once per mode.

Use when:
- Filling mode values after create_variable
- Updating one value in one mode without touching others
- Setting an alias from one variable to another

Returns: { data: { ok: true } }

Accepted value forms:
  COLOR    — "#RRGGBB" hex string OR {r, g, b, a?} in 0-1 range
  FLOAT    — number (numeric string also accepted)
  STRING   — string
  BOOLEAN  — true / false
  alias    — {type: "VARIABLE_ALIAS", id: "VariableID:x:y"}   (works for any type, links to another variable)

Skip when:
- Creating a variable with values across all modes from scratch — use create_variable with values_by_mode (one call vs N)
- You only know the mode NAME, not modeId — list_variables / create_collection return modeIds; or use create_variable which accepts mode names
- Same value across all modes — still call once per mode; this tool doesn't broadcast

Examples:
  set_variable_value({variable: "VariableID:1:5", mode: "1:0", value: "#FFFFFF"})           // COLOR hex
  set_variable_value({variable: "VariableID:1:5", mode: "1:1", value: {r: 0.1, g: 0.1, b: 0.1, a: 1}})  // COLOR object
  set_variable_value({variable: "VariableID:1:6", mode: "1:0", value: 16})                  // FLOAT
  set_variable_value({variable: "VariableID:1:7", mode: "1:0", value: {type: "VARIABLE_ALIAS", id: "VariableID:1:9"}})  // alias
- **bind_variable** — Bind a FLOAT, BOOLEAN, or STRING variable to a node property. COLOR variables go through set_fill / set_stroke / jsx instead — see Skip when.

prop is a flat Figma bindable field (e.g. fontSize, itemSpacing, paddingTop, cornerRadius, opacity, visible, width, height, characters). Shorthands accepted: gap → itemSpacing, padding → paddingTop, corner → cornerRadius, font-size → fontSize.

Use when:
- Wiring a numeric token (spacing, radius, fontSize) to a node so it tracks the design system
- Toggling a BOOLEAN visibility via a global variable
- Binding STRING content (e.g. characters) to a localized text variable

Returns: { data: { message, nodeId, variableId } } on success.

May carry warnings:
  MODE_FALLBACK — variable lacks a value for one of the node's reachable modes; Figma falls back to default mode along the mode chain.

Inline errors (LLM can self-correct from these):
  MISSING_MODE_VALUES — variable missing required modes. Response `data.recommended_next_action` carries a ready-to-call create_variable with the missing modes populated.

Skip when:
- Variable is COLOR — use set_fill({node, bg: "$VarName"}), set_stroke({node, color: "$VarName"}), or specify fill/bg="$VarName" at jsx creation
- Target prop is a Paint (fills, strokes) — same as COLOR above
- Variable missing required modes — call create_variable first to fill them, or expect MISSING_MODE_VALUES

Mode selection tip: when the node is a Tablet/Mobile variant (name or variant property contains "Tablet"/"Mobile"), match its property value against the Tablet/Mobile mode column from list_variables — not Desktop.

Examples:
  bind_variable({node: "1:2", prop: "fontSize", variable: "VariableID:1:6"})
  bind_variable({node: "1:3", prop: "paddingTop", variable: "VariableID:1:7"})
  bind_variable({node: "1:4", prop: "visible", variable: "VariableID:1:8"})
  bind_variable({node: "1:5", prop: "characters", variable: "VariableID:1:9"})

Bindable properties (full set):
  FLOAT  — fontSize, letterSpacing, lineHeight, paragraphSpacing, paddingTop/Right/Bottom/Left, itemSpacing (gap), counterAxisSpacing, cornerRadius (+ per-corner topLeftRadius/topRightRadius/bottomLeftRadius/bottomRightRadius), opacity, width, height, strokeWeight (+ per-side strokeTop/Right/Bottom/LeftWeight).
  STRING — characters (text content; drives i18n EN/CN modes), fontFamily, fontStyle.
  BOOLEAN— visible (e.g. hide nav links on mobile), locked, clipsContent, layoutPositioning.

Critical rules:
- characters binding vs set_text are mutually exclusive — LAST WRITE WINS. Binding prop:"characters" makes text auto-render on mode switch; a later set_text({text:"literal"}) CLEARS the binding. For variable-driven text, never set literal text after binding — fix content via set_variable_value on the variable, not the node.
- Mode-aware matching: for a Tablet/Mobile variant (name/variant contains "Tablet"/"Mobile"), match the node value against THAT mode's column, not Desktop. Safe if |mode_value − node_value| / node_value < 20%; above that, confirm with the user first.
- One-way flow: token → binding is one-directional. Changing a token's value after nodes bind to it cascades pollution with no undo. To fix a mismatch, rebind to a different/new token — never edit a bound token's value.
- Mass binding: print the planned node→token mapping with each match's diff %, then inspect after binding to verify (binding overwrites the original value permanently).
- To switch a node/page to a mode (Dark, Mobile, CN), use set_variable_mode — modes are set on the consumer node, not the variable.
- **set_variable_mode** — Set a node to use a specific mode of a variable collection.

This controls which variable values the node displays. For example, set a frame
to use "Dark" mode of the "Theme" collection so all bound variables show dark values.

Examples:
  set_variable_mode({node: "1:2", collection: "VariableCollectionId:1:2", mode: "1:1"})
  set_variable_mode({node: "1:5", collection: "VariableCollectionId:1:3", mode: "1:2"})
- **create_component** — Convert a FRAME or GROUP into a reusable COMPONENT in place. The new component inherits the original's children, auto-layout, fills, strokes, and effects. The original frame is removed; use the returned nodeId for subsequent references.

Use when:
- A finished frame will be reused (button, card, list item) and needs an instance master
- Before create_instance — instances require a COMPONENT, not a FRAME
- Promoting a one-off layout into a reusable building block

Returns: { data: { message: "Converted ...", nodeId: "1:5" } }

Idempotent: if the node is already a COMPONENT, returns success with the existing nodeId — no error, no duplicate.

Skip when:
- Source is TEXT, VECTOR, or INSTANCE — only FRAME/GROUP convert; clone first if needed
- Building from scratch — use jsx() with the component as a unit when reuse is known up front
- You need multi-variant — convert each variant individually, then combine_components

Examples:
  create_component({node: "1:2"})
- **combine_components** — Combine 2+ existing COMPONENTs into a COMPONENT_SET (variant set). The new set takes over the layout slot of the first input's parent; the input components become its children (the variants). Sensible defaults applied: horizontal wrap layout, 24px gap, 20px padding.

This is the ONLY way to create variant axes (Size, State, Theme). Figma derives each axis from the variant component names in "Axis=Value" form. Pass the `variants` mapping and the handler names each component for you (e.g. {Size:"Small", State:"Default"} → "Size=Small, State=Default"); all entries must share the same axis keys. Omit `variants` only if the components are already named in "Axis=Value" form — otherwise you get a single junk axis named "Property 1".

To build a variant set: create one COMPONENT per combination (create_component on frames, or jsx), then combine_components with `variants`.

Use when:
- You have ≥2 COMPONENTs that vary along one or more axes (Size, State, Theme) and need a typed variant set
- Per-instance variation (label text, icon swap, on/off) → use add_component_prop with TEXT/BOOLEAN/INSTANCE_SWAP/SLOT instead (NOT a variant axis)

Returns: { data: { message, nodeId: "1:5", variants: ["Size=Small", "Size=Large"] } }

Skip when:
- Inputs are FRAMEs — run create_component on each first
- The variation is per-instance rather than per-component shape — use add_component_prop

Examples:
  // Single axis (Size), handler names the children:
  combine_components({nodes: ["1:2", "1:3", "1:4"], name: "Button", variants: [
    {node: "1:2", props: {Size: "Small"}},
    {node: "1:3", props: {Size: "Medium"}},
    {node: "1:4", props: {Size: "Large"}}
  ]})
  // Two axes (Size × State):
  combine_components({nodes: ["1:2", "1:3"], name: "Button", variants: [
    {node: "1:2", props: {Size: "Small", State: "Default"}},
    {node: "1:3", props: {Size: "Small", State: "Hover"}}
  ]})
  // Components already named "Axis=Value" — no mapping needed:
  combine_components({nodes: ["1:2", "1:3"], name: "Button"})
- **add_component_prop** — Add a component property to a COMPONENT or COMPONENT_SET. Four types:

  TEXT          — overridable string per instance (button label). Auto-binds to a text child via "bind".
  BOOLEAN       — show/hide a layer per instance (icon toggle). Auto-binds to a node's visibility.
  INSTANCE_SWAP — swap a nested instance per instance (icon family, avatar). Auto-binds to an instance child. Optional "preferredValues" suggests swap candidates.
  SLOT          — open slot filled at instance use site (composable Card body, Modal content). Optional "description" hints what belongs in the slot; optional "preferredValues" suggests content. No "bind" (SLOT is not tied to one child).

VARIANT axes (Size, State, Theme) are NOT added here. Figma derives variant axes from the names of the variant components — create one COMPONENT per combination and use combine_components with a `variants` mapping. Calling this tool with type:"VARIANT" returns a redirect.

Auto-binding (TEXT/BOOLEAN/INSTANCE_SWAP): if "bind" omitted, the handler walks the component's children to match by name. SLOT skips binding (filled by consumers).

preferredValues is `{type, key}[]` where `type` is "COMPONENT" or "COMPONENT_SET" and `key` is the PUBLISHED component key (component.key) — NOT a scene-graph node id.

Use when:
- Building a reusable component whose instances need per-use variation (label, icon, on/off state)
- Defining a composable slot a consumer can fill (Card body, Modal content)

Returns: { data: { message, nodeId, property, bound: boolean } }

`bound` is false if auto-binding couldn't find a matching child — the property is still created, but instances won't override anything until you rerun with explicit "bind".

Skip when:
- Source is a FRAME — run create_component first; properties require COMPONENT or COMPONENT_SET
- You want a VARIANT axis (Size, State) — use combine_components instead (see above)
- Variation is one-off — edit the instance directly instead of templating

Examples:
  add_component_prop({node: "1:2", name: "Label", type: "TEXT", default: "Click me", bind: "1:5"})
  add_component_prop({node: "1:2", name: "Show Icon", type: "BOOLEAN", default: "true"})
  add_component_prop({node: "1:2", name: "Icon", type: "INSTANCE_SWAP", default: "1:99", preferredValues: [{type: "COMPONENT", key: "abc123"}]})
  add_component_prop({node: "1:2", name: "Footer", type: "SLOT", description: "Replace card footer"})
- **list_component_props** — List the properties (and variants, where applicable) of a COMPONENT, COMPONENT_SET, or INSTANCE.

Use when:
- Discovering which properties an existing component exposes before calling edit() or add_component_prop
- Checking which variants exist in a COMPONENT_SET before making a new instance
- Inspecting an INSTANCE's current property overrides

Returns: { data: { listing: <multi-line formatted string> } }

The listing is a human-readable text block (not structured JSON). Shape varies by node type:
- COMPONENT_SET: name, variant count, variant names, properties table (type / name / default)
- COMPONENT: name, id, properties table, parent set name (if part of one)
- INSTANCE: main component name + current property overrides

Skip when:
- Target is not a component/instance — use inspect() for arbitrary nodes
- You already listed this component's props earlier this turn — cache the listing, re-listing wastes an iteration
- You need the full subtree structure — use inspect() with facets

Examples:
  list_component_props({node: "1:2"})
- **create_instance** — Create an instance of an existing component. Mutates the canvas — appends a new InstanceNode as the last child of `parent` (or the active page root if omitted). The instance is LINKED to the component master, so future component edits propagate. Returns the new instance's nodeId.

Use when:
- Spawning runtime copies of a Component master (buttons, list items, cards)
- Reusing a design-system component in a fresh layout
- Programmatic instantiation outside a jsx() tree-build

Returns: { data: { message, nodeId: "5:42", componentId: "1:2" } }

Parameters beyond schema:
- `node` must be a COMPONENT or COMPONENT_SET. For a COMPONENT_SET, the default variant is instantiated unless you pass `variant`. Discover IDs with find_nodes({ type: "COMPONENT" }).
- `parent` optional. If parent is auto-layout, the instance enters the flow and inherits sizing rules. If omitted, the instance is placed at the active page root with detached position — may overlap existing content; set explicit position with edit afterwards.
- `variant` optional. For a COMPONENT_SET, pick the variant by axis values, e.g. {Size:"Large", State:"Hover"}. Values must match the set's axis options (see list_component_props).
- `props` optional. Override component properties by display name (the names from list_component_props, without the internal #suffix), e.g. {Label:"Submit", "Show Icon":"false"}. BOOLEAN props accept "true"/"false". Applied via setProperties; an unknown name or invalid variant value returns an error listing the valid options.

Skip when:
- Duplicating a non-component node — instance creation will fail; use clone_node instead.
- Building a subtree from scratch — use jsx with <instance ref="ComponentName"/> for atomic single-call construction.

Examples:
  create_instance({node: "1:2"})                                          // default, at page root
  create_instance({node: "1:2", parent: "1:4"})                           // inside frame 1:4
  create_instance({node: "1:2", variant: {Size: "Large", State: "Hover"}}) // pick a variant of a set
  create_instance({node: "1:2", props: {Label: "Submit", "Show Icon": "false"}}) // override props
- **edit_component_prop** — Edit an existing component property on a COMPONENT or COMPONENT_SET: rename it, change its default, or update preferred values / description. Cannot change a property's TYPE (delete + re-add to retype).

Per the Figma API:
- newName — supported for all types (TEXT/BOOLEAN/INSTANCE_SWAP/VARIANT/SLOT)
- default — BOOLEAN/TEXT/INSTANCE_SWAP only (NOT VARIANT or SLOT)
- preferredValues — INSTANCE_SWAP/SLOT only; {type, key}[] with published component keys
- description — SLOT only

`name` is the current display name from list_component_props (no internal #suffix). Provide at least one of newName/default/preferredValues/description.

Use when:
- Renaming a property after the fact (e.g. "Icon" → "Leading Icon")
- Changing a default (different default label/state/swap target)
- Adjusting INSTANCE_SWAP/SLOT preferred candidates

Returns: { data: { message, nodeId, property: <new display name> } }

Skip when:
- You want to change the property's type — delete_component_prop then add_component_prop
- Renaming a VARIANT axis value — that comes from variant component names (recombine)

Examples:
  edit_component_prop({node: "1:2", name: "Icon", newName: "Leading Icon"})
  edit_component_prop({node: "1:2", name: "Label", default: "Submit"})
  edit_component_prop({node: "1:2", name: "Icon", preferredValues: [{type: "COMPONENT", key: "abc123"}]})
- **delete_component_prop** — Delete a component property from a COMPONENT or COMPONENT_SET. Supports BOOLEAN, TEXT, INSTANCE_SWAP, and SLOT — NOT VARIANT (Figma restriction; a variant axis is removed by recombining the set without it).

`name` is the property's display name from list_component_props (no internal #suffix).

Use when:
- Removing a property that's no longer needed (cleanup, redesign)
- Undoing an add_component_prop mistake

Returns: { data: { message, nodeId } }

Skip when:
- Deleting a VARIANT axis — recombine the variant components without that axis instead
- You only want to rename or change its default — use edit_component_prop

Examples:
  delete_component_prop({node: "1:2", name: "Show Icon"})
  delete_component_prop({node: "1:2", name: "Footer"})
- **expose_nested_instances** — Expose the primary nested instances inside a COMPONENT or COMPONENT_SET so their own component properties surface on the parent — instances of the parent can then configure the nested instance's props directly (e.g. a Card exposing its nested Button's Label/State).

Walks the component's direct primary instances and sets isExposedInstance=true on each. Only PRIMARY instances (directly inside the component, not nested within another instance) are exposable — others are skipped.

Use when:
- A component contains reusable sub-component instances whose props should be configurable from the parent (Card → nested Button, List → nested Row)

Returns: { data: { message, nodeId, exposed: [names] } }

Skip when:
- The component has no nested INSTANCE children
- You want per-instance overrides only (set those on each instance via create_instance props)

Examples:
  expose_nested_instances({node: "1:2"})
- **set_text** — Set text content on one or more TEXT nodes. For text styling (font, size, weight, color), use edit.

Use when:
- Changing what a TEXT node says — labels, titles, button text, body copy
- Batch-updating multiple text nodes in one call

Returns: { data: { id, name, type, applied?, noop?, rejected? } } (Batch: { data: { count, results, errors?, partial? } }).
`applied`/`noop` list prop keys; `rejected: [{key, reason}]` is TERMINAL (prop invalid for this node type / readonly) — do not retry it. If the only requested prop is rejected, returns `{ error }`.

`partial: true` means some entries succeeded and some failed — check `errors[]` for which entries to retry.

Skip when:
- You want to change font/size/weight — use edit({node, props: {fontFamily, fontSize, ...}})
- You want to bind text to a STRING variable — use bind_variable

Examples:
  set_text({node: "1:2", text: "Sign in"})
  set_text({nodes: [{node: "1:2", text: "A"}, {node: "1:3", text: "B"}]})
- **set_fill** — Set fill or background color on a node.

`fill` = text color or shape fill. `bg` = frame background. For stroke color, use `set_stroke`.

Each call (single or batch item) needs `node` plus at least one of `fill`/`bg`. Batch by passing `nodes: [{node, fill?, bg?}, ...]`.

Accepted color formats (for fill or bg):
  hex                "#FFF", "#F5F5F5"
  gradient string    CSS-like subset, not full CSS:
                       "linear-gradient(<angle>deg, <#hex> <pos>%, ...)"
                       "linear-gradient(to <direction>, ...)"   directions: top/right/bottom/left + corners
                       "radial-gradient(<stops>)"               centered, no position/shape modifiers
                       "radial-gradient(circle, <stops>)"       circle shape only
                       "conic-gradient(from <angle>deg, ...)"
                     Rejected: "circle at X% Y%", "ellipse at ...", named colors, hsl().
  variable token     qualified bare name "$Surface/Card"
  transparent        "transparent" (bg only)
- **set_stroke** — Set stroke (border) on a node.

  set_stroke({node: "1:2", stroke: "1 #E0E0E0"})
  set_stroke({node: "1:2", stroke: "2 #333 inside"})
  set_stroke({node: "1:2", color: "#E0E0E0", weight: 1, align: "inside"})
  set_stroke({node: "1:2", color: "linear-gradient(90deg, #8B5CF6 0%, #F97316 100%)", weight: 1.5, align: "inside"})

  // Batch — bulk stroke update in one call:
  set_stroke({nodes: [{node: "1:2", color: "#E0E0E0", weight: 1}, {node: "1:3", color: "#333", weight: 2}]})

Shorthand: "weight color align" (e.g. "1 #E0E0E0 inside"). Hex only in shorthand.

Accepted color formats (for the explicit `color` field, not the shorthand):
  hex                "#E0E0E0"
  gradient string    CSS-like subset (see set_fill description for full grammar — same rules).
                     Common: "linear-gradient(<angle>deg, <#hex> <pos>%, ...)", "radial-gradient(<stops>)".
                     Rejected: "circle at X% Y%", named colors, hsl().
  variable token     qualified bare name "$Border/Default"

To bind a variable to the stroke color, use the explicit `color` field — the shorthand parser silently drops bare-name tokens.
- **set_layout** — Set auto-layout (flex or grid) on a container. Controls direction, spacing, padding, and alignment of the container's children.

Modes:
  row / column   — flex layout; use justify / align for axis alignment
  grid           — grid layout; cols/rows + gap (or rowGap/colGap). Per-track sizing: colSizes/rowSizes
                   ("240px 1fr 1fr" — Npx=fixed, Nfr=flexible share, hug=fit-content; infers cols/rows).
                   autoFlow=true packs children row-major; autoRows=true auto-manages the row count.
                   Per-child placement (colSpan/rowSpan/alignX/alignY/rowStart/colStart) is set via edit(), not here.

Use when:
- Setting up auto-layout on an existing frame (gap, padding, direction, alignment)
- Switching a container between row/column/grid
- Batch-tuning layout across multiple containers in one call

Returns: { data: { id, name, type, applied?, noop?, rejected? } } (Batch: { data: { count, results, errors?, partial? } }).
`applied`/`noop` list prop keys; `rejected: [{key, reason}]` is TERMINAL (prop invalid for this node type / readonly) — do not retry it. If the only requested prop is rejected, returns `{ error }`.

Skip when:
- Container is not a FRAME or COMPONENT — only frame-like containers hold auto-layout
- You only need to change ONE child's size — use edit({node, props: {w, h, ...}})
- You're building a container from scratch — pass layout attributes inline in jsx() instead of a follow-up call

Examples:
  set_layout({node: "1:2", gap: 16, p: 24})
  set_layout({node: "1:2", layout: "row", justify: "space-between"})
  set_layout({node: "1:2", layout: "column", gap: 8, p: "16 24", align: "center"})
  set_layout({node: "1:2", layout: "grid", cols: 3, rows: 2, gap: 16})
  set_layout({node: "1:2", layout: "grid", colSizes: "240px 1fr", gap: 24})   // fixed sidebar + flexible content
  set_layout({node: "1:2", layout: "grid", cols: 4, autoFlow: true, gap: 12}) // auto-flowing gallery
  set_layout({nodes: [{node: "1:2", gap: 16, p: 24}, {node: "1:3", gap: 8, p: 12}]})
- **get_selection** — Get the user's currently selected nodes in Figma.

Returns node names, types, and IDs of selected elements.
Call this when the user's intent involves modifying existing elements:
- "change this button", "update the card", "fix the spacing"
- References to "this", "the selected", "it"

Skip for fresh design requests ("design a login page", "create a dashboard") — a new canvas has no selection to read, so the call returns nothing and burns an iteration.

Examples:
  get_selection()
- **create_page** — Create a new top-level page in the file. Pages are independent canvases — useful for isolating exploratory work, scratch space, or separate feature areas without polluting the current page.

By default the new page is created but NOT activated — pass switchTo:true to also make it the current page (otherwise subsequent operations still target the previous page).

Returns: {id, name} of the created page.

Examples:
  create_page({name: "Scratch"})                    — create, stay on current page
  create_page({name: "MCP Tests", switchTo: true})  — create AND switch
- **delete_page** — Delete a page by ID. ALL nodes on the page are removed. Figma requires at least one page — attempting to delete the only page errors out.

If the target page is currently active, Figma auto-switches to another page first; the response includes the new current page id/name so callers can update their cursor.

Returns: {id, name, newCurrentPageId?, newCurrentPageName?}.

Examples:
  delete_page({pageId: "1:23"})

Use switch_page({}) first to discover IDs.
- **switch_page** — Navigate between pages in the Figma file. ID-driven — names are not addressable (they can collide and change).

Two modes:
- switch_page({})              → return the page roster only, no switch (use to discover IDs on first call)
- switch_page({pageId: "1:23"})  → switch and return the updated state + roster

Pages are top-level containers under the file root. Most read/write operations default to figma.currentPage. Call this when you need to operate on nodes that live on a different page than the current one.

Returns:
- currentPageId, currentPageName — the now-current page (always present)
- pages — full roster [{id, name}] of every page in the file (always present)
- previousPageId, previousPageName — what you switched from (only when an actual switch happened)
- unchanged — true if target was already current

Typical flow:
1. switch_page({})                         // get IDs
2. switch_page({pageId: "<picked id>"})    // switch

When to call:
- User mentions content on a different page than the current one
- A previous tool reported a node ID is on a non-current page
- You need to inspect/modify nodes outside the active page

Don't call:
- For nodes already on the current page — figma.currentPage is the default scope, this would just waste an iteration
- Repeatedly to "explore" — every call returns the full pages roster, cache it
- **create_vector** — Create a vector node from SVG path data or a list of points. Use for chart lines, custom icon paths, freeform curves, or any shape that needs path data.

Examples:
  // Polyline (chart trend line)
  create_vector({
    parent: "1:23", name: "TrendLine",
    x: 40, y: 20, width: 550, height: 240,
    points: [[0,144],[90,96],[180,120],[270,64],[360,80],[450,40],[540,72]],
    stroke: "#6366F1", strokeWeight: 2
  })

  // Raw SVG path (custom shape)
  create_vector({
    parent: "1:23", name: "Wave",
    width: 200, height: 60,
    data: "M 0 30 Q 50 0 100 30 T 200 30",
    stroke: "linear-gradient(90deg, #8B5CF6 0%, #F97316 100%)",
    strokeWeight: 1.5
  })

Path input — provide ONE of:
  points: [[x,y], ...]   compiled to "M x0 y0 L x1 y1 ..." (polyline shortcut)
  data:   "M ... L ..."  raw SVG path (LLM-native; supports M, L, C, Q, A, Z)

Stroke / fill (same formats as set_stroke / set_fill):
  hex            "#6366F1"
  gradient       "linear-gradient(angle, #color stop%, ...)"
  variable       qualified bare name "$Brand/Primary"

Default fill is "transparent" so the vector shows only its stroke. Pass an explicit fill if you want it filled.

When NOT to use:
  - Standard rectangles / ellipses / lines — use jsx <Rect/>, <Ellipse/>, <Line/> elements (simpler, batch-friendly)
  - Existing vector edits — use edit / set_stroke instead
- **get_screenshot** — Capture a PNG screenshot of a node.

Use after style changes to visually verify the result instead of reading properties back.
Returns base64 PNG data embedded in the response.

Parameters:
  node: Node ID from jsx/inspect results (e.g. "100:5"). Page root ("/") is not supported.
  scale: Export scale 0.5–2 (default 1). Higher = larger file.
  padding: Reserved for future use — currently ignored.

Examples:
  get_screenshot({node: "100:5"})              → PNG at 1x
  get_screenshot({node: "100:5", scale: 2})    → PNG at 2x (sharper)
- **reconcile_preview** — READ-ONLY reconcile dry-run. Given an existing subtree (scope) and a target JSX (markup), computes the minimal create/update/delete/move plan that would turn the live subtree into the target — preserving the id of every matched node — and PRINTS it without touching the canvas. Use it to preview how a declarative edit would be reconciled. Workflow: read_jsx({node}) to get the current subtree → edit that JSX → reconcile_preview({scope: same id, markup: your edited JSX}). Nothing is applied.
- **reconcile_apply** — Apply a declarative edit to an existing subtree, PRESERVING the id of every node you keep (so variable bindings, instance overrides and prototype links survive) — the id-preserving alternative to delete+rebuild. Workflow: read_jsx({node}) → edit that JSX (keep id= on kept nodes, drop id= on new ones, re-nest freely) → reconcile_apply({scope: same id, markup: edited JSX}). Matching is by explicit id only: an id= node is updated/moved in place (re-parenting = move, id kept); a node with no id is created. Props are a PATCH — only props in the markup are written, absent props are left unchanged (never cleared). Existing nodes not in the markup are KEPT unless allowDelete:true. Preview with reconcile_preview first to see the plan.

## Claude Desktop Quick Installation
Install path detected from listing signals. Uses `npx` (confidence: high):

```json
"mcpServers": {
  "genable": {
    "command": "npx",
    "args": ["-y","genable-mcp"]
  }
}
```

## Documentation

## What musepy/genable MCP server does

musepy/genable MCP server gives an MCP client write access to native Figma structure through the Genable plugin. It is intended for workflows where an agent creates or refines editable frames, text, vectors, components, instances, variables, and pages instead of returning a single rendered image.

The central creation operation is `jsx`. It accepts a JSX-like design dialect with nested elements such as frames, text, icons, images, components, groups, sections, and vectors. A call can create a complete subtree atomically. Existing subtrees can also be replaced when the markup has one root and a replacement node is supplied.

## How it works

The MCP package acts as a local plugin-backed bridge. Keep the Genable plugin running in Figma desktop, configure an MCP client to run the package over STDIO, and issue design operations through the client. The plugin communicates with the Figma scene graph, allowing the agent to create structure, read it back, inspect selected properties, and continue refining the same canvas.

Reading is deliberately split across several tools. `inspect` returns a skeleton by default and can expose selected facets such as layout, paint, typography, variables, effects, or lint results. `read_jsx` returns a canonical, line-numbered representation that can be edited with `edit_jsx`. Search, cloning, moving, deletion, property discovery, and exact bulk replacement are also available.

## Setup and configuration

Install the package in an MCP client with:

```json
{
  "mcpServers": {
    "genable": {
      "command": "npx",
      "args": ["-y", "genable-mcp"]
    }
  }
}
```

After installation, start the Genable plugin in Figma desktop and ask the client to list pages in the current file. The package documentation provides client-specific setup and the current tool schema. The MCP route uses the model configured in the client; it does not require a separate model API key inside Genable.

The Figma plugin itself supports OpenAI-compatible, Anthropic-compatible, and Gemini provider connections, but those provider settings apply to the plugin workflow. Provider billing and usage limits remain separate from the MCP package.

## Tools and capabilities

musepy/genable MCP server covers several design-system and editing tasks:

- Create nested layouts with JSX, including auto-layout, grid layout, text formatting, gradients, effects, and token references.
- Inspect nodes selectively, validate subtrees with lint information, search by name or type, and verify the canvas visually with screenshots.
- Change text, fills, strokes, layout, sizing, effects, component overrides, and other properties through focused setters or the general `edit` tool.
- Read and patch canonical JSX, while preserving node IDs for edits that target existing structure.
- Create, update, bind, inspect, and remove variables and variable collections, including collection modes and aliases.
- Promote frames or groups to components, combine components into variant sets, create instances, and manage text, boolean, instance-swap, and slot properties.
- Move, clone, delete, and rename nodes, and apply exact property replacements across a subtree.

## Limitations and notes

The bridge depends on the Genable plugin being active in Figma desktop. `edit_jsx` updates existing nodes whose IDs are preserved; it does not create missing children, delete omitted children, or reparent nodes. Use `jsx`, `delete_node`, or `move_node` for those operations.

Some operations are intentionally destructive. Bulk property replacement has no preview or multi-node undo, and changing a bound variable can affect connected nodes without undo. Inspect or discover values before broad replacements, and use variable-binding operations rather than exact property replacement when working with tokens.

Color variables use fill, stroke, or JSX token syntax, while `bind_variable` is for FLOAT, BOOLEAN, and STRING properties. A later literal text update can clear a string-variable binding. Gradient input also follows a limited CSS-like grammar; named colors, HSL values, and several positioned gradient forms are rejected.

_Full upstream README: https://allmcps.com/mcp/musepy-genable/readme_

