# io.github.musepy/genable-mcp (npm · genable-mcp)

Write to Figma from any MCP client (Claude Code, Cursor). Complements the read-only official MCP.

- Trust score: 64/100 (medium)
- Change this week: +22
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-08-03

## Components

- npm · `genable-mcp`: 64/100 (this document), [markdown](https://verifymcp.io/servers/musepy-genable-mcp/genable-mcp.md), [page](https://verifymcp.io/servers/musepy-genable-mcp/genable-mcp)

## Channel facts

- Registry: `npm`
- Package: `genable-mcp`
- Version: `0.1.3`
- Transport: `stdio`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. Scores are 0–100 per category. Scoring method: https://verifymcp.io/docs/scoring (what has changed: https://verifymcp.io/docs/scoring/changelog)

Scored 2026-08-03.

- **Supply Chain Security**: 87/100
  - No malware found by supply-chain analysis.
  - Only part of the dependency tree could be resolved (95 of 99), so this covers what we could see, not the whole tree.
  - No install/post-install scripts declared.
  - Only part of the dependency tree could be resolved (95 of 99), so this covers what we could see, not the whole tree.
- **Provenance & Transparency**: 45/100
  - Source repository is publicly reachable at the declared URL.
  - Provenance check failed: no build-provenance attestation is published.
  - Clear OSI-approved license (MIT).
  - Actively maintained (last published 59 days ago).
  - Disclosure check failed: no security disclosure policy was found in the source repository.
- **Schema Quality & AI Usability**: 53/100
  - AI-judged instruction clarity (good).
  - Context-footprint check failed: tool/resource definitions use about 11678 tokens (~284/item across 41 items; 41 tools + 0 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 23/100
  - Stability observed for 7 of 30 days with no destabilising changes; credit accrues until the full window elapses.
- **Tool Coverage**: 100/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 100% of tool parameters carry a description.
- **Capabilities**: 100/100
  - Implements a supported MCP spec version (2025-11-25); the latest is 2026-07-28.

## Install

### Claude

```bash
claude mcp add musepy-genable-mcp -- npx -y genable-mcp
```

### Codex

```bash
codex mcp add musepy-genable-mcp -- npx -y genable-mcp
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "musepy-genable-mcp": {
      "type": "local",
      "command": [
        "npx",
        "-y",
        "genable-mcp"
      ],
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add musepy-genable-mcp --command npx --arg -y --arg genable-mcp
```

### Hermes

```yaml
mcp_servers:
  musepy-genable-mcp:
    command: "npx"
    args: ["-y", "genable-mcp"]
```

### Other

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

## Changelog

Every change recorded for this component, newest first. Days that predate change tracking, or that we cannot explain, say so: "we were watching and nothing happened" and "we were not watching" are different claims.

### 2026-08-03 (score 64, +32)

- [security regression] Provenance: unverified → fail
- [security improvement] Install scripts: unverified → pass
- [security improvement] Known CVEs: unverified → partial
- [functional improvement] License: unverified → pass
- [functional improvement] Stability: unverified → 0.23
- [functional improvement] MCP protocol: unverified → pass
- [functional improvement] Maintenance: unverified → pass
- [functional improvement] Dependency health: unverified → partial
- [functional improvement] Schema quality: unverified → good
- [functional] Licence: MIT

### 2026-08-02 (score 32, +10)

- [security improvement] Malware scan: unverified → pass
- [security] Stability: Stability not yet verified: we do not have a sandbox capture of the MCP schema this version of the package serves yet.
- [functional regression] Capabilities: pass → unverified

### 2026-08-01 (score 22, +5)

- [security] Stability: Stability not yet verified: not enough scan history yet (needs a 30-day window).
- [functional improvement] MCP protocol: unverified → pass

### 2026-07-31 (score 17, −7)

- [functional] We updated how we score, so this day's move reflects our rubric, not a change to the server

### 2026-07-30 (score 24, −18)

- [functional regression] Tool coverage: 100 → unverified
- [functional] First check of Schema quality: unverified

### 2026-07-27 (score 42)

First indexed and scored.

## MCP tools (41)

### `jsx` (~939 tokens)

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 (text): size, weight, lineHeight, font, fill, w (w="fill" for wrap), maxLines, textTruncation
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%", "ellip…

Input parameters:

- `insertIndex` (number): Position among parent siblings (0 = first). Omit to append at end. Ignored when replaceId is set (inherits old node index).
- `markup` (string, required): JSX-like nested markup string
- `parent` (string): Target parent node ID — the frame/container the new subtree will live inside
- `replaceId` (string): Replace this existing node in-place (keeps parent + sibling index). Old node is deleted on success. Markup must be single-root. Mutually exclusive with parent.

### `inspect` (~424 tokens)

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)
  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).

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

Use `get_screenshot` for visual verification. Use `describe` for lint/validation.

Input parameters:

- `depth` (number): Max depth (default: 5, max: 10)
- `facets` (array): Property buckets to surface. Omit for a skeleton-only response.
- `node` (string, required): "/" for page root, or node ID (e.g. "100:5").

### `describe` (~184 tokens)

Validate a design subtree — semantic description, role detection, and lint rules. Checks for layout conflicts, overflow, missing properties, and structural issues.

Parameters:
  node: Node ID to describe (e.g. "100:5"). Required.
  depth: How deep to check children (default: 3, max: 8).

Returns per-node: role, visual summary, layout summary, and issues (severity: error/warning/info).

Examples:
  describe({node: "100:5"})             → validate subtree, depth 3
  describe({node: "100:5", depth: 1})   → shallow check (root + direct children only)

Input parameters:

- `depth` (number): Max depth to check (default: 3, max: 8)
- `node` (string, required): Node ID to describe (e.g. "100:5").

### `edit` (~320 tokens)

Batch update properties on multiple nodes.

For single-property changes, prefer focused setters:
  set_text  — text content
  set_fill  — fill/background color
  set_stroke — border
  set_layout — padding, gap, direction

Use edit for batch fixes or properties not covered by setters (sizing, radius, opacity, effects, component props):
  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)
  ]})

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

Input parameters:

- `content` (string): New text content (single mode)
- `node` (string): Node ID (e.g. "1:2") from jsx/inspect results
- `nodes` (array): Batch: array of {node, props?, content?} objects. No hard item cap — real ceiling is LLM output stream length (~10KB+ of rendered params can stall mid-JSON). If a batch is large and props are rich, s…
- `props` (object): Properties to update (single mode)

### `find_nodes` (~103 tokens)

Search nodes by name or type. Scoped to the current page — call switch_page first if your target lives on a different page.

Examples:
  find_nodes({query: "Button"})
  find_nodes({query: "frame", scope: "1:2"})

Input parameters:

- `query` (string, required): Search query — matches node name or type
- `scope` (string): Limit search to subtree. Node ID (e.g. "1:2"). Default: entire page.

### `discover_props` (~100 tokens)

Discover unique property values in a subtree.

Examples:
  discover_props({node: "1:2", props: ["fillColor", "fontSize"]})

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

Input parameters:

- `node` (string, required): Target node ID (e.g. "1:2")
- `props` (array, required): Properties to discover

### `replace_props` (~446 tokens)

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"}
  ]})

Input parameters:

- `node` (string, required): Target node ID (e.g. "1:2")
- `rules` (array, required): Replacement rules

### `find_references` (~264 tokens)

Find every node on the current page that references a given variable.

This is the REVERSE of inspect: inspect asks "what does this node bind?",
find_references asks "who uses this variable?". Use it when renaming,
auditing, or swapping tokens — you need to know all the binding sites
before you touch the variable.

Scan scope: currentPage only. Invisible nodes are skipped by default.
Node-level bindings (e.g. boundVariables.paddingLeft) and per-paint color
bindings (fills[i].boundVariables.color, strokes[i].boundVariables.color)
are both returned.

Parameters:
  variable  — VariableID (e.g. "VariableID:1:5"). Required.

Returns:
  {variable, variableName, variableType, referenceCount,
   references: [{nodeId, nodeName, nodeType, path}, ...]}

  path values look like:
    "boundVariables.paddingLeft"
    "fills[0].boundVariables.color"
    "strokes[2].boundVariables.color"

Examples:
  find_references({variable: "VariableID:1:5"})

Input parameters:

- `variable` (string, required): VariableID to look up (e.g. "VariableID:1:5"). Get IDs from list_variables.

### `delete_node` (~45 tokens)

Delete a node and its children.

Examples:
  delete_node({node: "1:2"})

Input parameters:

- `node` (string, required): Node ID (e.g. "1:2")

### `move_node` (~237 tokens)

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

Input parameters:

- `index` (number): Reorder position among siblings. 0 = first, -1 = last.
- `name` (string): New name (rename without changing parent)
- `node` (string, required): Node ID (e.g. "1:3") to move/rename
- `parent` (string): Target parent node ID — the frame/container the node should live inside after the call

### `clone_node` (~233 tokens)

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"}})

Input parameters:

- `name` (string): Name for the cloned node. Defaults to source node name.
- `node` (string, required): Source node ID (e.g. "1:2")
- `overrides` (object): Property overrides. Use "Child.prop" for child overrides.
- `parent` (string): Target parent node ID the clone should live inside, or "/" for page root. Defaults to page root.

### `skill` (~88 tokens)

Load a procedural skill — workflow + tool sequence + anti-patterns. Use FIRST when the user is changing/adjusting existing canvas, OR before creating a new design if a matching skill exists.

Example: skill({ name: "restyle" })

Input parameters:

- `name` (string, required): The skill name exactly as it appears in the KNOWLEDGE LIBRARY menu — no "skill:" prefix, no quotes.

### `style` (~102 tokens)

Load a visual style preset as INSPIRATION — color tokens, typography, shape, depth. Treat as a reference library, not a cage: pick one wholesale, mix elements, or invent your own. You are not restricted to the menu.

Example: style({ name: "neon-cyber" })

Input parameters:

- `name` (string, required): The style name exactly as it appears in the KNOWLEDGE LIBRARY menu — no "style:" prefix, no quotes.

### `guideline` (~71 tokens)

Load a page-type design guideline — layout patterns for landing pages, dashboards, login flows, forms, etc.

Example: guideline({ name: "form" })

Input parameters:

- `name` (string, required): The guideline name exactly as it appears in the KNOWLEDGE LIBRARY menu — no "guideline:" prefix, no quotes.

### `help` (~67 tokens)

Load narrow how-to / process help — tool usage rules, edge cases, naming conventions.

Example: help({ name: "interaction-model" })

Input parameters:

- `name` (string, required): The help name exactly as it appears in the KNOWLEDGE LIBRARY menu — no "help:" prefix, no quotes.

### `list_variables` (~224 tokens)

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"})

Input parameters:

- `collection` (string): VariableCollectionId to filter by
- `cursor` (string): Opaque pagination cursor from a previous call
- `filter` (string): Substring match on variable name (case-insensitive)
- `limit` (number): Max variables per page (default 100)

### `create_collection` (~122 tokens)

Create a VariableCollection with named modes.

The first mode in the array becomes the default mode. Returns
{data: {id, modes: [{modeId, name}]}} — 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"]})

Input parameters:

- `modes` (array, required): Mode names (first becomes default)
- `name` (string, required): Collection name

### `create_variable` (~132 tokens)

Create a variable in an existing collection.

No value is set here — use set_variable_value after. Returns {data: {id}}.

Examples:
  create_variable({collection: "VariableCollectionId:1:2", name: "Theme/bg", type: "COLOR"})
  create_variable({collection: "VariableCollectionId:1:2", name: "spacing/md", type: "FLOAT"})

Input parameters:

- `collection` (string, required): VariableCollectionId to create the variable in
- `name` (string, required): Variable name (slashes denote hierarchy in the Figma UI)
- `type` (string, required): Variable type

### `ensure_collection` (~229 tokens)

Idempotent VariableCollection creation — safe to retry.

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

Prefer this over create_collection — re-running with the same name + modes
returns the existing collection instead of creating a duplicate.

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}}.

Examples:
  ensure_collection({name: "Theme", modes: ["Light", "Dark"]})

Input parameters:

- `idempotency_key` (string): Optional. Handler auto-computes canonically if omitted. Pass only if you need strict concurrency-safety validation against the SHA-256 formula in spec §3.1.
- `modes` (array, required): Mode names (first becomes default)
- `name` (string, required): Collection name

### `ensure_variable` (~703 tokens)

Idempotent variable creation — populates values_by_mode in one shot.

Prefer this over create_variable — re-running with the same args returns the
existing variable instead of creating a duplicate.

Behavior (spec §3.1):
  \- Exactly 1 variable with (collection_id, name, type) in target collection → idempotent reuse.
  \- 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_id, 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:
  ensure_variable({collection_id: "VariableCollectionId:1:2", name: "Text/Primary", type: "COLOR", values_by_mode: {Light: "#111", Dark: "#EEE"}})
  ensure_variable({collection_id: "VariableCollectionId:1:2", name: "Spacing/desktop", type: "FLOAT", values_by_mode: {Desktop: 24}, mode_coverage_required: "opt-in…

Input parameters:

- `collection_id` (string, required): Target VariableCollectionId — strict ID, no name lookup.
- `fallback_reason` (string): REQUIRED iff mode_coverage_required="opt-in-fallback". Must contain the structured phrase "fallback to <mode_name>". Persisted on the variable for audit trail.
- `idempotency_key` (string): Optional. Handler auto-computes canonically if omitted. Pass only if you need strict concurrency-safety validation against the SHA-256 formula in spec §3.1.
- `mode_coverage_required` (string): Mode coverage policy — "all" (default; every mode must have explicit value) or "opt-in-fallback" (allow fallback, requires fallback_reason).
- `name` (string, required): Variable name (slashes denote hierarchy).
- `type` (string, required): Variable type
- `values_by_mode` (object, required): Map of mode name OR modeId → value. Hex strings allowed for COLOR.

### `set_variable_value` (~308 tokens)

Set a variable's value for a specific mode.

Thin wrapper over Figma's variable.setValueForMode(modeId, value). Call once per
mode. Value is a raw value (COLOR/FLOAT/STRING/BOOLEAN) OR an alias object
{type: "VARIABLE_ALIAS", id: "VariableID:x:y"}. Hex strings are accepted for
COLOR and normalized to {r,g,b,a} in 0-1 range.

Examples:
  set_variable_value({variable: "VariableID:1:5", mode: "1:0", value: "#FFFFFF"})
  set_variable_value({variable: "VariableID:1:5", mode: "1:1", value: {r: 0.1, g: 0.1, b: 0.1, a: 1}})
  set_variable_value({variable: "VariableID:1:6", mode: "1:0", value: 16})
  set_variable_value({variable: "VariableID:1:7", mode: "1:0", value: {type: "VARIABLE_ALIAS", id: "VariableID:1:9"}})

Input parameters:

- `mode` (string, required): Mode id from the variable's collection (e.g. "1:0")
- `value` (object, required): Raw value (COLOR/FLOAT/STRING/BOOLEAN) or {type: "VARIABLE_ALIAS", id}
- `variable` (string, required): VariableID to set

### `bind_variable` (~390 tokens)

Bind a FLOAT, BOOLEAN, or STRING variable to a node property.

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

COLOR variables are NOT bound here — they live inside Paint objects. To apply
a color token, specify it at the source instead:
  • At creation:  jsx <frame bg="$TokenName" ...> or fill="$TokenName"
  • Post-hoc:     set_fill({node, bg: "$TokenName"}) or set_stroke

When selecting which variable to bind: if the node is a Tablet or Mobile variant
(name or variant property contains "Tablet"/"Mobile"), match the node's 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"})

Input parameters:

- `node` (string, required): Node ID (e.g. "1:2")
- `prop` (string, required): Flat Figma bindable field (fontSize, paddingTop, itemSpacing, visible, characters, etc.). COLOR props (fills/strokes) not supported — use set_fill/jsx.
- `variable` (string, required): VariableID to bind (FLOAT/BOOLEAN/STRING only)

### `set_variable_mode` (~164 tokens)

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"})

Input parameters:

- `collection` (string, required): VariableCollectionId
- `mode` (string, required): Mode id (e.g. "1:1")
- `node` (string, required): Node ID (e.g. "1:2")

### `create_component` (~52 tokens)

Convert a frame or group to a Figma component.

Examples:
  create_component({node: "1:2"})

Input parameters:

- `node` (string, required): Node ID (e.g. "1:2") to convert

### `combine_components` (~69 tokens)

Combine multiple components into a variant set (ComponentSet).

Examples:
  combine_components({nodes: ["1:2", "1:3", "1:4"], name: "Button"})

Input parameters:

- `name` (string): Component set name
- `nodes` (array, required): Component node IDs to combine

### `add_component_prop` (~254 tokens)

Add a component property and bind it to a child node.

For TEXT properties: binds to the target text node's characters, so instances can override the text content.
For BOOLEAN properties: binds to the target node's visibility.

Parameters:
  node: The component node ID (must be COMPONENT or COMPONENT_SET).
  name: Property display name.
  type: TEXT, BOOLEAN, or INSTANCE_SWAP.
  default: Default value.
  bind: Child node ID to bind this property to. For TEXT, binds to text content. For BOOLEAN, binds to visibility.

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", bind: "1:6"})

Input parameters:

- `bind` (string): Child node ID to bind this property to
- `default` (string): Default value
- `name` (string, required): Property name
- `node` (string, required): Component node ID (e.g. "1:2")
- `type` (string, required): Property type

### `list_component_props` (~57 tokens)

List properties and variants of a component, component set, or instance.

Examples:
  list_component_props({node: "1:2"})

Input parameters:

- `node` (string, required): Component/instance node ID (e.g. "1:2")

### `create_instance` (~351 tokens)

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: { id: "5:42", name: "Button", componentId: "1:2" } }

Parameters beyond schema:
\- `node` must be a Component node (not Frame, Text, or another Instance). 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.

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"})                    // at page root
  create_instance({node: "1:2", parent: "1:4"})     // inside frame 1:4

Input parameters:

- `node` (string, required): Component node ID (e.g. "1:2") to instantiate
- `parent` (string): Parent node ID for placement

### `set_text` (~121 tokens)

Set text content on one or more nodes.

  set_text({node: "1:2", text: "Hello World"})
  set_text({nodes: [{node: "1:2", text: "Title"}, {node: "1:3", text: "Subtitle"}]})

Use this when changing what text says. For text styling (font, size, weight), use edit.

Input parameters:

- `node` (string): Node ID
- `nodes` (array): Batch: [{node, text}]
- `text` (string): New text content

### `set_fill` (~406 tokens)

Set fill or background color on a node.

  set_fill({node: "1:2", bg: "#F5F5F5"})
  set_fill({node: "1:2", fill: "#333333"})
  set_fill({node: "1:2", bg: "linear-gradient(135deg, #8B5CF6 0%, #F97316 100%)"})

  // Batch — bulk paint update in one call:
  set_fill({nodes: [{node: "1:2", bg: "#FFF"}, {node: "1:3", bg: "#F5F5F5"}]})

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

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)

Input parameters:

- `bg` (string): Background — hex, gradient string, "transparent", or qualified bare-name token
- `fill` (string): Text color or shape fill — hex, gradient string, or qualified bare-name token
- `node` (string): Node ID (single mode)
- `nodes` (array): Batch: [{node, fill?, bg?}]

### `set_stroke` (~479 tokens)

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.

Input parameters:

- `align` (string): Stroke alignment relative to the frame edge
- `color` (string): Stroke color — hex, gradient string, or qualified bare-name token
- `node` (string): Node ID (single mode)
- `nodes` (array): Batch: [{node, color?, weight?, align?, stroke?}]
- `stroke` (string): Shorthand: "1 #E0E0E0 inside" — single-string form. Hex only; for gradient/variable use the explicit color field.
- `weight` (number): Stroke weight in px

### `set_layout` (~406 tokens)

Set auto-layout properties on a container.

  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})

  // Batch — bulk update in one call:
  set_layout({nodes: [{node: "1:2", gap: 16, p: 24}, {node: "1:3", gap: 8, p: 12}]})

Controls spacing, padding, direction, and alignment of a container's children.
Grid: use layout:"grid" with cols/rows + gap (or rowGap/colGap for asymmetric).
Children fill the grid in insertion order.

Input parameters:

- `align` (string): Cross axis (flex only)
- `colGap` (number): Grid column gap (px, grid only)
- `cols` (number): Grid column count (required when layout="grid")
- `gap` (number): Spacing between children (px). On grid sets both row+column gap.
- `justify` (string): Main axis (flex only)
- `layout` (string): Auto-layout mode
- `node` (string): Node ID (single mode)
- `nodes` (array): Batch: [{node, layout?, gap?, p?, ...}]
- `p` (number): Padding — number, "v h", or "t r b l"
- `rowGap` (number): Grid row gap (px, grid only)
- `rows` (number): Grid row count (required when layout="grid")
- `wrap` (string): Wrap behaviour (flex only)

### `get_selection` (~112 tokens)

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()

### `switch_page` (~348 tokens)

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

Input parameters:

- `pageId` (string): Target page ID (e.g., "0:1"). Omit to just fetch the page roster without switching.

### `read_plugin_data` (~212 tokens)

Read plugin data (private or shared) from a Figma node.

Use for: i18n metadata, design-system tags, custom plugin annotations, anything stored via setPluginData / setSharedPluginData.

If `namespace` is omitted, reads private pluginData (`node.getPluginData(key)`).
If `namespace` is provided, reads sharedPluginData (`node.getSharedPluginData(namespace, key)`).

Returns `{value: ""}` (empty string) when the key does not exist — Figma's API never throws here.

Examples:
  read_plugin_data({node_id: "1:5", key: "ref"})
  read_plugin_data({node_id: "1:5", namespace: "i18n", key: "ref"})

Input parameters:

- `key` (string, required): Key to read.
- `namespace` (string): Optional sharedPluginData namespace. Omit for private pluginData.
- `node_id` (string, required): Figma node id (resolve via find_nodes / get_selection first).

### `write_plugin_data` (~189 tokens)

Write plugin data (private or shared) to a Figma node.

If `namespace` is omitted, writes private pluginData (`node.setPluginData(key, value)`).
If `namespace` is provided, writes sharedPluginData (`node.setSharedPluginData(namespace, key, value)`).

Pass an empty string as `value` to delete a key.

Examples:
  write_plugin_data({node_id: "1:5", key: "ref", value: "home_title"})
  write_plugin_data({node_id: "1:5", namespace: "i18n", key: "ref", value: "home.welcome_title"})

Input parameters:

- `key` (string, required): Key to write.
- `namespace` (string): Optional sharedPluginData namespace.
- `node_id` (string, required): Figma node id.
- `value` (string, required): Value (string). Pass "" to delete.

### `create_vector` (~675 tokens)

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

Input parameters:

- `data` (string): Raw SVG path string. Mutually exclusive with `points`.
- `fill` (string): Fill — hex / gradient / variable / "transparent" (default: "transparent").
- `height` (number, required): Vector bounds height in px.
- `name` (string): Node name (default: "Vector").
- `parent` (string): Parent node ID. Omit to attach to the current page.
- `points` (array): Polyline points as [[x,y], ...]. Compiled internally to "M x0 y0 L x1 y1 ...". Mutually exclusive with `data`.
- `stroke` (string): Stroke color — hex, gradient string, or qualified bare-name token.
- `strokeAlign` (string): Stroke alignment relative to the path (default: center).
- `strokeWeight` (number): Stroke weight in px (default: 1).
- `width` (number, required): Vector bounds width in px.
- `windingRule` (string): Path fill winding rule (default: NONZERO).
- `x` (number): X position relative to parent (default: 0).
- `y` (number): Y position relative to parent (default: 0).

### `get_screenshot` (~209 tokens)

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)

Input parameters:

- `node` (string, required): Node ID (e.g. "100:5"). Page root "/" is not supported.
- `padding` (number): Reserved for future use.
- `scale` (number): Export scale 0.5–2 (default 1).

### `ask_user` (~555 tokens)

Ask the user 1-3 questions in a single form. Each question has its own options and can be single- or multi-select. Bundle related decisions in ONE call instead of multiple turns.

Use when:
\- The prompt is ambiguous on multiple dimensions (audience + aesthetic + length) — bundle them into one form
\- You need a decision before proceeding (delete existing? which section first?)
\- Multiple valid approaches exist and user preference matters

Returns one of:
\- { answers: [...] } — array indexed to questions order. string for single-select, string[] for multi-select. The string MAY be one of the option labels OR custom text the user typed via the auto-injected "Other..." option.
\- { freeText: "..." } — when the user typed a free-form answer in the chat input instead of submitting the form. Treat as authoritative — user is overriding the structured options.

Each question:
\- question: required prompt string. Self-contained — no separate header/label, the question text IS the heading.
\- options: 2-3 options, each { label, description? }. The form auto-injects an "Other..." row, so the user always sees options.length + 1 rows total — keep options ≤ 3 to stay within the 4-row visual cap.
\- multiSelect: optional boolean (default false). Use only when the answer is genuinely a list (e.g. "which features?"). For mutually exclusive choices keep false.

Conventions:
\- **First option = recommended.** If you have a strong default for the user, put it FIRST and add "(Recommended)" at the end of the label. The form auto-focuses the first option and the dev/auto-pick fallback selects it — both work better with a deliberate recommendation.
\- **Do NOT include an "Other" option yourself** — the form auto-injects an "Other..." row per question with an inline text input. Don't add a redundant one.
\- **Bundle aggressively.** 2 related dimensions in ONE call beats 2 sequential turns.

Example:
  ask_user({ questions: [
    { question: "Who is this for?", options: [{label:"B2B SaaS"},{label:"…

Input parameters:

- `questions` (array, required): 1-3 questions to present in a single form (3 is the soft cap; bundle related decisions but don't pad).

### `subtask` (~193 tokens)

Delegate a focused sub-task to a typed child agent. Each type has its own tools, iteration budget, and behavioral constraints.

Available agent types:
\- create: Build an independent UI section (header, sidebar, form, card). Default.
\- audit: Read-only design review — find layout issues, property omissions, report PASS/FAIL.
\- token: Variable system operations — create collections, bind tokens, set up aliases.

Use when the prompt names 3+ distinct regions (e.g. header, sidebar, main) that share no nodes, or when specialized behavior is needed (audit, token ops). For 1-2 tool-call operations, inline calls finish faster than the subtask spin-up cost.

Input parameters:

- `prompt` (string, required): Description of the sub-task to delegate. Be specific about what to create/modify/audit.
- `type` (string): Agent type. Defaults to "create" if omitted.

### `session_note` (~1095 tokens)

Read / write your own session scratchpad. Persists across turns within one design session ("New Design" resets it). Notes are how state carries from this turn into the next — the next session loads them as context.

Actions:
  action: "read"   → read({key}) returns the current value (empty string if unset)
  action: "write"  → write({key, value}) replaces or deletes (pass value:"" to delete)
  action: "list"   → list({}) returns [{key, chars}] for all existing notes

Slots — FORWARD-LOOKING (what we plan / commit to):
  \- plan       — this turn's intent + step outline (write at turn start)
  \- decisions  — locked choices: style picked + reason, accent token, font scale, hero treatment, etc. (write BEFORE jsx)
  \- brand      — durable brand notes pulled from a project design.md (if user supplied one)
  \- todo       — TRULY unfinished work for the next turn (omit if everything shipped)

Slots — BACKWARD-LOOKING (what happened, write AT TURN END — AUTO-MERGE on write):
  \- failures   — tool calls that failed this turn + how you worked around them.
                 Example: "jsx items='stretch' rejected (DSL valid: center|start|end|space-between|baseline); retried with 'center'."
                 If a failure repeats a class you've seen before, name the class.
  \- gotchas    — validator warnings you noticed but chose not to fix + why.
                 Example: "4 LOW_CONTRAST on nav links (2.5:1) — deliberate for ambient-grey style; revisit if user complains."
                 Also: magic numbers / hand-tuned positions and what motivated them.
                 Example: "Glow ellipses at (-180, 220) / (1060, 70) — placed half outside frame to bleed in."
  \- learnings  — surprises about this codebase / DSL / Figma API.
                 Example: "radial-gradient(circle at X% Y%) rejected — DSL only takes the simple form. Same trap as CSS-prior bleed elsewhere."

BACKWARD-LOOKING slots auto-merge: writing to `failures / gotchas / learnings` appends to prior content after a…

Input parameters:

- `action` (string, required): read | write | list
- `key` (string): Note key. Required for read/write. Recommended slots: plan, decisions, brand, todo.
- `value` (string): Markdown body for write. Pass "" to delete. Ignored for read/list.

## Diagnostics

Captured diagnostic sections: Provenance, Dependencies. The full working is on the page: https://verifymcp.io/servers/musepy-genable-mcp/genable-mcp#diagnostics

## Score history

- 2026-08-03: 64
- 2026-08-02: 32
- 2026-08-01: 22
- 2026-07-31: 17
- 2026-07-30: 24
- 2026-07-28: 42
- 2026-07-27: 42

## Links

- npm package: https://www.npmjs.com/package/genable-mcp
- Socket report: https://socket.dev/npm/package/genable-mcp
- Repository: https://github.com/musepy/genable
- Changelog RSS feed: https://verifymcp.io/servers/musepy-genable-mcp/genable-mcp/changelog.xml
- Changelog JSON feed: https://verifymcp.io/servers/musepy-genable-mcp/genable-mcp/changelog.json
- HTML version of this page: https://verifymcp.io/servers/musepy-genable-mcp/genable-mcp
