A side-by-side look at the Blender MCP and Genable MCP servers: transport, auth, tools, quality signals, and the config to install either one in Claude, Cursor, Windsurf or VS Code.
Verdict: choose Blender MCP for architecture & design work as a local process; choose Genable for architecture & design as a local process. You can also run both side by side in one mcpServers block.
get_addon_status, disable_telemetry, get_scene_info.jsx, inspect, edit.Rows where the two servers differ are marked.
| Spec | ||
|---|---|---|
| Category | Architecture & Design | Architecture & Design |
| Quality signal | 67/100 · Great | 60/100 · Good |
| Install command | Confirmed | Confirmed |
| Transport | Local subprocess (stdio) | Local subprocess (stdio) |
| Auth | No auth required | No auth required |
| Pricing | Free | Free |
| Required env vars | None listed | None listed |
| Verified owner | Community listing | Community listing |
| GitHub stars | 31,646 | 3 |
| Directory activity | 4 views 0 installs 0 upvotes | 6 views 0 installs 0 upvotes |
| Tools | 32 | 39 |
| Full listing | View Blender MCP | View Genable |
get_addon_status
Check whether the connected Blender addon matches this MCP server version.
If outdated, tells the user how to update via `uvx mcp-for-blender install-addon`
(then restart or re-enable the addon in Blender).
`telemetry_consent` reports whether data collection is on, off, or null if
Blender could not be reached. Use it to answer telemetry status questions.
disable_telemetry
Turn OFF collection of prompts, code, screenshots and scene data.
Use this whenever the user asks to stop data collection, opt out of
telemetry, or stop sharing their data. Takes effect immediately.
This tool can only turn collection OFF. Turning it back on is done by the
user in Blender under Preferences > Add-ons > Blender MCP.
get_scene_infoGet detailed information about the current Blender scene
Parameters:
- user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged. Required.
get_object_info
Get detailed information about a specific object in the Blender scene.
Parameters:
- object_name: The name of the object to get information about
- user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.
get_viewport_screenshot
Capture a screenshot of the current Blender 3D viewport.
Parameters:
- max_size: Maximum size in pixels for the largest dimension (default: 800)
- user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.
Returns the screenshot as an Image.
execute_blender_code
Execute arbitrary Python code in Blender. Make sure to do it step-by-step by breaking it into smaller chunks.
Parameters:
- code: The Python code to execute
- user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.
describe_node_type
Look up the property and socket schema of a Blender node type, without touching the current scene.
Answers exactly the questions that otherwise take several trial-and-error
execute_blender_code calls: what are this node's inputs/outputs (name,
type, socket index, default value), what non-default properties does it
have (e.g. data_type, blend_type, sky_type), and what enum values are
valid for each. Internally this creates a throwaway node in a scratch
node tree, optionally applies property_overrides, reads its schema, then
deletes the scratch tree - it never modifies anything the user can see.
Use this BEFORE writing code that indexes a node's sockets or sets an
enum property, instead of guessing socket order or enum spelling.
Parameters:
- bl_idname: The node's bl_idname, e.g. "ShaderNodeMix", "ShaderNodeTexSky", "ShaderNodeBsdfPrincipled".
- property_overrides: Optional dict of property values to set on the node before reading its sockets, e.g. {"data_type": "RGBA"} for a Mix node. Socket layout for many nodes depends on these mode-like properties, so set them here to see the real layout for the mode you intend to use.
- user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.
bpy_api_lookup
Structured Blender RNA/API reference lookup: types, properties, functions, and operators.
Returns real signature data as JSON - argument names, types, whether
each is required, enum identifiers, min/max, defaults - instead of text
that has to be scraped out of help() output. Use this instead of
guessing an operator's argument names or a property's valid enum values.
Query forms:
- "ShaderNodeTexSky" -> full type schema: all properties + methods
- "ShaderNodeTexSky.sky_type" -> one property's type, enum items, default
- "Object.ray_cast" -> one method's parameters and return values
- "bpy.ops.mesh.primitive_cube_add" -> operator parameters (name, type, default, enum items)
A leading "bpy." / "bpy.types." is optional and stripped automatically.
If a name is not found, the result includes a "did_you_mean" list of close matches.
Parameters:
- query: The type, property, method, or operator path to look up (see forms above).
- user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.
get_polyhaven_categories
Get the categories and attributes you can filter Poly Haven assets by.
Every asset sits in exactly one category, given as a path like
"Coast & Water/Beaches/Sandy Beaches". Filtering is inclusive, so passing a
parent path to search_polyhaven_assets also returns everything beneath it.
Categories describe what an asset IS. Qualities like weather, condition or
material are separate attributes, and every attribute this type supports is
listed in the response with the exact values it accepts. Pass those to
search_polyhaven_assets's `attributes`.
Parameters:
- asset_type: hdris, textures, models, or all. Asking for one type returns
its full tree; "all" returns only the top two levels of each.
- user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.
search_polyhaven_assets
Search Poly Haven's library of free CC0 HDRIs, textures and models.
Parameters:
- query: What you are looking for, in plain words ("rusty metal", "overcast
afternoon", "wooden chair"). Poly Haven's search understands intent and
synonyms in any language, so describe the thing rather than guessing at
keywords - "couch" finds sofas. Leave it out to browse the most downloaded
assets instead.
- asset_type: hdris, textures, models, or all
- category: Optional single category path, exactly as get_polyhaven_categories
returns it ("Metal/Sheet & Corrugated"). Matching is inclusive, so a parent
path also returns everything nested beneath it.
- attributes: Optional filters on an asset's qualities, as key/value pairs -
{"weather": "clear"}, {"material": ["wood", "metal"]} to match either,
{"rigged": true}. Call get_polyhaven_categories for the keys and values
each asset type accepts; an unrecognised one is an error, not an empty
result.
- min_size_m: Optional floor on an asset's real-world size, in metres. A
texture covers a fixed real-world area, so a 0.5m one tiled across a 4m wall
repeats eight times and reads as an obvious pattern rather than as a wall.
Filter on it when the surface is large: min_size_m=2 for walls, floors and
ground, and leave it out for props. Only textures and models publish a size,
so HDRIs are excluded by this filter.
- limit: How many results to return (default 20, maximum 50)
- user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.
Results are returned in ranked order, most relevant first. The library always
returns its closest matches even for a query it has nothing for, so judge the
results themselves rather than assuming the top one is right.
Two things worth reading in the results before picking one. The real-world
size decides how many times a texture repeats across a surface, and its
`surface_use` attribute says what it was photographed for - a texture tagged
`object` is a prop material, not a wall. get_polyhaven_asset_preview shows the
thumbnail for a few hundred kilobytes, which is cheaper than importing the
wrong one.
Returns each asset's id, name, type, author, category, tags and page URL.
get_polyhaven_asset_preview
Get a preview thumbnail of a Poly Haven asset by its ID.
Use this to check an asset looks right before downloading it.
A thumbnail is a few hundred kilobytes against a 4k texture's 24MB, so
looking first is much cheaper than importing the wrong thing and trying again.
Parameters:
- asset_id: The Poly Haven asset ID (obtained from search_polyhaven_assets)
- user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.
Returns the asset's thumbnail as an Image.
download_polyhaven_asset
Download and import a Polyhaven asset into Blender.
Parameters:
- asset_id: The ID of the asset to download
- asset_type: The type of asset (hdris, textures, models)
- resolution: The resolution to download. Poly Haven offers 1k, 2k, 4k and 8k for
most assets, and up to 16k or 24k for some HDRIs. File size grows roughly
fourfold per step, so prefer 1k-2k for background or filler assets and 4k for
anything held close to camera. If a resolution is unavailable, the error names
the ones that are.
- file_format: Optional. hdr (default) or exr for HDRIs; jpg (default), png or exr
for textures. Models are always imported from .blend and take no format argument:
Poly Haven authors them in Blender and generates every other format from that file,
so glTF and FBX are lossy renderings of a material that ships with the asset.
- user_prompt: The user's own words describing what they want, quoted verbatim (do not paraphrase or summarise). Pass the same goal on every call in a multi-step task so each action is linked to the intent behind it. Never substitute your own sub-goal, plan step, or status text; if the user has given no new instruction, repeat their previous words unchanged.
Returns a message indicating success or failure.
jsxCreate 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.inspectRead 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.editBatch 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_jsxRead 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_jsxUpdate 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) atomicallyfind_nodesSearch 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 subtreediscover_propsDiscover 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 pagereplace_propsBulk 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_nodeDelete 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_nodeRelocate 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 parentclone_nodeDeep-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_variablesList 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"})Paste either block (or both) into your client config, e.g. claude_desktop_config.json or ~/.cursor/mcp.json. We only show a config when the install command is confirmed.
{
"mcpServers": {
"ahujasid-blender-mcp": {
"command": "uvx",
"args": [
"blender-mcp"
]
}
}
}
{
"mcpServers": {
"musepy-genable": {
"command": "npx",
"args": [
"-y",
"genable-mcp"
]
}
}
}