Skip to content
Go back

Blender MCP vs Headless bpy Scripts

By KingPin 14 min read
Blender MCP vs Headless bpy Scripts
Contents

Use Blender MCP when you want to explore and build one model with an agent watching screenshots. Use headless blender -b --python build scripts once you need a batch of models, parallel agent sessions, re-runnable builds, and automated checks. I used both on the same project, back to back, and the switch happened when the agent pointed out why the first approach would not scale.

Here is the short version of how it went. I built a quick 3D asset pipeline for Wavesworn, a browser pirate game, as a demo project for a client. The first ship took one 20-minute live MCP session, and turning it into the player hull took another round of live work the next morning. The next 17 GLB files, 183 root objects in total, came out of plain Python scripts that never touched my open Blender window. Same agent, same Blender, very different tool.

Full example: Clone the working files at github.com/KingPin/sumguy-examples/blender/blender-mcp-vs-headless-bpy

What “Blender MCP” means here

There are several Blender MCP servers. The one I used is the official Blender Lab blender_mcp, version 1.0.2, not the community ahujasid/blender-mcp server. I ran it against Blender 5.2.2 LTS.

It has two parts. A Blender add-on runs inside your open Blender and listens on localhost port 9876. A separate stdio server, launched by your MCP client, relays tool calls to that socket. The data flow looks like this:

MCP client <-- stdio --> blender-mcp <-- TCP 9876 --> Blender add-on

Claude Code registers it as a stdio server:

Terminal window
claude mcp add blender -- uv --directory /path/to/blender_mcp/mcp run blender-mcp

Other MCP clients read a JSON config instead. The kit ships this as mcp.json.example:

mcp.json.example
{
"mcpServers": {
"blender": {
"command": "uv",
"args": ["--directory", "/path/to/blender_mcp/mcp", "run", "blender-mcp"]
}
}
}

The server ships about 20 tools. The ones that mattered in my sessions were execute_blender_code (run Python inside the live Blender), get_screenshot_of_window_as_image and get_screenshot_of_area_as_image (the agent looks at your viewport), get_objects_summary, and get_blendfile_summary_path_info. The server also ships _for_cli variants that run code in a background Blender process, which I did not use.

One gotcha is easy to hit. I had registered the server in one Claude Code profile and not in another, so the tools simply did not exist in the second. A server added mid-session also does not load its tools until you restart the session. claude --continue brings the conversation back, so you lose nothing but the pause.

Phase 1: one ship, live, watching screenshots

The goal was a tier-1 sloop for the game. The agent loaded a Kenney Pirate Kit ship into Blender as a scale and style reference and asked the agent to build a sloop in that spirit. Then the loop ran: the agent executes code, grabs a screenshot of my Blender window, looks at it, and adjusts.

Across October 15 and 16, 2026, counting the subagents the main session spawned, that came to 143 MCP tool calls:

ToolCalls
execute_blender_code101
get_screenshot_of_window_as_image21
get_screenshot_of_area_as_image9
get_blendfile_summary_path_info9
get_objects_summary3

That is the shape of exploration. About seven in ten calls ran code in the scene, and most of the rest were screenshots. You are in the viewport too, so you can say “the bow is too blunt” and watch it change. For a model nobody has designed yet, that feedback loop is what you are paying for.

The sloop became the player hull, hull20. It was stretched to 20 units, and it exports as a 3376-triangle GLB. The export itself is a headless script, scripts/export-hull20.py, so even the “live” model ended up with a re-runnable export step.

Where live MCP stops working

The next day, at the start of a batch of new models, the agent said something I wrote down: “Headless jobs can run in parallel. The live Blender is a single shared scene, so I will use it only for my own review.”

That sentence names the limits. A live Blender session is one scene, one viewport, one window, and one human’s attention. Four things follow from that.

  1. No parallelism. Two agent sessions sharing port 9876 are editing the same scene. One agent’s bpy.ops.wm.read_factory_settings is the other agent’s disaster.
  2. No re-runs. The result lives in the session. If you want a variation, the agent edits state, and the history of how you got there is a chat transcript.
  3. No gate. Nothing fails when a model blows its triangle budget. A person has to look.
  4. It is your session. The agent is clicking around in the Blender window you have open.

The agent then measured something useful: a headless EEVEE preview render takes about one second. A screenshot tool call took 1.2 to 2.4 seconds, and the render needs no window at all. After that the agent made no more live MCP calls, and every review used headless preview PNGs. Two minutes later the agent wrote it into the art brief as a rule: “Never connect to the live Blender on port 9876 (the user’s open session).”

Phase 2: headless batch builds

For the batch, the agent wrote art/BRIEF.md and a shared helper library, then launched 8 parallel subagents. Each read the brief and wrote one build script per category. A top-level art/build.py and a stdlib checker came later that evening. Over October 16 and 17 that produced 17 new GLBs and 183 roots (127 of them without skins): figureheads, flags, lanterns, pets, sea life, combat effects, island props, monsters, part styles, tier-4 parts, and 7 hull skins. 76 commits touched art/ for the batch.

The rules in the brief did the heavy lifting:

That list is the real deliverable. The MCP session produced one ship. The rules made 17 files reproducible, and they made a second agent able to continue the work without me.

I will not paste the client’s helper library. Here is a generic version of the same skeleton, small enough to read in one sitting. It lives in the kit as build.py.

The build script

Failure first. A build script that cannot fail the shell is just a script that prints things:

build.py
def fail(msg):
print(f"build: FAIL {msg}", file=sys.stderr)
sys.exit(1) # exits 1; --python-exit-code 1 also covers uncaught exceptions
def reset():
bpy.ops.wm.read_factory_settings(use_empty=True)

reset() is what makes a build re-runnable. Every run starts from an empty factory scene, so there is no hidden state from the last run. Then a flat material and a triangle counter:

build.py
def material(name, rgb):
m = bpy.data.materials.new(name)
m.diffuse_color = (*rgb, 1.0)
m.use_nodes = True
m.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (*rgb, 1.0)
return m
def tris(obj):
return sum(len(p.vertices) - 2 for p in obj.data.polygons)

The props and the budget assert:

build.py
def prop(name, op, mat, loc, **kw):
op(location=loc, **kw)
o = bpy.context.active_object
o.name = name
o.data.name = name
o.data.materials.append(mat)
return o
def build():
red, wood = material("role_red", (0.8, 0.1, 0.1)), material("role_wood", (0.45, 0.28, 0.12))
crate = prop("crate", bpy.ops.mesh.primitive_cube_add, wood, (0, 0, 0.5)) # 12 tris
buoy = prop("buoy", bpy.ops.mesh.primitive_cone_add, red, (3, 0, 1), vertices=16, radius1=0.6, depth=2)
for o in (crate, buoy):
if tris(o) > BUDGET[o.name]:
fail(f"{o.name} has {tris(o)} tris, budget {BUDGET[o.name]}")
return [crate, buoy]

The BUDGET dict at the top of the file holds the numbers: 24 triangles for the crate, 120 for the buoy. The crate measures 12 and the buoy 30. Lower the buoy budget to 10 and the build exits 1 with build: FAIL buoy has 30 tris, budget 10. I tested that.

Next, export and a re-import check. This is the step the client’s pipeline hand-wrote nine times before it moved into a shared helper (more on that below):

build.py
def export(objs, path):
bpy.ops.object.select_all(action="DESELECT")
for o in objs:
o.select_set(True)
bpy.ops.export_scene.gltf(filepath=str(path), export_format="GLB", use_selection=True)
def reimport_check(path, names):
reset()
bpy.ops.import_scene.gltf(filepath=str(path))
got = {o.name for o in bpy.data.objects if o.type == "MESH"}
missing = set(names) - got
if missing:
fail(f"re-import is missing {sorted(missing)}")
if not bpy.data.materials:
fail("re-import lost the materials")

Last, the preview render. This is what replaces the screenshot of a live window:

build.py
def preview(path):
sc = bpy.context.scene
sc.render.engine = "BLENDER_EEVEE"
sc.render.resolution_x, sc.render.resolution_y = 640, 400
sc.render.filepath = str(path)
bpy.ops.object.camera_add(location=(8, -8, 6), rotation=(1.1, 0, 0.785))
sc.camera = bpy.context.active_object
bpy.ops.object.light_add(type="SUN", location=(4, -4, 8), rotation=(0.8, 0.2, 0.6))
bpy.ops.render.render(write_still=True)

The agent runs it, then reads out/preview.png with its normal file reader and critiques the image. That is the same visual feedback as the MCP screenshot, but it comes from a job that anyone can launch from a shell.

Running it

Terminal window
blender -b --python-exit-code 1 --python build.py -- --norender

-b is background mode. --python-exit-code 1 makes any uncaught Python exception exit 1 instead of 0, which is the difference between a CI gate and a silent pass. Everything after the lone -- goes to your script, so --norender skips the preview. Drop it to also write out/preview.png:

Terminal window
blender -b --python-exit-code 1 --python build.py

On Blender 5.2.2 the build without a render takes under a second here, and about 1.2 seconds with the preview render.

Checking the GLB without Blender

The build script checks its own work, but a second checker that does not import bpy is worth having. It runs anywhere, including a CI box with no Blender installed. The kit’s check_glb.py reads only the JSON chunk of the GLB with the standard library. The core is counting triangles per primitive:

check_glb.py
# glTF primitive modes: 4 = TRIANGLES, 5 = STRIP, 6 = FAN
def tri_count(gltf, prim):
n = gltf["accessors"][prim["indices"]]["count"] if "indices" in prim else \
gltf["accessors"][prim["attributes"]["POSITION"]]["count"]
mode = prim.get("mode", 4)
return n // 3 if mode == 4 else max(n - 2, 0) if mode in (5, 6) else 0

Run it against the build output:

Terminal window
python3 check_glb.py out/props.glb --budget 120
ok buoy 30 tris (budget 120)
ok crate 12 tris (budget 120)

Pass --budget 20 and it prints FAIL buoy 30 tris (budget 20) and exits 1. That exit code is the useful part: a loop of agents can run it after every build and stop when it goes red.

Parallel sessions are the actual win

Once builds are scripts, parallelism is free. Each category has its own script and its own output file, so 8 subagents worked on sea life, combat effects, island props, and the rest at the same time. Each one runs blender -b in its own process with its own scene. Nothing shares port 9876. The top-level art/build.py takes a category name, all, or a hull skin id, and validates flags up front.

I would not try this with live MCP. You would have one scene and N agents with opinions.

Scripts also give you something a chat transcript never will: a diff. The git history shows what changed between the first figurehead and the fifth, and the agent can read it.

What went wrong anyway

The pipeline was not clean. The audit file from this project (docs/art/audit-2026-10-16/pipeline_review.md) lists real problems:

Several of these were fixed in later commits. The re-import check, mount empties, and sign-off renders moved into the shared helper. One flag vocabulary now covers every build script, and build.py rejects unknown flags. The build now stops the .blend churn and compresses renders, and models are served gzipped under content-hash URLs. The helper also gained validation for open edges, material counts, and child scale at export.

I did not re-check every item in that audit against the final tree, so treat the list as findings from a review. The bigger lesson is the pattern. Headless scripts invite copy-paste, because each category script is a standalone file. Budget time to pull the repeated parts into one helper early, before the ninth copy.

So which one do you pick

Pick by what you are doing, not by which one feels more modern.

SituationTool
One new model, no design yetLive MCP
You want to say “make the bow sharper” and watch itLive MCP
A batch of 5 or more related modelsHeadless scripts
Two or more agents working at onceHeadless scripts
You need the same output tomorrowHeadless scripts
A triangle budget or naming rule must be enforcedHeadless scripts
The agent needs to look at a resultEither: screenshot or preview PNG

My workflow now is this. Explore the first model live with MCP and screenshots until the look is right. Then ask the agent to write the build script that reproduces it, run it headless, and compare the preview PNG against the live result. From that point on, the script is the source of truth and the live session is a review window.

If an agent starts using your open Blender for production work, stop it and write the rule into the brief. In this project the agent wrote the port 9876 line itself.

Common Questions

Can Claude Code control Blender?

Yes. Claude Code can drive a running Blender through an MCP server such as the official Blender Lab blender_mcp, which exposes tools to execute Python and capture screenshots. It can also run Blender headless through the shell with blender -b --python script.py, which needs no MCP server at all.

How do I run a Blender Python script without opening Blender?

Run blender -b --python-exit-code 1 --python build.py. The -b flag starts Blender in background mode with no window, and --python-exit-code 1 makes a Python exception exit with status 1. Put script arguments after a lone --, then read them from sys.argv in the script.

Is Blender MCP safe to leave connected to my open session?

No, not for production work. The add-on executes whatever Python the agent sends in your live scene, and the scene is shared. An agent can overwrite your unsaved edits. Use live MCP for exploration you are watching, and keep batch jobs on headless Blender with their own scenes.

How do I check a GLB file’s triangle count?

Read the GLB’s JSON chunk and sum the index accessor counts for each mesh primitive, divided by 3 for triangle mode. Python’s standard library can do this in about 20 lines with struct and json. The kit’s check_glb.py does it and exits 1 when any mesh goes over budget.

Do I need the Blender add-on for headless builds?

No. Headless builds use Blender’s own bpy module through blender -b, with no add-on, socket, or MCP server. The add-on and port 9876 matter only for live MCP sessions against an open Blender window. The same build.py runs on a CI runner or a headless server with only Blender installed.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Firewall Rule Audit: Find the Dead Rules

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts