Several agents

Blender runs every command on one main thread, so agents that share a Blender share a queue: their commands run one after another, and a render inside a tool call holds everyone until it finishes. Parallel work therefore happens in separate processes, and the server gives three ways to keep it from colliding.

One Blender per agent

The cleanest split is the studio one: each agent works in its own Blender on its own port (SNAIL_MCP_BLENDER_BRIDGE__PORT, one server per Blender) and in its own .blend. A coordinator owns the shot file and brings the results in with blender_append_assets (the io skill) and link true, linked rather than copied, so the dependency runs one way and nothing is edited twice.

Batches

blender_batch runs a list of bridge commands in a headless Blender on a copy of the file and saves a .blend. The calling Blender stays free; several batches run at once. blender_batch_status reports how many steps finished and the last 20 with their results or errors. Batches are how sub-agents that share one server still work in parallel: they plan, submit a batch, and the coordinator links the output.

Journal and leases

When agents must share one file, set SNAIL_MCP_BLENDER_AGENT on each server. Every mutating command is then recorded with its agent in the journal (blender_journal), and an agent can claim objects or collections for a while with blender_lease: other agents' mutating commands on them come back as Leased with the holder's name until the lease is released or expires. Taking another agent's lease, by release or by clear, needs force. Read-only commands always pass.

Over stdio the name belongs to the server, which serves one client. A server reached over HTTP serves several, and the name of each is the name on the key it presents: that name signs its commands, so the journal and the leases tell them apart, and it holds a tool list of its own, so a skill one client loads is not loaded for the rest. The name is not the caller's to choose — a header would make the agent on a lease a claim nobody had checked — so an operator mints a key per client with clients add (see Configuration). Only the token the server was configured with still lets its holder name itself with X-Snail-Agent, because that is what that token could always do. blender_diagnose reports the name in hand under bridge.agent.

Both live in files under the add-on's data directory, on the machine Blender runs on (state/journal.jsonl, state/leases.json), so they survive a restart of Blender, and the worker Blenders that batches start read and write the same files: a batch step on a leased object fails with Leased like a direct call would, and its steps appear in the journal with the worker's pid. A render job's worker runs no commands and touches neither file. A journal entry names the objects a command addressed under targets; batches, snapshots, scenes, view layers, File Output nodes, render jobs and projects are named after something else, so their name is not listed there.

The lease guard refuses on the objects a command will really act on, and on nothing else. Where those are named is written down for each command in addon/commands.json and taken out of the handler itself, so it covers a name in the parameter you would expect, a name inside a block (focus.object, dof.focus_object), a name in a parameter of its own (pivot_name, bevel_object, taper_object, curve, cameras), a name that is the key of a block (object_motion_blur) and a name inside each item of a list (variables[].object); for a command that works on the selection, the objects selected at the time. A lease on a collection is a lease on everything in it.

Guessing from the parameters that usually carry an object was wrong in both directions. It missed every name written somewhere else, so depth of field could be pointed at an object another agent held, a curve taken as a bevel profile, a camera pivoted around one — with no refusal and no journal entry, which is worse than a refusal, because the holder had no way to find out. And it refused too much: name is also the parameter that carries a material, a compositor node, a sequencer strip, an operator and the name of something about to be created, so a lease on an object called Steel stopped a material called Steel being edited, and a lease on Crate stopped a second object being made under that name — one Blender would have called Crate.001 and left the held one alone.

The journal is not narrowed with the guard. An entry's targets still names everything a command carried, the material and the strip included, and the name a new object was created under: the record answers what an agent did, rather than refusing anybody, and no lease could have been held on a name nothing had yet. Three commands address nothing either reading can see: blender_python, blender_run_operator and blender_batch. They pass the guard and land in the journal flagged unguarded, with the code, the operator name or the batch name, so a coordinator sees them. Agents that must respect leases use the dedicated tools; a shared set-up that cannot trust them switches Python off with SNAIL_MCP_BLENDER_PYTHON=off, which also refuses script operators, and sequences and batches with a python step or a script-operator step. Blender is told the same thing — --python off, which compose passes from PYTHON_ACCESS — so the rule holds where the power is: a client that reaches the add-on directly, with the bridge token, is refused there too, along with the operators that install or trust code and any file written outside the add-on's own areas. fallback is the server's reading of a script against the tool catalog, so to the add-on it is allow; off is the boundary. Which parameters of a command name a path is written down once, in the command schema the add-on reads, so the rule covers an export, a render output and an image loaded from disk, not only the transfers.

Sequences, not scripts

run_tools sends a list of commands to the Blender that is already open, in one call. Each step is dispatched as if it had been called on its own — with the bridge command's name and its params as the add-on reads them, the tool's parameter names in snake_case, so set_cycles takes adaptive_threshold, not adaptiveThreshold, and a key the command does not read fails the step with the name it was probably meant to be — so the lease guard refuses a step on another agent's object, the journal names each step with the objects it touched, and a failed step stops the sequence and returns every step that ran. That is the difference from blender_python: a script of the same length passes no guard and tells the journal nothing, which is why it appears there as unguarded. A set-up where agents must respect each other's work runs builds as sequences, and SNAIL_MCP_BLENDER_PYTHON=fallback refuses a script whose work the tools already do while leaving the API no tool reaches available — a blender_python call and a python step of run_tools or blender_batch alike, so a sequence is no way around it.

Programs, when the steps are not known in advance

run_tools carries a list decided before the call. run_program carries a program that decides as it goes: a small JavaScript that calls the commands itself.

const parts = blender.list_objects({ pattern: "Crate*" }).objects;
for (const part of parts) {
  blender.transform_object({ name: part.name, location: [part.location[0], 0, 0] });
}
return { moved: parts.length };

Every call goes over the same link as a tool call, so the lease guard, the journal and the validation hold for each step, and a failed command throws {type, message, details} the program may catch. Only what the program returns comes back, with the log it wrote and the list of commands it sent: twenty steps cost one call and one reply instead of twenty of each, and a listing a program filters never reaches the conversation whole. It is bounded by the timeout it was given, by its memory, and by 200 commands; past that it stops and reports what it managed. It is not blender_python: it reaches the commands and nothing else — no files, no network, no bpy.

When the tool for a step is not in the list, find_tool searches the whole catalog by what the step should do. Its answer carries each tool's command and every parameter it takes, which is enough to send it with run_program; enable loads the skill instead, so the tools themselves join the tool list. Keeping the list small is the point: it is sent to every client on every session, so the tools that are always there are the ones every session needs, and the rest arrive when they are asked for.

Gates

blender_snapshot before a stage and after it; blender_render_check and blender_render_budget before a render; blender_inspect_image on the result. With several agents in one Blender, render only in the background — blender_render_job, or blender_render_animation, which does it by default when called as a tool; a render_animation step of run_tools renders in the open Blender — so a render never freezes the others.