Skip to main content
Glama
Blender MCP Server

Blender MCP Server

by BlkDem

Blender MCP Server

Blender MCP Server lets AI clients control a running Blender through the Model Context Protocol. Point Claude, Cursor, OpenCode or any other MCP client at it, and the model can inspect the scene, create and edit objects, run renders, and execute Blender Python.

Everything runs locally. There is no cloud component, no account, no database.

User
 │
 ▼
AI client  ── "make me a table"
 │
 │ MCP (stdio)
 ▼
┌────────────────────────────┐
│  Blender MCP Server        │
│                            │
│  Tools      blender.*      │
│  Resources  blender://     │
│  Validation AST policy     │
└─────────────┬──────────────┘
              │ WebSocket (JSON)
              ▼
┌────────────────────────────┐
│  Blender add-on            │
│                            │
│  main-thread dispatch      │
│  bpy / bmesh / operators   │
└─────────────┬──────────────┘
              ▼
           Blender

The two processes are separate on purpose. The MCP server never imports bpy; the add-on never imports the MCP SDK. They meet at one JSON protocol over a WebSocket, which is what makes each half testable on its own and what will let the transport change later without touching either side.


Table of contents


Related MCP server: bMCP

Requirements

Python

3.11 or newer

Blender

3.6 or newer (4.x recommended)

MCP SDK

mcp 2.x — the current major version

Blender add-on dependencies

none (standard library only)

The add-on deliberately has no pip dependencies. Blender's bundled Python ships neither websockets nor pydantic, so addon/blender_mcp/websocket.py is a small RFC 6455 client built on socket, and protocol.py is a plain-dict mirror of the server's models. Nothing needs installing into Blender itself.

Installation

git clone https://github.com/your-org/blender-mcp.git
cd blender-mcp

python3 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate

pip install -e .

Optionally copy the example environment file and edit it:

cp .env.example .env

You can also install with the dev extras, which bring in pytest and the linters:

pip install -e ".[dev]"

Check that the server starts:

python -m server.main

It will print a startup line to stderr and then sit waiting on stdio, which is normal: with no AI client attached, silence is the correct behaviour. Watch the logs from another terminal while you start Blender.

Installing the Blender add-on

  1. Zip the add-on package including the top-level folder:

    cd addon
    zip -r blender_mcp.zip blender_mcp -x '*.pyc' '*__pycache__*'

    The archive must contain blender_mcp/__init__.py at its root.

  2. In Blender: Edit → Preferences → Add-ons → Install… (or Install from Disk in 4.2+) and select blender_mcp.zip.

  3. Enable the Development: Blender MCP check box.

  4. Open the 3D Viewport, press N, and switch to the Blender MCP tab:

    ┌──────────────────────────┐
    │ Blender MCP              │
    │                          │
    │ Status: Connected        │
    │ Server: 127.0.0.1:8765   │
    │                          │
    │ Host: [127.0.0.1      ]  │
    │ Port: [8765            ]  │
    │                          │
    │ [      Connect      ]    │
    └──────────────────────────┘
  5. Press Connect. The status turns green once the add-on has reached the MCP server; it retries automatically with a backoff if the server is not up yet, so you can start Blender first and the server second.

Blender must be the one holding the connection: the MCP server only ever dials nothing, it listens.

Running the server

The server speaks stdio, which is what MCP clients spawn:

python -m server.main

or, equivalently, through the installed console script:

blender-mcp-server

It opens two things at once:

  • the MCP stdio channel to the AI client, and

  • a WebSocket listener on BLENDER_HOST:BLENDER_PORT (default 127.0.0.1:8765) for the add-on.

Both start and stop with the process, and all logs go to stderr — stdout belongs to the protocol.

For debugging you can also serve MCP over HTTP instead:

MCP_TRANSPORT=streamable-http MCP_PORT=8000 python -m server.main

Checking the bridge without an AI client

Two scripts speak the bridge protocol directly:

python examples/create_cube.py     # one cube, one round trip
python examples/create_scene.py    # a table, several requests, in a transaction

They are clients of the bridge, not of Blender, so they run anywhere Python 3.11 is available. See Testing it for more ways to check a change.

Connecting an AI client

Add the server to your client's MCP configuration. The command is the interpreter from your virtual environment, so the client can find the dependencies.

Claude Desktop — claude_desktop_config.json (~/Library/Application Support/Claude/ on macOS, %APPDATA%\Claude\ on Windows):

{
  "mcpServers": {
    "blender": {
      "command": "/absolute/path/to/blender-mcp/.venv/bin/python",
      "args": ["-m", "server.main"],
      "env": {
        "BLENDER_HOST": "127.0.0.1",
        "BLENDER_PORT": "8765",
        "ALLOW_PYTHON_EXECUTION": "true"
      }
    }
  }
}

Cursor — .cursor/mcp.json in your project, or the global ~/.cursor/mcp.json:

{
  "mcpServers": {
    "blender": {
      "command": "/absolute/path/to/blender-mcp/.venv/bin/python",
      "args": ["-m", "server.main"],
      "env": {
        "ALLOW_PYTHON_EXECUTION": "true"
      }
    }
  }
}

OpenCode — opencode.json:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "blender": {
      "type": "local",
      "command": ["/absolute/path/to/blender-mcp/.venv/bin/python", "-m", "server.main"],
      "enabled": true,
      "environment": {
        "ALLOW_PYTHON_EXECUTION": "true"
      }
    }
  }
}

Restart the client after editing. If a tool call fails with NOT_CONNECTED, the bridge is up but the add-on is not connected — check the Blender panel.

.env is read relative to the server process's working directory, and an MCP client chooses that for you. For a setup you can rely on, pass the settings in the env block of the client's MCP configuration, as above.

First test

Say to your AI client:

Create a red cube at the origin.

A good model does this in two calls:

blender.create_object  {"type": "cube", "name": "RedCube", "location": [0, 0, 0]}
blender.execute_python
import bpy

cube = bpy.data.objects["RedCube"]
material = bpy.data.materials.new("Red")
material.use_nodes = True
material.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (0.8, 0.05, 0.05, 1.0)
material.diffuse_color = (0.8, 0.05, 0.05, 1.0)   # so the solid viewport matches
cube.data.materials.append(material)

result = {"object": cube.name, "material": material.name}

Setting only material.diffuse_color is the classic mistake: it colours the solid viewport but leaves the Principled BSDF at its default grey, so a render comes out white. Set both, as above.

and answers with the first response, not the whole bpy object dump:

{
  "success": true,
  "object": {
    "name": "RedCube",
    "type": "MESH",
    "location": [0.0, 0.0, 0.0],
    "scale": [1.0, 1.0, 1.0],
    "dimensions": [2.0, 2.0, 2.0]
  }
}

Then ask it to inspect the current scene and render, and watch the viewport.

Testing it

There are four levels, from cheapest to most convincing. Pick how far down you want to go.

1. The test suite, no Blender needed

pip install -e ".[dev]"
pytest

295 tests, ~12 s, no Blender required. These cover the protocol, the WebSocket transport (driving the add-on's own client against the real server), every tool, the policy screen, and a full MCP session over a real socket. See Development for what each file covers.

2. The bridge, without an AI client

Two scripts speak the bridge protocol directly, so you can prove the server and the add-on are talking to each other before involving a model:

python examples/create_cube.py     # one request, one object
python examples/create_scene.py    # a table, several requests, grouped in a transaction

Each prints what Blender answered and exits non-zero on failure.

3. A real scene, headless

Builds a still life in Blender, saves a .blend and renders it — no GUI, no AI client, just the add-on's operator layer doing real bpy work:

# with the Blender app
blender --background --python examples/test_scene.py -- --out /tmp/mcp-demo

# or with the bpy Python module, no Blender install needed
pip install bpy==4.2.0        # must match your Python; 4.2.0 is the 3.11 build
python -m bpy --background --python examples/test_scene.py -- --out /tmp/mcp-demo
Scene built in 0.04s: 13 objects
  Ball       MESH   [0.5, 0.5, 0.5]
  Cone       MESH   [0.24, 0.24, 0.36]
  Cup        MESH   [0.24, 0.24, 0.36]
  Floor      MESH   [16.0, 16.0, 0.0]
  LegBL      MESH   [0.16, 0.16, 1.4]
  ...
  transaction: {'success': True, 'transaction': 'committed', 'undo_steps': 4}
Saved /tmp/mcp-demo/test_scene.blend
Rendered /tmp/mcp-demo/test_scene.png in 5.07s -> {'engine': 'CYCLES', 'resolution': [640, 400], 'samples': 32}

the scene the script builds

Every primitive type, a material pass, a transaction, and a Cycles render. If this produces an image, blender.create_object, blender.update_object, blender.begin_transaction and blender.render all work.

4. The whole stack, in a real Blender window

The three levels above need no AI client. This one is the real thing: an MCP client drives a running Blender GUI and photographs the result.

Blender 5.2 with the add-on connected and a scene built through MCP

Two scripts, in two processes, exactly like a real setup:

# terminal 1 — the AI client, which also starts the MCP server
python examples/mcp_client_demo.py \
    --out /tmp/mcp-demo \
    --blender "blender --python examples/gui_blender_demo.py -- --out /tmp/mcp-demo"

# Windows
python examples\mcp_client_demo.py --out C:\temp\mcp-demo --blender ^
    '"C:\Program Files\Blender Foundation\Blender 5.2\blender.exe" --python ^
     C:\path\to\blender-mcp\examples\gui_blender_demo.py -- --out C:\temp\mcp-demo'

mcp_client_demo.py spawns server.main over stdio, waits for the add-on to connect, then builds a table through the tools and writes a flag file. gui_blender_demo.py runs inside Blender: it enables the add-on, connects it, opens the Blender MCP tab, and takes the screenshot once the flag appears.

Output lands in --out: screenshot.png, mcp_demo.blend, gui.log.

Two Blender rules that this exercise pins down, both of which cost a crash to learn:

  • Do scene work in a timer, not in a --python startup script. The UI is not realised yet during startup and Blender 5.x segfaults.

  • Region.active_panel_category is read-only until the region has been drawn with panels in it. Open the sidebar, redraw, then select the tab.

5. The acceptance scenario

Two scripts, and they check the same things at different levels:

# in-process, against a real bpy: the operator layer
blender --background --python examples/acceptance_check.py

# the whole chain: MCP client -> server -> WebSocket -> add-on -> bpy
python examples/mcp_acceptance.py --port 8774 \
    --blender-arg=blender --blender-arg=--background \
    --blender-arg=--python --blender-arg=examples/blender_attach.py

acceptance_check.py prints a PASS/FAIL line per step and exits non-zero on failure. mcp_acceptance.py does the same over MCP, and additionally asserts the tool surface, the resources, the error codes and the transaction bookkeeping. Together they are the definition of done for this project: every tool, every resource, the idempotency rule, five error paths, a render and an undo group.

6. The full stack against real Blender, in CI

bpy installed → two more test modules switch themselves on and test the real thing, headless:

pip install bpy==4.2.0
pytest                          # 387 tests without bpy, plus 99 against real bpy
BLENDER_MCP_SKIP_BPY=1 pytest   # 295, opt the real-Blender ones back out
  • tests/test_blender_integration.py — the operator layer against real bpy: every primitive at Blender's own default size, transform reporting, undo and transaction rollback, execute_python, and a real Cycles render written to disk.

  • tests/test_blender_bridge.py — the whole chain: a real MCP client calls tools, the real server forwards them over a real WebSocket, the add-on's own client receives them, and real bpy mutates a real scene. Includes the build-a-table example from below, and 10 concurrent requests to prove responses stay correlated.

The one thing this cannot cover headlessly is Blender's timer scheduler, which needs the GUI event loop. The tests call the dispatch function directly instead — same function, same thread, same queue.

The bpy module segfaults while tearing itself down at interpreter exit, after pytest has reported. tests/conftest.py exits hard with pytest's own status so a green run stays green.

Troubleshooting

Symptom

Cause

Fix

NOT_CONNECTED on every tool

The add-on is not connected

Blender → N → Blender MCP → Connect; check the port matches BLENDER_PORT

TIMEOUT

Blender is busy or the add-on is disconnected mid-request

Check Blender's console; a modal dialog blocks the main thread

Connection lost / Reconnecting in the panel

The server is not running, or on a different port

Start python -m server.main; the add-on retries with a backoff on its own

PERMISSION_DENIED from execute_python

The gate is off by default

ALLOW_PYTHON_EXECUTION=true

VALIDATION_ERROR from execute_python

The code hit the policy screen

The violations list names the line and the reason

The AI client sees no tools

The server failed to start

Run python -m server.main by hand and read stderr; stdout is the protocol

Render fails with an OpenGL/EGL error

EEVEE needs a GPU context

Use engine: "CYCLES" (and set scene.cycles.device = "CPU") when headless

INVALID_PARAMETER: Unknown render engine

The id is version-specific

blender.get_scene reports render.engines; the error also lists known_ids. EEVEE is BLENDER_EEVEE in 5.x and BLENDER_EEVEE_NEXT in 4.x

MCP tools

Scene

Tool

Required

Optional

What it does

blender.get_scene

—

object_limit

Compact summary of the active scene

blender.get_objects

—

type, collection, name_contains, limit, offset

Filtered, paged object list

blender.get_instances

—

—

Which Blender this server is attached to, and what happened to the others

Object

Tool

Required

Optional

What it does

blender.get_object

name

—

Full detail of one object, mesh statistics included

blender.create_object

type, name

location, rotation, scale, collection

Create a primitive

blender.update_object

name

location, rotation, scale, dimensions, visibility, new_name, material, material_color

Change only the fields given

blender.delete_object

name

—

Delete an object, by exact name

Advanced and render

Tool

Required

Optional

What it does

blender.render

—

engine, resolution_x, resolution_y, samples, output_path

Render the scene

blender.render_preview

—

max_edge, engine, samples, include_image

Small render, returned as an image

blender.wait_for_change

—

timeout, objects, include_image

Wait until something changes, or time out

blender.execute_python

code

—

Run Python inside Blender; a power-user escape hatch, not the normal path

Humans

Realistic human bodies, built with MPFB (MakeHuman for Blender) inside the connected Blender. A model describes the body — sex, age in years, height in metres, how muscular and how heavy — and the add-on builds it with skin, eyes and brows.

Tool

Required

Optional

What it does

blender.get_human_options

—

section

Whether MPFB and its assets are installed; skins, parts, target categories

blender.create_human

name

location, gender, age_years, height_m, muscle, weight, proportions, cupsize, firmness, race, targets, skin, skin_type, eyes, eyebrows, eyelashes, hair

Build a human from a description

blender.update_human

name

the same, minus location

Change only what is given; parts are refitted

blender.add_human_lod

name

subdivision_levels, low_ratios, spacing_m

Multires detail on the body, decimated copies beside it

Setting it up, in the Blender the add-on runs in:

  1. Edit → Preferences → Get Extensions, search MPFB, install it, and make sure it is ticked under Add-ons. The tools never enable it themselves; without it they answer HUMAN_GENERATOR_UNAVAILABLE.

  2. Download the MakeHuman system assets zip and install it from the 3D view sidebar: MPFB tab → Start here → Install pack from zip. Without it a body is built bare, and asking for a skin or a part answers HUMAN_ASSETS_MISSING.

targets is MPFB's fine shaping, by category: {"nose-volume-decr-incr": 0.4}. A category with two directions takes −1..1 (negative is the first word), one with a single shape 0..1; get_human_options(section="nose") lists a section's categories. skin: "auto" (the default) picks a skin by sex, age and race, and picks again when those change. A rollback undoes a created human, its parts and its LOD copies, but not a shape change made by update_human.

examples/human_check.py builds a line-up of humans through the add-on and renders it, for checking the result by eye:

blender --background --python examples/human_check.py -- --out ~/human-check

Undo grouping

Tool

Required

Optional

What it does

blender.begin_transaction

—

label

Start a transaction

blender.checkpoint

label

—

Name a point a later rollback can stop at

blender.commit_transaction

—

—

Keep the changes

blender.rollback_transaction

—

to

Undo the transaction, or just the stage after a checkpoint

create_object accepts cube, sphere, cylinder, cone, plane and torus, in any case. The enum is in the tool's JSON schema, so a model cannot invent a primitive that does not exist, and the case is normalised for it.

get_scene returns the whole scene; get_objects is the one to reach for when there are hundreds of objects, because it filters and pages:

{
  "objects": [{"name": "LegFL", "type": "MESH", "...": "..."}],
  "count": 1, "total": 42, "offset": 0, "limit": 50, "truncated": true
}

total is the number of matches before paging and truncated says whether more remain, so a model can tell that it has seen everything without guessing.

get_scene sends an object list too, capped at object_limit (200 by default, 1000 at most) and saying so with objects_shown and objects_truncated. A scene with ten thousand objects is a few hundred KB of JSON; a model that wants the rest asks for it with get_objects instead of having it forced on every scene read.

Looking at what you built

blender.render_preview renders small and returns the image itself, so a vision-capable model can check its own work:

{"success": true, "output_path": "/tmp/blender/preview.png", "render_time": 0.42}

followed by a PNG content block. It is capped at 2048 px on the long edge and defaults to 512, because a render is expensive in time and an image is expensive in context. blender.render is still the tool for the picture the user will keep.

One path caveat, because it is a trap: the add-on and the MCP server have to share a filesystem for a path to mean anything. Normally they do. If you run Blender on Windows and the server in WSL — or the other way round — a path the server sends is a path the other side cannot resolve, and a render fails with "cannot save". render_preview and blender://render/latest avoid the problem by using Blender's own temp directory, because the add-on names the file and reports the absolute path back. The images work across that boundary; an explicit output_path does not.

blender.wait_for_change is the companion: after a change you are unsure about, wait instead of re-reading the whole scene. It returns as soon as Blender reports a change, and tells you which objects changed:

{"changed": true, "changes": 2, "waited": 0.3, "changed_objects": ["Cube"], "watched": "scene"}

It notices edits the user makes in the UI as well as the ones the tools make, because a user dragging an object is the most common reason to need a second look. It is a long poll rather than a push: this SDK can only publish notifications/resources/updated from inside a request, and a client that wants to be told something changed gets the same answer either way.

Examples

blender.get_scene — the whole scene, no mesh data:

{
  "scene": "Scene",
  "objects_count": 2,
  "frame": 1,
  "active_object": "TableTop",
  "active_camera": "Camera",
  "render_engine": "CYCLES",
  "objects": [
    {
      "name": "TableTop", "type": "MESH",
      "location": [0.0, 0.0, 0.75], "rotation": [0.0, 0.0, 0.0],
      "scale": [1.6, 0.8, 0.05], "dimensions": [3.2, 1.6, 0.1], "visible": true
    }
  ],
  "collections": ["Collection"],
  "cameras": ["Camera"],
  "lights": [{"name": "Key", "type": "AREA", "energy": 400.0}],
  "render": {"engines": ["CYCLES", "BLENDER_EEVEE"], "resolution": [1920, 1080]}
}

blender.get_objects — filtered and paged:

blender.get_objects {"type": "MESH", "name_contains": "leg", "limit": 2}

blender.create_object:

blender.create_object {"type": "CUBE", "name": "Table", "location": [0, 0, 1], "scale": [2, 1, 1]}

blender.update_object — rename and material in one call:

blender.update_object {"name": "Table", "new_name": "TableTop"}
blender.update_object {"name": "TableTop", "material": "Wood"}
blender.update_object {"name": "TableTop", "material": "Red", "material_color": [0.8, 0.05, 0.05]}

material is created when it does not exist, and the response says so, so a model never has to guess whether the colour landed:

{"success": true, "object": {"...": "..."}, "material": "Red", "material_created": true}

blender.delete_object:

blender.delete_object {"name": "TableTop"}

blender.render:

blender.render {"resolution_x": 1024, "resolution_y": 1024}

Responses

Success:

{"success": true, "object": {"name": "Table", "type": "MESH", "location": [0, 0, 1]}}

Failure — the call is marked failed and the body stays machine-readable:

{"success": false, "error": {"code": "OBJECT_NOT_FOUND", "message": "Object 'Chair' does not exist"}}

Every tool returns small JSON summaries. Vertices, polygons, face loops and raw bpy dumps are never sent, whatever the caller asks for; use blender.execute_python and return a result variable when you need more.

Two behaviours worth knowing:

  • blender.render overrides are temporary. Engine, resolution, samples and output path are restored to whatever the scene had once the image is written, so rendering twice gives the same result and a user's Cycles setup is not silently switched to EEVEE. The response reports what was actually used:

    {
      "success": true,
      "output_path": "/home/you/render.png",
      "render_time": 3.42,
      "used": {"engine": "CYCLES", "resolution": [1024, 1024], "samples": 64},
      "settings_restored": true
    }
  • blender.create_object does not use bpy.ops. Primitives are built with bmesh, so creation works the same in the UI and under blender --background, where operators that need a window context cannot run.

Idempotency

An agent may repeat a call. Two rules, both about names:

  • create_object(name="Table") when Table exists fails with OBJECT_ALREADY_EXISTS. It does not quietly create Table.001, because the model asked for an object by name and would then go on to refer to a name that does not exist. Delete and re-create is two deliberate calls.

  • update_object(new_name=...) onto a taken name fails the same way, for the same reason. Renaming an object to the name it already has is a no-op that succeeds, so a retried rename is harmless.

delete_object matches the exact name only. There is no fuzzy matching: a model that meant Table must not delete Table.001 by accident.

MCP resources

Resources are for state you want without spending a tool call.

URI

MIME

Contents

blender://scene

application/json

Scene name, object list with transforms, collections, cameras, lights, render settings

blender://objects

application/json

The same object list, ready to quote

blender://render/latest

image/png

The most recent render, as image bytes

blender://render/latest is empty until something has been rendered, and it reports "no rendered image" rather than inventing one. It also stops offering a render once the file it was made in has been closed: the picture belongs to that scene, and serving it under a caption it does not match is worse than serving nothing.

// blender://objects
{
  "scene": "Scene",
  "objects_total": 1,
  "objects": [
    {
      "name": "RedCube",
      "type": "MESH",
      "location": [0.0, 0.0, 0.0],
      "rotation": [0.0, 0.0, 0.0],
      "scale": [1.0, 1.0, 1.0],
      "dimensions": [2.0, 2.0, 2.0]
    }
  ],
  "collections": ["Collection"],
  "cameras": ["Camera"],
  "lights": []
}

blender.get_object is the same idea for a single object, plus materials, modifiers, visibility and parent.

Planned, and already shaped for by the resource layer: blender://materials, blender://collections, blender://cameras, blender://render/latest.

Example AI workflows

1. "Create a cube named Box at the origin."

blender.create_object {"type": "cube", "name": "Box", "location": [0, 0, 0]}

One call.

2. "Create three cylinders in a row."

blender.begin_transaction {}
blender.create_object {"type": "cylinder", "name": "Cyl1", "location": [-1, 0, 0]}
blender.create_object {"type": "cylinder", "name": "Cyl2", "location": [ 0, 0, 0]}
blender.create_object {"type": "cylinder", "name": "Cyl3", "location": [ 1, 0, 0]}
blender.commit_transaction {}

The transaction means one bad placement can be undone with a single blender.rollback_transaction instead of three deletes.

3. "Inspect the current scene and tell me what objects are present."

blender.get_scene {}

Then, if the model needs detail on one of them:

blender.get_object {"name": "TableTop"}

4. "Create a simple table with a wooden top and four legs."

blender.begin_transaction {}
  blender.create_object {"type":"cube","name":"TableTop","location":[0,0,0.75],"scale":[2,1,0.05]}
  blender.create_object {"type":"cube","name":"LegFL","location":[-0.9,-0.4,0.35],"scale":[0.1,0.1,0.7]}
  blender.create_object {"type":"cube","name":"LegFR","location":[ 0.9,-0.4,0.35],"scale":[0.1,0.1,0.7]}
  blender.create_object {"type":"cube","name":"LegBL","location":[-0.9, 0.4,0.35],"scale":[0.1,0.1,0.7]}
  blender.create_object {"type":"cube","name":"LegBR","location":[ 0.9, 0.4,0.35],"scale":[0.1,0.1,0.7]}
blender.commit_transaction {}
blender.execute_python
    import bpy
    wood = bpy.data.materials.new("Wood")
    wood.diffuse_color = (0.32, 0.19, 0.07, 1.0)
    for name in ("TableTop", "LegFL", "LegFR", "LegBL", "LegBR"):
        obj = bpy.data.objects.get(name)
        if obj is not None:
            obj.data.materials.append(wood)
    result = {"materialised": 5}
blender.render {"resolution_x": 800, "resolution_y": 600}

That chain — create, transform, materialise, render — is the shape a future autonomous agent will follow too.

Units and conventions

  • Lengths are Blender units (metres), at every tool and in every payload.

  • Rotations are degrees in XYZ Euler order, everywhere. bpy stores radians; the conversion happens inside the add-on so models never have to think about it. This is the single most common source of nonsense output from a model driving a 3D tool, so it is worth being explicit about.

  • Sizes are reported as dimensions, the world-space bounding box.

  • Names are Blender data-block names, unique per type.

Error handling

Failures are structured, with a stable error.code to branch on:

Code

Meaning

OBJECT_NOT_FOUND

No object with that name

OBJECT_ALREADY_EXISTS

Refused rather than silently renamed

INVALID_OBJECT_TYPE

Unknown primitive

INVALID_PARAMETER

Bad vector, empty name, unknown engine

BLENDER_NOT_CONNECTED

No add-on is attached to the bridge

BLENDER_OPERATION_FAILED

Blender itself raised

TIMEOUT

Blender did not answer in time

PYTHON_EXECUTION_DISABLED

ALLOW_PYTHON_EXECUTION=false

PYTHON_EXECUTION_ERROR

execute_python code raised

VALIDATION_ERROR

Code blocked by the Python policy screen

CONNECTION_LOST

The socket dropped mid-request

MALFORMED_MESSAGE

A frame could not be parsed

UNKNOWN_ACTION

The add-on does not implement that action

TRANSACTION_ACTIVE

A transaction is already open

TRANSACTION_NOT_ACTIVE

Commit or rollback without a begin

INTERNAL_ERROR

Anything unclassified

HUMAN_GENERATOR_UNAVAILABLE

MPFB is not installed or not enabled in this Blender

HUMAN_ASSETS_MISSING

A skin or part was asked for without the MakeHuman asset pack

Tracebacks are logged on the server and in Blender's console; the model gets the exception type, message and a short traceback tail, which is what it needs to fix its own mistake. Full tracebacks are not shipped to the AI.

The bridge protocol

One JSON object per WebSocket text frame. Every request carries a unique id, and exactly one response is sent per request.

Request:

{"id": "3f2a…", "action": "create_object", "params": {"type": "cube", "name": "Box"}}

The dispatch field is action; the add-on also accepts method as a synonym, so a peer written against the other common spelling interoperates.

Success:

{"id": "3f2a…", "success": true, "result": {"object": {"name": "Box"}}}

Error:

{
  "id": "3f2a…",
  "success": false,
  "error": {"code": "OBJECT_NOT_FOUND", "message": "Object 'Box' does not exist", "details": {}}
}

Actions

Action

Kind

Params

get_scene

read

object_limit

get_objects

read

type, collection, name_contains, limit, offset

get_object

read

name

ping

read

— (the answer carries the pid, version and file: the add-on's identity)

change_count

read

—

changes

read

since

create_object

write

type, name, location, rotation, scale, collection

update_object

write

name + any of location, rotation, scale, dimensions, visibility, new_name, material, material_color

delete_object

write

name

render

write

engine, resolution_x, resolution_y, resolution_percentage, samples, output_path

render_preview

write

resolution_x, resolution_y, engine, samples

last_render

read

—

execute_python

write

code

begin_transaction

write

label

checkpoint

write

label

commit_transaction

write

—

rollback_transaction

write

to

get_human_options

read

section

create_human

write

name + body, skin and part fields (see Humans)

update_human

write

name + any body, skin or part field

add_human_lod

write

name, subdivision_levels, low_ratios, spacing_m

"write" means it pushes an undo step. Adding an action means adding an entry here, a handler in addon/blender_mcp/operators.py (the human actions live in addon/blender_mcp/humans.py), and — if the model should see it — a tool in server/mcp/tools/. The two protocol modules mirror each other field for field, and tests/test_addon_protocol.py fails if they drift.

There is one frame that is not a request: {"type": "disconnect", "reason": …}, which the server sends to refuse a Blender that is not the one already attached. It is how a refusal reaches the add-on instead of leaving it retrying a connection it will never be given.

Failure handling on the wire

Situation

What happens

Blender not connected

NOT_CONNECTED immediately, no socket involved

Blender never answers

TIMEOUT after BLENDER_REQUEST_TIMEOUT

Blender disconnects mid-request

every pending request fails with CONNECTION_LOST

Unparsable frame

logged and dropped; the connection survives

A second Blender connects

it takes over the bridge; the old one's pending requests fail fast

A handler raises

caught, logged with a traceback, returned as BLENDER_ERROR

Security

Read this before pointing anything other than your own machine at this server.

Network exposure

  • The WebSocket bridge binds to BLENDER_HOST, which defaults to 127.0.0.1. Keep it there. It has no authentication of any kind: anything that can reach the port can drive your Blender, including blender.execute_python if you have enabled it.

  • The MCP server speaks stdio, so it is only reachable by whoever can spawn the process. MCP_TRANSPORT=streamable-http exists for tooling and debugging; if you use it, bind it to loopback and put authentication in front of it. It has none of its own.

  • There is no auth, no multi-user support and no audit trail beyond the request log. That is deliberate for a local tool, not an oversight.

One Blender at a time

The bridge serves one Blender. A second one connecting is refused by default, with the reason sent to the add-on so its panel says why instead of retrying forever:

blender-4711 (pid 4711) is already connected. Close it, or start the server
with BLENDER_ALLOW_TAKEOVER=1 if you meant to replace it.

The alternative — letting the newcomer take the socket — is the failure mode this prevents: one user's tool calls quietly start landing in someone else's scene, and the symptom is impossible to trace back. Both instances are recorded either way, so blender.get_instances can explain a scene that changed unexpectedly:

{
  "active": {"id": "blender-4711", "status": "active", "pid": 4711, "blender_version": "5.2.2"},
  "takeover_allowed": false,
  "instances": [
    {"id": "blender-4711", "status": "active"},
    {"id": "blender-4822", "status": "refused", "reason": "..."}
  ]
}

A reconnect of the same process is not a takeover and is allowed: the pid identifies the instance, so a dropped socket is not mistaken for a second Blender.

If you must expose it

Don't, but if you do: keep it on a private network or behind an SSH tunnel, set ALLOW_PYTHON_EXECUTION=false, and treat the Blender process as compromised. blender.execute_python can run arbitrary code inside Blender, and Blender's Python can read your files.

Production notes

  • The request log is the only record of what a client did. It is one line per call, on stderr, and safe to ship to a collector.

  • Error messages sent to a client are structured and short. Tracebacks stay in the log, so a client cannot use an error to read the filesystem.

  • The add-on is standard-library only, has no network listener of its own, and only ever dials the bridge address you give it.

blender.execute_python

blender.execute_python is not a sandbox. It is intended for a trusted, local AI client. Blender's own Python API can already read and write files, open sockets and terminate the application; no amount of source screening changes that. Keep ALLOW_PYTHON_EXECUTION=false unless you trust the model and the prompt it is working from.

Two gates, both outside the tool so that no tool grows its own ad-hoc checks:

  1. Permission. ALLOW_PYTHON_EXECUTION=false (the default) refuses the call with PYTHON_EXECUTION_DISABLED before the socket is touched.

  2. AST screen. server/validation/python.py parses the code and rejects imports and calls that have no business in a 3D scripting session: os, subprocess, shutil.rmtree, socket, requests, urllib, pathlib writes, open, eval, exec, __import__, builtins, pickle, ctypes, importlib, dunder escapes such as __globals__ and __subclasses__, and the bpy.ops.wm calls that would replace or quit the file.

import bpy, import mathutils and the rest of the standard library stay available — they are what makes the tool useful at all. The screen is a filter against accidents, not a security boundary, and it is structured so a stricter policy can replace it later.

The code itself never runs in the MCP server. It is validated, forwarded, and executed by the add-on on Blender's main thread in a fresh namespace, where result is pre-defined and nothing leaks between calls.

When to use execute_python

It is a power-user escape hatch, not the intended path. In order of preference:

  1. blender.get_scene / get_objects / get_object to read the scene

  2. create_object / update_object / delete_object to change it

  3. blender.begin_transaction … commit_transaction to group the changes

  4. blender.render to look at the result

  5. blender.execute_python only for what the tools above do not cover

If a model is writing Blender Python for something a tool could do, that is a gap in the tools, not a reason to reach for step 5. The tools return compact, predictable JSON and push an undo step; a Python snippet can do neither for you.

Transactions, checkpoints and undo

Every mutating action pushes one Blender undo step, so a user can always step back by hand.

On top of that, four tools group a sequence:

blender.begin_transaction    → start recording
  …mutating calls…           → each object's state is captured before it changes
blender.checkpoint "shell"   → name a point in the middle
  …more calls…
blender.commit_transaction   → close it, keep the changes
blender.rollback_transaction → put everything back, or only what came after "shell"

Rollback restores recorded state — transforms, names, visibility, materials, collection membership — rather than replaying undo steps. That distinction matters: Blender's undo history is not readable from Python, so a count of steps is only right if nothing else touched the undo stack, and a user clicking around mid-transaction breaks that assumption. Restoring recorded state is exact for what the tools changed, whatever the user does in between. The undo steps stay on the stack for the human.

A checkpoint splits a transaction into stages, which is what a build with phases needs:

begin "layout"
  build the shell
checkpoint "shell"
  add the details
rollback to="shell"   → the details go, the shell stays, the transaction stays open

The limits, stated plainly: edits made through blender.execute_python are not recorded, so a transaction containing them restores only its tool calls — the rollback result says so in a note field rather than pretending otherwise. And an object deleted by a tool can be brought back, but only while its mesh data still exists; if Blender has purged it, the object comes back empty and the rollback result lists it under unrecoverable.

Configuration

All configuration is environment variables, read from the process environment or a .env file next to the server. See .env.example.

Variable

Default

Meaning

BLENDER_HOST

127.0.0.1

Address the add-on connects to

BLENDER_PORT

8765

Bridge port

MCP_TRANSPORT

stdio

stdio or streamable-http

MCP_HOST

127.0.0.1

Bind address for HTTP transport

MCP_PORT

8000

Bind port for HTTP transport

BLENDER_REQUEST_TIMEOUT

30.0

Seconds to wait for a response

BLENDER_CONNECT_TIMEOUT

5.0

Socket connect timeout used by the add-on

BLENDER_RENDER_TIMEOUT

600.0

Seconds to wait for a render

ALLOW_PYTHON_EXECUTION

false

Gate for blender.execute_python

BLENDER_ALLOW_TAKEOVER

false

Let a newly connected Blender replace the attached one

ENABLED_TOOLS

all

Comma-separated allowlist of tool names

LOG_LEVEL

INFO

DEBUG, INFO, WARNING, ERROR, CRITICAL

LOG_FORMAT

see .env.example

logging format string

Names are explicit about scope on purpose: BLENDER_REQUEST_TIMEOUT is the wait for a Blender response, BLENDER_RENDER_TIMEOUT is the longer one a render needs, and MCP_PORT only matters if you switch away from stdio.

ENABLED_TOOLS is for narrowing the surface, not for switching things on:

ENABLED_TOOLS=blender.get_scene,blender.get_objects,blender.get_object,blender.create_object

Read-only tools stay available whatever the list says, because a client that cannot read the scene cannot do anything sensible with it. An unknown name in the list is refused at startup rather than ignored, so a typo does not quietly remove a tool you meant to keep.

2026-09-27 12:30:21 INFO server.mcp.support: mcp tool=blender.create_object request=7 duration_ms=41.8 success=true
2026-09-27 12:30:24 INFO server.mcp.support: mcp tool=blender.render request=8 duration_ms=3012.4 success=true
2026-09-27 12:30:25 INFO server.mcp.support: mcp tool=blender.get_object request=9 duration_ms=2.1 success=false error=OBJECT_NOT_FOUND
2026-09-27 12:30:26 INFO server.blender.connection: Blender add-on connected from ('127.0.0.1', 51234)

One line per MCP call, in a fixed key=value shape, added by server/mcp/support.py at registration so no tool has to remember to do it:

Field

Meaning

tool

The MCP tool name

request

The MCP request id, so a line can be tied to a client trace

duration_ms

Wall-clock time of the tool call

success

true / false

error

The structured error code, when it failed

An expected failure is INFO with no traceback, because a model asking for an object that is not there is normal operation. A traceback appears only for something nobody anticipated, and it stays in the log rather than in the reply.

Project layout

blender-mcp/
├── README.md  LICENSE  pyproject.toml  .env.example  .gitignore
│
├── server/                      # runs as its own process; never imports bpy
│   ├── main.py                  # entry point: python -m server.main
│   ├── config.py                # Settings from the environment
│   ├── errors.py                # error codes and the structured envelope
│   ├── transactions.py          # begin / commit / rollback helpers
│   ├── mcp/
│   │   ├── server.py            # assembles tools, resources, lifespan
│   │   ├── support.py           # AppContext: how a tool reaches the bridge
│   │   ├── tools/               # scene, objects, render, python, transactions
│   │   └── resources/           # blender://scene, objects, render/latest
│   ├── blender/                 # the transport
│   │   ├── protocol.py          # pydantic request/response models
│   │   ├── client.py            # one connection, request/response correlation
│   │   └── connection.py        # the WebSocket server, one active add-on
│   └── validation/
│       └── python.py            # the AST policy screen
│
├── addon/blender_mcp/           # runs inside Blender; never imports the MCP SDK
│   ├── __init__.py              # bl_info, register(), unregister()
│   ├── protocol.py              # stdlib mirror of the server's protocol
│   ├── websocket.py             # minimal RFC 6455 client (no dependencies)
│   ├── connection.py            # socket thread, reconnect, main-thread dispatch
│   ├── operators.py             # every bpy operation
│   ├── humans.py                # human characters, through MPFB
│   ├── humans_catalog.py        # their arithmetic: ages, targets, skins (no bpy)
│   ├── executor.py              # executes model-written code
│   └── ui.py                    # N-panel and its operators
│
├── tests/                       # pytest; 387 tests without bpy, 99 more with it
│   └── support/                 # protocol double, stubs, the bpy gate
│
├── examples/                    # bridge scripts, acceptance checks, GUI demo
├── docs/                        # what the two demo scripts produce
└── …

The layering is enforced by tests, not just convention: tests/test_architecture.py fails the build if bpy appears anywhere under server/, or if the MCP SDK or pydantic appear anywhere in the add-on.

Development

pip install -e ".[dev]"

pytest                       # the whole suite
pytest tests/test_protocol.py -v
pytest -k "not subprocess"   # skip the spawned-server smoke test

ruff check .
ruff format .
mypy server

The suite is layered to match the architecture:

File

Covers

test_protocol.py

Request/response/error serialisation, malformed frames, action classification

test_validation.py

The Python policy screen: what is allowed, what is blocked, why

test_tools.py

Each tool's request shape, and the error it returns

test_transport.py

The add-on's real WebSocket client against the real server: handshake, framing, pings, timeouts, disconnects, takeover

test_end_to_end.py

A full MCP session against a Blender double over a real socket, plus request logging through the real registry

test_addon_protocol.py

The add-on's stdlib protocol copy has not drifted from the server's

test_architecture.py

The layering rules, and that every module compiles

test_request_logging.py

One log line per call: id, duration, outcome, error code, and that the signature survives instrumentation

test_entrypoint.py

python -m server.main spawned as a subprocess and driven over stdio

test_blender_integration.py

Needs bpy. The operator layer against real Blender: primitive sizes, transforms, get_objects paging, rename, materials, mesh statistics, undo, execute_python, a real render

test_blender_bridge.py

Needs bpy. The whole chain, MCP client → server → add-on → real scene

Where a real Blender is not available, the tests use a protocol double (tests/support/fake_blender.py): it speaks the real protocol over a real WebSocket using the add-on's own client, and keeps a small in-memory object store. It verifies the transport and the MCP surface. It does not verify bpy, and it is not dressed up as if it did — mesh generation, modifiers and rendering are exercised by running the real add-on in Blender.

Roadmap

Planned, roughly in the order they become useful:

  • Richer mesh operations — primitives beyond the six, booleans, extrusion, subdivision, mesh statistics per selection rather than per object

  • Materials — create_material and the node properties models actually reach for (metallic, roughness, node graphs). Today update_object(material=...) assigns by name and creates a flat colour, which covers "make this red" and nothing more

  • Modifiers — add, configure, order, remove

  • Geometry nodes — build and wire node trees

  • Textures and UV — image textures, UV operations, unwrapping

  • Collections — create, nest, move objects between them

  • Camera and light controls — add cameras, set the active one, place and tune lights, depth of field. Today this is execute_python territory

  • Animation — keyframes, drivers, frame ranges, playback control

  • Rigging — armatures and pose

  • Scene diff — "what changed since I last looked?", so a model can verify its own work instead of re-reading everything

  • Render resources — blender://render/latest, and the image returned as MCP image content

  • Vision feedback loop — blender.inspect_image: hand a render to a vision model and get critique back. This is the piece that closes the loop plan → build → render → look → correct → render, and it is why the render tool's response shape leaves room for the image itself. The vision model belongs to the client, not here

  • Transaction and undo — named checkpoints, selective undo

  • Batch operations — apply a list of operations in one call, with one undo step and per-item results

  • Blender event notifications — push notifications when the scene changes underneath the model, rather than having it poll

  • Authentication — a token on the bridge, so it can leave loopback

  • Multi-Blender — a bridge that routes to several instances by name

Out of scope on purpose, and not planned: distributed deployment, Docker, Kubernetes, multi-user, a database, a web dashboard, vector stores and RAG. This is a local MCP backend, and adding those would not make it better at being one.

Not this project's job

This is an MCP server and nothing more. It has no opinion about models, and it must stay that way for a client to be able to swap one out:

  • no OpenAI, Anthropic or Gemini SDK, no provider-specific branches, no if model == ... anywhere

  • no GUI toolkit, no chat, no benchmark harness

  • no Tripo or any other asset-provider integration

A separate GUI client may use blender-mcp as an MCP backend, exactly the way Claude Desktop or Cursor does:

                  Blender AI Studio            (a separate project)
                         |
                     AI Agent
                    /        \
                  LLM        Tools
                 /             \
        GPT / Claude /      MCP Client
        Gemini / etc.           |
                                v
                          blender-mcp
                                |
                             Blender

The agent on top may also call other providers — Tripo, a vision model — but it does that itself. blender-mcp only speaks MCP to Blender, which is why it can sit underneath any of them.

License

MIT — see LICENSE.

Available Tools

15 tools
blender.begin_transactionA

Open a transaction. Every mutating call until commit or rollback is recorded, and blender.rollback_transaction puts all of them back.

label: optional name for the starting point, e.g. "layout". Later rollbacks can return to it by name.

Fails with TRANSACTION_ACTIVE if one is already open.

Inputs: label. Returns: object

blender.checkpointA

Record a named point inside the open transaction, so a later rollback can stop there instead of unwinding everything.

Use it between stages of a build: begin -> build the shell -> checkpoint "shell" -> add the details -> commit. A rollback to "shell" then discards only the details.

label: required, and unique within the transaction.

Inputs: label (required). Returns: object

blender.commit_transactionA

Close the open transaction and keep the changes. The undo steps stay on Blender's undo stack, so the user can still step back by hand.

Fails with TRANSACTION_NOT_ACTIVE if no transaction is open.

Inputs: no named parameters. Returns: object

blender.create_objectA

Create a mesh object in the active scene.

type: one of cube, sphere, cylinder, cone, plane, torus. Case does not matter. name: must be unique; an existing name fails with OBJECT_ALREADY_EXISTS rather than being renamed to "Name.001". location / scale: Blender units (meters), as [x, y, z]. rotation: DEGREES as [x, y, z], applied in XYZ Euler order.

Example: {"type": "cube", "name": "TableTop", "location": [0, 0, 1], "scale": [2, 1, 0.1]}

Inputs: name (required), type (required), scale, location, +2 more. Returns: object

blender.delete_objectA

Delete an object from the active scene, checking first that it exists.

Fails with OBJECT_NOT_FOUND when the name is unknown.

Inputs: name (required). Returns: object

blender.execute_pythonA

Execute Python inside the running Blender and return whatever the snippet left in a variable named result.

code: statements to run. import bpy, import mathutils and the standard library are available. Anything that touches the network, spawns processes, reads or writes files, or reaches for eval/exec/import is rejected with VALIDATION_ERROR before Blender is contacted. Assign a JSON-serialisable value to result to get structured data back; otherwise only a summary is returned.

  Otherwise, if the server was started with ALLOW_PYTHON_EXECUTION=false,
  the call is refused with PYTHON_EXECUTION_DISABLED without contacting Blender.

Example: import bpy bpy.ops.mesh.primitive_uv_sphere_add(location=(0, 0, 1)) obj = bpy.context.object obj.name = "Ball" result = {"created": obj.name, "verts": len(obj.data.vertices)}

This tool is meant for a trusted, local AI client. It is a policy screen, not a sandbox: Blender's own API is powerful enough to do damage, and enabling this tool should be a deliberate choice.

Inputs: code (required). Returns: object

blender.get_instancesA

List the Blender instances that have connected to this server, newest first.

The active instance is the one every other tool acts on. The others are kept as history so a scene that changed unexpectedly can be explained: an entry with status "refused" or "replaced" means a second Blender tried to take the socket.

Answers even when no Blender is connected, which makes it a useful first call after a connection error.

Inputs: no named parameters. Returns: object

blender.get_objectA

Return the full detail of a single object: transform, dimensions, visibility, collection, material names, modifiers, and vertex/edge/polygon counts for meshes.

Fails with OBJECT_NOT_FOUND when the name does not exist.

Inputs: name (required). Returns: object

blender.get_objectsA

List objects with filters and a page window, for scenes too large for blender.get_scene to be useful.

type: object type to keep, e.g. "MESH", "CAMERA", "LIGHT". collection: only objects linked to this collection. name_contains: case-insensitive substring of the name. limit: page size, 1-500, default 50. offset: how many matches to skip, default 0.

Returns the page, plus "total" (matches before paging) and "truncated" (true when more objects remain) so you know whether to ask for the next page.

Inputs: type, limit, offset, collection, +1 more. Returns: object

blender.get_sceneA

Return a compact, LLM-sized summary of the active scene: object names, types, transforms and visibility, plus collections, cameras, lights, the active object, the render engine and the current frame.

Use this first to see what exists. At most object_limit objects are inlined (200 by default); when objects_truncated is true, use blender.get_objects to page through the rest. No mesh data is included.

Inputs: object_limit. Returns: object

blender.renderA

Render the active scene with the current camera and return the output path and the wall-clock render time.

engine: Blender engine id, e.g. "CYCLES" or "BLENDER_EEVEE" (4.x called EEVEE "BLENDER_EEVEE_NEXT"). blender.get_scene lists the ids this Blender accepts under render.engines. Omit to keep the scene's current engine. resolution_x / resolution_y: pixels. Omit to keep the scene's render size. samples: Cycles sample count; ignored by engines without sampling. output_path: absolute or Blender-relative path. Omit to use the scene's configured output path.

Render time grows quickly with resolution and samples; prefer 512-1024 px while iterating.

Inputs: engine, samples, output_path, resolution_x, +1 more. Returns: object

blender.render_previewA

Render a small preview and return the image, so you can look at what you built.

max_edge: longest edge in pixels, 64-2048, default 512. Bigger costs a lot of context, and a vision model rarely needs more to spot a mistake. engine: optional engine id; omit to keep the scene's current one. samples: optional sample count; omit for a cheap default. include_image: set false to get only the file path and no image.

Prefer this over blender.render while iterating: it is capped in size and samples and it comes back as an image. Use blender.render for a final image the user will keep.

Inputs: engine, samples, max_edge, include_image.

blender.rollback_transactionA

Put back everything the transaction changed.

to: optional checkpoint label. Omit it to rewind to the start of the transaction; name one to rewind only to that checkpoint.

Restores what the tools changed: transforms, names, visibility, materials and collection membership. Edits made through blender.execute_python are not tracked, so a transaction containing them rolls back only its tool calls.

Fails with TRANSACTION_NOT_ACTIVE if no transaction is open.

Inputs: to. Returns: object

blender.update_objectA

Change an existing object. Only the fields you pass are touched; omitted fields keep their current value.

location / scale: Blender units (meters) as [x, y, z]. rotation: DEGREES as [x, y, z]. dimensions: final bounding-box size in meters as [x, y, z] (replaces scale). visibility: hide or show the object in the viewport and renders. new_name: rename the object. Fails with OBJECT_ALREADY_EXISTS if that name is taken, rather than silently becoming "Name.001". material: material name to assign, replacing the object's material slots. Created if it does not exist yet; the response says whether it did, through "material_created". material_color: [r, g, b] or [r, g, b, a] in 0..1, used only when the material has to be created. Sets both the viewport colour and the shader, so the solid view and the render agree.

Inputs: name (required), scale, location, material, +5 more. Returns: object

blender.wait_for_changeA

Wait until the scene changes, or until timeout seconds pass.

Use it after a change you are unsure about, instead of reading the whole scene again: it returns as soon as Blender reports a change, and tells you what changed.

timeout: seconds to wait, 0-60, default 10. objects: optional object names to narrow the answer to. Omit to watch everything. include_image: set true to also return the last render, if there is one, so a wait-and-look step is a single call.

Returns {"changed": bool, "changes": int, "waited": seconds, "changed_objects": [...]}. changed false means nothing happened before the timeout, which is an answer rather than a failure.

Inputs: objects, timeout, include_image.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.3/5.0

    Scored across 15 tools

    Disambiguation5/5

    Each tool has a clearly distinct purpose, and the read-tool triad (get_scene for a compact overview, get_objects for paging, get_object for full single-object detail) is explicitly delineated in the descriptions. The render vs render_preview distinction is also spelled out ('prefer this over blender.render while iterating'). No meaningful overlap remains.

    Naming Consistency5/5

    All tools use snake_case with a consistent verb_noun or clear domain-noun pattern (get_objects, create_object, update_object, delete_object, begin_transaction, commit_transaction, rollback_transaction). Minor verb-only names like render and checkpoint fit naturally and introduce no inconsistency.

    Tool Count5/5

    At 15 tools the set is well-scoped: a coherent object-CRUD core, a self-consistent transaction group (begin/checkpoint/commit/rollback), rendering, an inspection tool, and a controlled Python escape hatch. Every tool earns its place without bloat.

    Completeness4/5

    Object CRUD, transactions, rendering, and inspection are fully covered for the domain. Minor gaps remain: create_object only makes mesh primitives (no dedicated camera/light creation), and there are no standalone collection or material lifecycle tools, though update_object and execute_python provide workarounds.

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      D
      maintenance
      Enables remote control of Blender via the Model Context Protocol, allowing users to execute Python scripts, query scene data, and generate 3D models from images. It provides a bridge for AI clients to interact directly with Blender's internal environment and automate 3D content creation.
      3
      6
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables AI to control Blender 3D through the Model Context Protocol, allowing Python execution, scene state queries, and automation of 3D workflows.
      5
      GPL 3.0
    • A
      license
      Not graded
      quality
      D
      maintenance
      Connects MCP-compatible clients to a live Blender scene for AI-assisted 3D workflows, enabling inspection and controlled operations on objects, materials, cameras, lights, render settings, animation, UVs, Geometry Nodes, imports, exports, and Python execution.
      1
      MIT