Blender MCP Server
Use it to inspect, edit, render, and script a live Blender scene through MCP tools, with transaction and rollback support.
Inspect the active scene, including objects, collections, cameras, lights, render settings, and frame.
List and filter objects with paging, or get full detail for one object including transforms, materials, modifiers, and mesh stats.
Create primitive mesh objects: cube, sphere, cylinder, cone, plane, or torus.
Update object transforms, dimensions, visibility, name, material, or material color.
Delete objects by exact name.
Render the scene or produce small preview images, optionally returned as image content.
Execute Blender Python with a permission gate and AST policy screen.
Group edits with transactions and checkpoints, then commit or roll back changes.
Wait for scene changes and see which objects changed.
Inspect connected Blender instances and connection/takeover history.
Read scene, object, and latest-render state through MCP resources.
Allows AI clients to control a running Blender instance through MCP, enabling scene inspection, object creation and editing, rendering, and execution of Blender Python scripts.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Blender MCP Server create a low-poly wooden coffee table and render it"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 │
└─────────────┬──────────────┘
▼
BlenderThe 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 |
|
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 .envYou 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.mainIt 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
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__.pyat its root.In Blender: Edit → Preferences → Add-ons → Install… (or Install from Disk in 4.2+) and select
blender_mcp.zip.Enable the Development: Blender MCP check box.
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 ] │ └──────────────────────────┘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.mainor, equivalently, through the installed console script:
blender-mcp-serverIt opens two things at once:
the MCP stdio channel to the AI client, and
a WebSocket listener on
BLENDER_HOST:BLENDER_PORT(default127.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.mainChecking 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 transactionThey 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.
.envis 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 theenvblock 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_coloris 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]"
pytest295 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 transactionEach 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-demoScene 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}
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.

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
--pythonstartup script. The UI is not realised yet during startup and Blender 5.x segfaults.Region.active_panel_categoryis 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.pyacceptance_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 outtests/test_blender_integration.py— the operator layer against realbpy: 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 realbpymutates 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
bpymodule segfaults while tearing itself down at interpreter exit, after pytest has reported.tests/conftest.pyexits hard with pytest's own status so a green run stays green.
Troubleshooting
Symptom | Cause | Fix |
| The add-on is not connected | Blender → |
| Blender is busy or the add-on is disconnected mid-request | Check Blender's console; a modal dialog blocks the main thread |
| The server is not running, or on a different port | Start |
| The gate is off by default |
|
| The code hit the policy screen | The |
The AI client sees no tools | The server failed to start | Run |
Render fails with an OpenGL/EGL error | EEVEE needs a GPU context | Use |
| The id is version-specific |
|
MCP tools
Scene
Tool | Required | Optional | What it does |
| — |
| Compact summary of the active scene |
| — |
| Filtered, paged object list |
| — | — | Which Blender this server is attached to, and what happened to the others |
Object
Tool | Required | Optional | What it does |
|
| — | Full detail of one object, mesh statistics included |
|
|
| Create a primitive |
|
|
| Change only the fields given |
|
| — | Delete an object, by exact name |
Advanced and render
Tool | Required | Optional | What it does |
| — |
| Render the scene |
| — |
| Small render, returned as an image |
| — |
| Wait until something changes, or time out |
|
| — | 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 |
| — |
| Whether MPFB and its assets are installed; skins, parts, target categories |
|
|
| Build a human from a description |
|
| the same, minus | Change only what is given; parts are refitted |
|
|
| Multires detail on the body, decimated copies beside it |
Setting it up, in the Blender the add-on runs in:
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.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-checkUndo grouping
Tool | Required | Optional | What it does |
| — |
| Start a transaction |
|
| — | Name a point a later rollback can stop at |
| — | — | Keep the changes |
| — |
| 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.renderoverrides 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_objectdoes not usebpy.ops. Primitives are built withbmesh, so creation works the same in the UI and underblender --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")whenTableexists fails withOBJECT_ALREADY_EXISTS. It does not quietly createTable.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 |
|
| Scene name, object list with transforms, collections, cameras, lights, render settings |
|
| The same object list, ready to quote |
|
| 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.
bpystores 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 |
| No object with that name |
| Refused rather than silently renamed |
| Unknown primitive |
| Bad vector, empty name, unknown engine |
| No add-on is attached to the bridge |
| Blender itself raised |
| Blender did not answer in time |
|
|
|
|
| Code blocked by the Python policy screen |
| The socket dropped mid-request |
| A frame could not be parsed |
| The add-on does not implement that action |
| A transaction is already open |
| Commit or rollback without a |
| Anything unclassified |
| MPFB is not installed or not enabled in this Blender |
| 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 |
| read |
|
| read |
|
| read |
|
| read | — (the answer carries the pid, version and file: the add-on's identity) |
| read | — |
| read |
|
| write |
|
| write |
|
| write |
|
| write |
|
| write |
|
| read | — |
| write |
|
| write |
|
| write |
|
| write | — |
| write |
|
| read |
|
| write |
|
| write |
|
| write |
|
"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 |
|
Blender never answers |
|
Blender disconnects mid-request | every pending request fails with |
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 |
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 to127.0.0.1. Keep it there. It has no authentication of any kind: anything that can reach the port can drive your Blender, includingblender.execute_pythonif you have enabled it.The MCP server speaks stdio, so it is only reachable by whoever can spawn the process.
MCP_TRANSPORT=streamable-httpexists 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_pythonis 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. KeepALLOW_PYTHON_EXECUTION=falseunless 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:
Permission.
ALLOW_PYTHON_EXECUTION=false(the default) refuses the call withPYTHON_EXECUTION_DISABLEDbefore the socket is touched.AST screen.
server/validation/python.pyparses the code and rejects imports and calls that have no business in a 3D scripting session:os,subprocess,shutil.rmtree,socket,requests,urllib,pathlibwrites,open,eval,exec,__import__,builtins,pickle,ctypes,importlib, dunder escapes such as__globals__and__subclasses__, and thebpy.ops.wmcalls 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:
blender.get_scene/get_objects/get_objectto read the scenecreate_object/update_object/delete_objectto change itblender.begin_transaction…commit_transactionto group the changesblender.renderto look at the resultblender.execute_pythononly 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 openThe 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 |
|
| Address the add-on connects to |
|
| Bridge port |
|
|
|
|
| Bind address for HTTP transport |
|
| Bind port for HTTP transport |
|
| Seconds to wait for a response |
|
| Socket connect timeout used by the add-on |
|
| Seconds to wait for a render |
|
| Gate for |
|
| Let a newly connected Blender replace the attached one |
| all | Comma-separated allowlist of tool names |
|
|
|
| see |
|
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_objectRead-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 |
| The MCP tool name |
| The MCP request id, so a line can be tied to a client trace |
| Wall-clock time of the tool call |
|
|
| 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 serverThe suite is layered to match the architecture:
File | Covers |
| Request/response/error serialisation, malformed frames, action classification |
| The Python policy screen: what is allowed, what is blocked, why |
| Each tool's request shape, and the error it returns |
| The add-on's real WebSocket client against the real server: handshake, framing, pings, timeouts, disconnects, takeover |
| A full MCP session against a Blender double over a real socket, plus request logging through the real registry |
| The add-on's stdlib protocol copy has not drifted from the server's |
| The layering rules, and that every module compiles |
| One log line per call: id, duration, outcome, error code, and that the signature survives instrumentation |
|
|
| Needs |
| Needs |
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_materialand the node properties models actually reach for (metallic,roughness, node graphs). Todayupdate_object(material=...)assigns by name and creates a flat colour, which covers "make this red" and nothing moreModifiers — 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_pythonterritoryAnimation — 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 contentVision 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 hereTransaction 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 == ...anywhereno 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
|
BlenderThe 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 toolsblender.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
Scored across 15 tools
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.
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.
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.
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
Connect your AI assistant to Blender for 3D modeling, Geometry Nodes, materials, lighting, animation, rendering and export. Inspect real scenes and viewport images, refine the result and restore checkpoints. OAuth connects your chat to Blender Bridge Desktop on an online Windows 10/11 x64 computer running Blender 4.2+. The chat can run on another device. Requires an active Blender Bridge account; 7-day trial available. AI model access is separate.
A comprehensive Model Context Protocol (MCP) server that enables AI assistants to interact with yo…
A comprehensive Model Context Protocol (MCP) server that enables AI assistants to control Unreal E…
Blender-as-a-service for agents: search 3D assets, run Blender Python, or brief the studio agent.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables 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.36MIT
- AlicenseNot gradedqualityDmaintenanceEnables AI to control Blender 3D through the Model Context Protocol, allowing Python execution, scene state queries, and automation of 3D workflows.5GPL 3.0
- AlicenseCqualityDmaintenanceMCP server that enables AI to control Blender 3D, providing 175 typed tools for objects, materials, animation, compositing, and more via the Model Context Protocol.1002MIT
- AlicenseNot gradedqualityDmaintenanceConnects 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.1MIT