Dispatch tools
24 first-class MCP tools, all blender_-prefixed, that wrap the addon’s @command handlers. The LLM calls them like any MCP tool; the server registers a Future via JobWaiter, routes a command_dispatch payload through the bus to a Blender peer, awaits the addon’s blender_job_update reply, and returns the result synchronously. The bus round-trip is invisible from the caller’s perspective.
Choose dispatch tools over blender_send_message + script-dispatch unless you need arbitrary Python. See Command dispatch vs script dispatch for the decision tree.
Common parameters
Section titled “Common parameters”Every dispatch tool accepts the same two infrastructure parameters in addition to its own:
| Parameter | Default | Purpose |
|---|---|---|
| target_uuid | None | A Blender client UUID, "latest" (newest registration among live clients) or "pid:<n>". If omitted and exactly one live Blender peer is connected, that one is used. Peers with no heartbeat for BLENDER_MCP_CLIENT_STALE_SECONDS (default 180) don’t count. Otherwise returns ambiguous_target, no_live_client or no_client. |
| _timeout | varies | Seconds to wait for the addon’s reply. Underscore-prefixed so it doesn’t leak into the addon-side params dict. |
Tools also receive a ctx: Context injected by FastMCP for auth.
Return envelope
Section titled “Return envelope”Every dispatch tool returns a JSON string with this shape:
{ "status": "completed" | "failed" | "queued" | "running" | "timeout" | "no_client" | "ambiguous_target" | "unknown_target", "command": "<command name>", "target_uuid": "<chosen client>", "job_id": "j-<12 hex>", "result": <inner JSON>, "error": "<message or empty>"}| Status | Meaning |
|---|---|
| completed | Addon handler returned successfully; result holds its output |
| failed | Addon handler raised; error holds the message |
| queued / running | _timeout elapsed but the job is still waiting or still running in Blender. It keeps going and its result is kept; poll it with job_id (see Long-running jobs) |
| timeout | _timeout elapsed without an addon reply and the job store was unavailable, so there is no job_id to follow |
| no_client | No Blender peer on the user’s bus could be targeted |
| ambiguous_target | Multiple Blender peers, no target_uuid to disambiguate |
| no_live_client | Several Blender peers are registered but none has heartbeated recently; candidates lists them |
| unknown_target | target_uuid was set but no matching client is registered |
Tier 1: always-on core (7)
Section titled “Tier 1: always-on core (7)”Foundational tools. No gating; always available when at least one Blender peer is connected.
blender_get_scene_info
Section titled “blender_get_scene_info”blender_get_scene_info(scene: str | None = None, target_uuid: str | None = None, _timeout: float = 30) -> strSnapshot of the active scene: name, object count, first ~10 objects, materials count, type counts and the user’s selection. scenes lists every scene in the file with its object count and which one is active. Pass scene to summarise another scene without switching the user to it; an unknown name comes back as an error listing the scenes. blender_list_scene_objects takes the same scene argument for the full object list.
A listed object parented to an armature bone carries parent_bone (its location is then relative to that bone). rigs names up to three armatures with up to 16 of their bones each, plus the armature’s how_to_pose text when it has one, so a request to pose or animate a character finds the bones instead of moving the parts.
blender_get_object_info
Section titled “blender_get_object_info”blender_get_object_info(name: str, target_uuid: str | None = None, _timeout: float = 30) -> strDetailed descriptor for a specific object by name: transform, mesh stats, material slots. Returns failed if name doesn’t exist.
A parented object also gets parent; parented to a bone, it gets parent_type: "BONE", parent_bone and a pose_note saying to pose that bone rather than the object. An armature gets bone_count, bones (up to 40 names), bone_rotation_mode and the pose_path to key them, such as pose.bones["<bone>"].rotation_euler. Any object with a string custom property how_to_pose gets that text back (up to 1200 characters); scene authors can use it to say how their rig is meant to be posed, as the B. Clip template does.
blender_browse_data
Section titled “blender_browse_data”blender_browse_data( collection: str | None = None, item_name: str | None = None, page: int = 1, page_size: int = 50, detail_level: str = "summary", target_uuid: str | None = None, _timeout: float = 30,) -> strPaginated bpy.data.* browser. Pass collection="objects", "materials", "meshes", etc. Omit collection to list available collections. Pass item_name to drill into a single item. detail_level is summary or full.
blender_execute_code
Section titled “blender_execute_code”blender_execute_code(code: str, target_uuid: str | None = None, _timeout: float = 60) -> strExecute arbitrary Python in the Blender main thread. Captures stdout and returns it as result. The script’s globals include bpy, bmesh, mathutils. Use this when no dedicated dispatch tool exists for the operation; for multi-step orchestration in a single round-trip, see blender_send_message with a job_dispatch payload.
blender_get_viewport_screenshot
Section titled “blender_get_viewport_screenshot”blender_get_viewport_screenshot( filepath: str | None = None, max_size: int = 0, format: str = "png", frame: list[str] | None = None, crop: bool = False, crop_margin: float = 0.05, store: bool = False, annotations: bool = True, deselect: bool = False, target_uuid: str | None = None, _timeout: float = 30,) -> strSave a 3D viewport screenshot on the Blender host. deselect=True captures with nothing selected or active, so selection outlines don’t tint the objects, then puts the selection and active object back exactly as they were; the chat’s look_at_viewport always asks for it. Add-ons older than this parameter ignore it. filepath is a path on the Blender machine; leave it out and the image goes to a unique file under Blender’s temp dir (blender_mcp_screenshots/, cleared when Blender exits). Either way the result’s filepath says where it was written. max_size caps the longest edge in pixels; 0 (the default) keeps the viewport’s native resolution. format is png or jpg.
blender_get_console_output
Section titled “blender_get_console_output”blender_get_console_output( level: str = "all", page: int = 1, page_size: int = 50, target_uuid: str | None = None, _timeout: float = 30,) -> strPaginated scrape of the Blender console. level filters lines by kind: all, info, warning, error, output. Also exposed as a resource — see blender://console/{level}.
blender_console_operations
Section titled “blender_console_operations”blender_console_operations( operation: str = "get_info", params: dict | None = None, target_uuid: str | None = None, _timeout: float = 30,) -> strInvoke a bpy.ops.console.* operator (e.g. execute, autocomplete, clear). See the addon’s console_operations handler for the operation catalog.
Tier 2: integration status probes (3)
Section titled “Tier 2: integration status probes (3)”Always-on. Call these before a Tier-3 tool to check whether the corresponding integration is enabled in the addon’s preferences.
blender_get_polyhaven_status
Section titled “blender_get_polyhaven_status”blender_get_polyhaven_status(target_uuid: str | None = None, _timeout: float = 10) -> strReturns whether prefs.use_polyhaven is enabled.
blender_get_hyper3d_status
Section titled “blender_get_hyper3d_status”blender_get_hyper3d_status(target_uuid: str | None = None, _timeout: float = 10) -> strReturns Hyper3D Rodin integration state: enabled flag, mode (MAIN_SITE or FAL_AI), key type (trial vs configured).
blender_get_sketchfab_status
Section titled “blender_get_sketchfab_status”blender_get_sketchfab_status(target_uuid: str | None = None, _timeout: float = 30) -> strReturns Sketchfab integration state. Validates the configured API key against /v3/me (hence the longer timeout).
Tier 3a: msgbus (5, ungated)
Section titled “Tier 3a: msgbus (5, ungated)”Bridge to Blender’s RNA bpy.msgbus for change notifications.
blender_msgbus_clear_by_owner
Section titled “blender_msgbus_clear_by_owner”blender_msgbus_clear_by_owner(owner_id: str = "default", target_uuid: str | None = None, _timeout: float = 10) -> strDrop all bpy.msgbus subscriptions owned by owner_id.
blender_msgbus_publish_rna
Section titled “blender_msgbus_publish_rna”blender_msgbus_publish_rna(data_path: str | None = None, target_uuid: str | None = None, _timeout: float = 10) -> strPublish an RNA-property change. data_path in frame_current, active_object, selected_objects. Pass nothing to flush all pending messages.
blender_msgbus_subscribe_rna
Section titled “blender_msgbus_subscribe_rna”blender_msgbus_subscribe_rna( data_path: str, owner_id: str = "default", notify_type: str = "UPDATE", persistent: bool = True, target_uuid: str | None = None, _timeout: float = 10,) -> strSubscribe to RNA-property changes. Notifications queue in the addon for later drainage.
blender_msgbus_get_notifications
Section titled “blender_msgbus_get_notifications”blender_msgbus_get_notifications( owner_id: str | None = None, clear: bool = False, target_uuid: str | None = None, _timeout: float = 10,) -> strDrain the addon’s RNA-change notification queue. Set clear=True to also drop the queued entries.
blender_msgbus_list_subscriptions
Section titled “blender_msgbus_list_subscriptions”blender_msgbus_list_subscriptions(owner_id: str | None = None, target_uuid: str | None = None, _timeout: float = 10) -> strList active RNA subscriptions, optionally filtered by owner_id.
Tier 3b: PolyHaven (4, gate: use_polyhaven)
Section titled “Tier 3b: PolyHaven (4, gate: use_polyhaven)”CC0 asset library. Probe with blender_get_polyhaven_status before calling.
blender_get_polyhaven_categories
Section titled “blender_get_polyhaven_categories”blender_get_polyhaven_categories(asset_type: str, target_uuid: str | None = None, _timeout: float = 30) -> strasset_type in hdris, textures, models, all.
blender_search_polyhaven_assets
Section titled “blender_search_polyhaven_assets”blender_search_polyhaven_assets( asset_type: str | None = None, categories: str | None = None, target_uuid: str | None = None, _timeout: float = 30,) -> strSearch. Addon caps the response at 20 entries.
blender_download_polyhaven_asset
Section titled “blender_download_polyhaven_asset”blender_download_polyhaven_asset( asset_id: str, asset_type: str, resolution: str = "1k", file_format: str | None = None, target_uuid: str | None = None, _timeout: float = 120,) -> strDownload + import. Models prefer glTF; HDRIs default to .hdr; textures default to .jpg. HDRIs replace the active world, textures create a new material, models append to the active scene.
blender_set_texture
Section titled “blender_set_texture”blender_set_texture(object_name: str, texture_id: str, target_uuid: str | None = None, _timeout: float = 30) -> strApply a previously-downloaded texture to object_name. Builds a fresh Principled-BSDF material; handles ARM packing and AO multiplication into base color.
Tier 3c: Hyper3D Rodin (3, gate: use_hyper3d)
Section titled “Tier 3c: Hyper3D Rodin (3, gate: use_hyper3d)”3D-model generation. Two backends (MAIN_SITE / FAL_AI) selected by addon prefs. Probe with blender_get_hyper3d_status.
blender_create_rodin_job
Section titled “blender_create_rodin_job”blender_create_rodin_job( text_prompt: str | None = None, images: list | None = None, bbox_condition: Any | None = None, target_uuid: str | None = None, _timeout: float = 60,) -> strStart a text-to-3D or image-to-3D job. images shape varies by backend: MAIN_SITE wants [[suffix, base64_data], ...]; FAL_AI wants [url, ...]. Returns the provider’s raw response (subscription_key or request_id).
blender_poll_rodin_job_status
Section titled “blender_poll_rodin_job_status”blender_poll_rodin_job_status( subscription_key: str | None = None, request_id: str | None = None, target_uuid: str | None = None, _timeout: float = 10,) -> strPass subscription_key for MAIN_SITE, request_id for FAL_AI.
blender_import_generated_asset
Section titled “blender_import_generated_asset”blender_import_generated_asset( name: str, task_uuid: str | None = None, request_id: str | None = None, target_uuid: str | None = None, _timeout: float = 120,) -> strImport a completed Rodin GLB. Pass task_uuid for MAIN_SITE, request_id for FAL_AI. Returns the imported object’s name, transform, and bounding box.
Tier 3d: Sketchfab (2, gate: use_sketchfab)
Section titled “Tier 3d: Sketchfab (2, gate: use_sketchfab)”Marketplace asset import. Probe with blender_get_sketchfab_status.
blender_search_sketchfab_models
Section titled “blender_search_sketchfab_models”blender_search_sketchfab_models( query: str, categories: str | None = None, count: int = 20, downloadable: bool = True, target_uuid: str | None = None, _timeout: float = 30,) -> strReturns the raw Sketchfab v3 search response.
blender_download_sketchfab_model
Section titled “blender_download_sketchfab_model”blender_download_sketchfab_model(uid: str, target_uuid: str | None = None, _timeout: float = 120) -> strDownload + import by UID. Zip-slip protected.
Annotations (5)
Section titled “Annotations (5)”Read what the user drew with Blender’s annotation tool (D-key and drag), and draw marks of your own to point things out. Blender 5.x keeps these strokes in bpy.data.annotations; the scene’s annotation datablock holds one or more layers.
Write tools draw on their own layer, LLM by default, created orange and slightly thicker than the user’s default so the two are easy to tell apart. The user’s layers are never changed unless you name one. Strokes you draw read back through blender_list_annotations and blender_get_annotation like anything the user drew.
blender_list_annotations
Section titled “blender_list_annotations”blender_list_annotations(include_bbox: bool = True, limit: int = 200, target_uuid: str | None = None, _timeout: float = 30) -> strEvery stroke with its id, layer, point count, colour, coordinate space and, for 3D strokes, a world-space bounding box. Cheap: no point coordinates.
blender_get_annotation
Section titled “blender_get_annotation”blender_get_annotation(annotation_id: str, target_uuid: str | None = None, _timeout: float = 30) -> strFull geometry for one stroke. world_points is filled only for space: "3D"; view-space strokes return raw_points in viewport-percentage units and report their space as 2D-Local.
blender_add_annotation_stroke
Section titled “blender_add_annotation_stroke”blender_add_annotation_stroke(points: list[list[float]], layer: str = "LLM", color: list[float] | None = None, thickness: int | None = None, space: str = "3D", cyclic: bool = False, target_uuid: str | None = None, _timeout: float = 30) -> strDraw one stroke. With space="3D", points are world coordinates [x, y, z] and the mark sits in the scene. With space="view", points are [x, y] percentages of the viewport region (0 to 100, origin bottom-left) and the mark stays fixed on screen. color ([r, g, b], 0 to 1) and thickness (1 to 10) apply to the whole layer, because Blender stores them per layer; use another layer name for another colour. cyclic closes the stroke. Returns the stroke id.
Arguments are checked before anything is sent to Blender: fewer than two points, wrong point shapes, colours outside 0 to 1 or thickness outside 1 to 10 return invalid_argument immediately.
blender_annotate_object
Section titled “blender_annotate_object”blender_annotate_object(object: str, style: str = "box", label: str | None = None, layer: str = "LLM", color: list[float] | None = None, thickness: int | None = None, padding: float = 0.05, target_uuid: str | None = None, _timeout: float = 30) -> strMark an object so the user can see which one you mean. box outlines its world-space bounding box (six strokes), circle rings it at mid height, arrow comes down at an angle and stops just above its top. padding grows the outline by that fraction of the object’s largest side. Annotations can’t hold text, so label is echoed back with label_drawn: false; put it in your reply instead.
blender_clear_annotations
Section titled “blender_clear_annotations”blender_clear_annotations(layer: str = "LLM", target_uuid: str | None = None, _timeout: float = 30) -> strRemove every stroke on one layer and report how many. Other layers are untouched and the layer itself is kept. A missing layer returns found: false, not an error.
Marks are drawn on the layer’s earliest frame so they stay visible while the timeline moves, and the annotation overlay is switched on in every 3D viewport.
Assembly analysis and keyframes (5, ungated)
Section titled “Assembly analysis and keyframes (5, ungated)”For physical assemblies and keyframed configurations, where bounding boxes overlap far more often than parts actually intersect, and where Blender 5.x removed action.fcurves. Every tool evaluates the scene as shown (modifiers, animation), and any frame argument puts the scene back on its original frame afterwards.
blender_check_interference
Section titled “blender_check_interference”blender_check_interference( objects: list[str] | str | None = None, # None = visible geometry, "all" includes hidden frame: int | None = None, frames: list[int] | None = None, # sweep up to 50 frames ignore_pairs: list[list[str]] | None = None, include_hidden: bool = False, tolerance: float = 0.0, check_containment: bool = True, sample_points: int = 3, target_uuid: str | None = None, _timeout: float = 60,) -> strTests every pair whose world bounds overlap for real triangle intersection on evaluated, world-space meshes. Returns intersecting pairs with a triangle_pairs count and sample points, contained pairs (a part entirely inside another crosses no triangles, so it’s found by an inside test that needs closed meshes with outward normals), and bbox_overlap_pairs so you can see how many box overlaps were false alarms. Faces that only touch coplanar don’t count as intersecting; raise tolerance (scene units) to catch near or touching contact. Use ignore_pairs for intended contact such as snap fits. Up to 300 objects per call; a long sweep comes back as a job to follow with blender_job_status.
blender_world_bounds
Section titled “blender_world_bounds”blender_world_bounds( objects: list[str] | str | None = None, frame: int | None = None, include_hidden: bool = False, exact: bool = False, target_uuid: str | None = None, _timeout: float = 30,) -> strWorld-space min, max, size and center per object, plus their union. The default transforms the evaluated local bound box, which never under-reports but can over-report for rotated parts; exact=True uses evaluated vertices for tight bounds.
blender_get_keyframes
Section titled “blender_get_keyframes”blender_get_keyframes(object: str, data_path: str | None = None, owner: str = "object", target_uuid: str | None = None, _timeout: float = 30) -> strChannels of the object’s assigned action with each key’s frame, value and interpolation. Reads Blender 5.x slotted Actions through the assigned slot’s channelbag and falls back to legacy actions; api in the result says which. owner is "object", "data" (a light’s energy, a camera’s lens) or "shape_keys".
blender_insert_keyframe
Section titled “blender_insert_keyframe”blender_insert_keyframe( object: str, data_path: str, frame: float, value: float | list[float] | bool | str | None = None, index: int = -1, overwrite: bool = False, interpolation: str | None = None, owner: str = "object", target_uuid: str | None = None, _timeout: float = 30,) -> strSets the frame, then the value, then inserts the key. That order matters: setting a value and then changing frame re-evaluates the animation and silently discards the value. An existing key on the same channel and frame is never replaced unless overwrite=True, and replaced values come back in replaced, so an authored pose can’t be destroyed by accident. Omit value to key the property’s current value.
blender_set_interpolation
Section titled “blender_set_interpolation”blender_set_interpolation(object: str, interpolation: str, data_path: str | None = None, frames: list[float] | None = None, owner: str = "object", target_uuid: str | None = None, _timeout: float = 30) -> strSets CONSTANT, LINEAR, BEZIER or any other Blender interpolation on the object’s keys, optionally for one property and/or specific frames.
Booleans and mesh health (2, ungated)
Section titled “Booleans and mesh health (2, ungated)”Booleans that check their inputs and report what they did. Blender’s own boolean modifier fails in ways that look like success, so these tools measure the target before and after every operand.
blender_mesh_health
Section titled “blender_mesh_health”blender_mesh_health(objects: list[str] | str, evaluated: bool = False, target_uuid: str | None = None, _timeout: float = 30) -> strPer object: vertex, edge and face counts; non-manifold edges broken down into boundary_edges (open), multi_face_edges (shared by three or more faces), wire_edges and non_contiguous_edges (flipped normals); loose_vertices, zero_area_faces, connected shells, whether the mesh is closed, its volume in world units, and world bounds. volume is null when the mesh isn’t closed, because an open mesh has no meaningful volume. evaluated=True measures the mesh with its modifiers; the default reads the stored mesh. Nothing is modified.
blender_boolean
Section titled “blender_boolean”blender_boolean( target: str, operands: list[str] | str, operation: str = "DIFFERENCE", # DIFFERENCE, UNION, INTERSECT solver: str = "EXACT", # EXACT, FLOAT, MANIFOLD (no FAST in 5.x) apply: bool = True, check: bool = True, force: bool = False, use_self: bool = False, use_hole_tolerant: bool = False, coplanar_tolerance: float = 1e-4, coplanar_nudge: float | None = None, keep_operands: bool = True, hide_operands: bool = True, target_uuid: str | None = None, _timeout: float = 60,) -> strOperands are applied one at a time. Each entry in steps gives the face count, volume and non-manifold edge count before and after, plus that step’s warnings, so a bad result can be traced to the operand that caused it.
With check on (the default), the call is refused and nothing changes when:
- the target or an operand isn’t a closed manifold solid (open, wire or three-face edges, flipped normals), or
- an operand’s faces lie in the same plane as the target’s, either coincident (same direction) or abutting (touching back to back).
The refusal names the problem and where it was found. Fix the inputs, pass coplanar_nudge (a small distance in scene units: a temporary copy of each operand is pushed outward by that much so no faces share a plane), or pass force=True to run anyway.
Warnings flag results that look fine but aren’t:
- a DIFFERENCE or INTERSECT that changed nothing,
- a UNION that left the target unchanged (the operand was dropped, unless it lies entirely inside),
- a UNION smaller than one of its inputs, or a DIFFERENCE that increased the volume,
- new non-manifold edges, or a result that is no longer a closed solid.
use_self also unions overlapping parts within the target, which cleans up duplicated or overlapping pieces before trimming. apply=False leaves the boolean modifiers on the target for inspection. Operands are kept and hidden by default; keep_operands=False deletes them after applying. Modifiers are applied to the base mesh, ahead of any modifiers the target already has.
Booleans that fail silently
Section titled “Booleans that fail silently”These came from a real modelling session (a house roof built from nine unioned gables) and are what the checks above look for:
- Faces sharing a plane corrupt the result. Unioning solids that abut on the same plane, or cutting with an operand whose face lies exactly on the target’s surface, can leave non-manifold edges with no error. The mesh still renders correctly, so nothing looks wrong until a later boolean on it misbehaves. Overlap abutting parts slightly (a few hundredths of a unit) instead of letting faces meet exactly, or use
coplanar_nudge. - A boolean can do nothing, or drop an operand, and report success. Face counts alone don’t catch this, because valid unions also remove faces. The volume check does: a union can never be smaller than either input, and a difference can never be larger than its target.
- Volume stops meaning anything once a mesh is non-manifold, so non-manifold edges are reported on every step rather than relying on volume alone.
- Solvers fail differently. In testing,
FLOATproduced non-manifold results on coplanar and abutting inputs whereEXACTandMANIFOLDstayed clean.MANIFOLDneeds clean closed inputs. Blender 5.x has noFAST;FLOATreplaced it. - Sometimes a boolean is the wrong tool. A flat plate with a hole is better built by clipping the outline and extruding it than by cutting a solid.
Job-pump health
Section titled “Job-pump health”The addon reports its job queue about every 10 seconds from its network thread, so a report still arrives while Blender’s main thread is busy. blender_list_available_clients shows it as health (queue_depth, active_jobs with running_seconds, drain_timer_registered, last_drain_seconds_ago, reported_seconds_ago), and a dispatch that times out or comes back queued includes it as target_health, with a hint that tells the three usual causes apart: Blender busy with a long job, a stalled job pump, or a dispatch that hasn’t arrived yet.
blender_reset_pump
Section titled “blender_reset_pump”blender_reset_pump(target_uuid: str | None = None, bus_id: str | None = None) -> strRestarts the addon’s job pump over the bus, for the “connected, jobs queued, nothing running” stall that used to need a click on Disconnect and Connect in the sidebar. The request travels with the addon’s next check-in (up to about 10 seconds) and runs on its network thread, so it works even when no dispatched command can. It can’t help when Blender’s main thread is busy with a long job; health in the reply shows which case you’re in.
Gates: how Tier-3 disablement works
Section titled “Gates: how Tier-3 disablement works”The recommended pattern: probe with a Tier-2 status tool first, branch on the result. Example:
status = json.loads(await client.call_tool("blender_get_polyhaven_status", {}))if json.loads(status["result"]).get("enabled"): assets = await client.call_tool( "blender_search_polyhaven_assets", {"asset_type": "textures"} )else: raise RuntimeError("PolyHaven disabled — enable in addon prefs")Related
Section titled “Related”- Bus tools — the control plane (
blender_send_message,blender_register_client, etc.) - Addon commands — the underlying
@commandhandler registry these tools wrap - Command dispatch vs script dispatch — when to use these tools vs
blender_send_messagewith a script - Resources —
blender://scene/*andblender://console/*share the same dispatch round-trip
A Supported Systems Joint
BlenderMCP is built and maintained by Supported Systems, a small studio that writes software for people who have to keep it running afterwards.
Visit supported.systems