Skip to content
BlenderMCP

Run heavy work in a background worker

A job on the user’s Blender holds its main thread, so a multi-minute rebuild freezes the viewport for as long as it runs. A background worker avoids that. It’s a headless blender -b that the user’s Blender starts on a saved copy of its scene. The worker joins the bus as its own client, heavy jobs go to it, and the user keeps working.

  1. Spawn the worker.

    blender_spawn_worker()
    → {"status": "ok", "worker_uuid": "blender-worker-3f9a1c…", "pid": 48213,
    "worker_dir": "/tmp/blender_mcp_workers/blender-worker-3f9a1c…",
    "snapshot_path": ".../snapshot.blend", "expires_at": 1790461234}

    The user’s Blender saves a copy of the current scene, unsaved changes included, into a private directory and starts the worker on it. The call returns once the worker has joined the bus. Pass target_uuid when more than one Blender is connected.

  2. Send the heavy work to the worker. Target it by uuid:

    blender_submit(command="execute_code",
    params={"code": "..."},
    target_uuid="blender-worker-3f9a1c…")

    Inside the code, call report_progress(fraction, message) as the work moves along:

    for i, part in enumerate(parts):
    rebuild(part)
    report_progress((i + 1) / len(parts), f"boolean {i + 1} of {len(parts)}")

    Save the result inside the worker’s directory, for example with bpy.ops.wm.save_as_mainfile(filepath=..., copy=True), and print its path.

  3. Follow the job. blender_job_status(job_id, wait_seconds=50) returns when the job reports progress or finishes, with the latest report under progress:

    {"status": "running", "progress": {"fraction": 0.4, "message": "boolean 3 of 7"}}

    Read the output with blender_job_result(job_id) when done is true.

  4. Offer the result to the user.

    blender_offer_reload(path=".../attic.blend", message="attic rebuild finished")

    The user’s sidebar shows a Background result ready banner with Reload and Dismiss. Nothing is loaded until they click Reload. If they have unsaved changes, the banner warns that reloading discards them.

    If the user has kept working while the worker ran, offer only the collections the job changed instead, so their other edits survive (see Merging collections).

  5. Stop the worker with blender_stop_worker(worker_uuid) once you’re done with it.

A reload replaces the whole scene with the worker’s file, including anything the user changed after the snapshot was taken. When the job only rebuilt part of the scene, merge just those collections:

blender_offer_merge(path=".../attic.blend", collections=["Attic", "Roof"],
message="attic rebuild finished")

The banner reads Background result ready, names the collections, and offers Merge and Dismiss. Nothing changes until the user clicks Merge. Then, for each named collection:

  • The collection is appended from the result file and linked in the same place as the live collection of the same name (the same parent collection), then the old one is deleted along with objects that lived only in it. Objects the old collection shared with other collections are kept, only unlinked. The new collection takes the old name back, and names Blender suffixed during the append (Beam.001) are restored.
  • A collection that doesn’t exist in the live scene is added under the scene’s top-level collection.
  • A collection missing from the result file is skipped and reported; nothing is deleted for it.
  • Meshes and materials that only the deleted objects used are removed. Other unused data the user already had is left alone.
  • If the append itself fails, everything it brought in is removed again and the scene is unchanged.

mode="add" brings the collections in alongside the existing ones without deleting anything; Blender’s .001 suffix stays on any clashing name.

Check what happened with blender_get_merge_result(). pending is set while the banner waits for an answer; last_result lists what was replaced, added or skipped with object counts, or dismissed: true.

Edits the user made inside a replaced collection are lost, since that collection is swapped for the worker’s version. Everything outside the named collections is kept.

  • Never picked by accident. Workers are reached only by uuid or pid:. Calls without a target, and target_uuid="latest", still go to the user’s Blender. blender_list_available_clients shows workers with role: "worker" and the parent_uuid that spawned them.
  • Cancel works. A worker is a separate process, so blender_job_cancel on a running worker job stops the worker. A running job on the user’s own Blender still can’t be interrupted.
  • They clean up after themselves. A worker exits when its parent Blender goes away, when it’s stopped, or when its time runs out. Files in its directory (snapshot, results, log) stay on disk.
  • One at a time by default. The addon preference Max workers allows up to four.
  • Memory roughly doubles while a worker runs, since it holds a full copy of the scene. GPU renders on a worker compete with the user’s viewport.
  • At most two hours. A worker uses the user’s current access token and can’t refresh it, so it also stops five minutes before that token expires. Spawning refreshes the token first when it has less than 30 minutes left.
  • The snapshot is a point in time. Edits the user makes after the spawn aren’t in the worker’s copy, which is why reloading is always the user’s choice.
  • Paths. Relative texture paths are remapped when the copy is saved; absolute paths and packed data carry over. External caches with unusual paths may need checking.