Stage snapshot
The add-on publishes a small summary of its state for other Python code running in the same Blender session. The agent-stage app template draws its viewport overlay from it, and anything else can read it the same way. It lives in Blender’s driver namespace, so a reader needs no import of the add-on and keeps working when the add-on is missing.
import bpy
snap = bpy.app.driver_namespace.get("blender_mcp.stage")if snap is None: ... # BlenderMCP isn't runningelif snap.get("v") != 1: ... # a newer contract than this reader knows: skip itShape (v1)
Section titled “Shape (v1)”{ "v": 1, "source": "blender_mcp", "addon_version": "2026.1001.5", "connected": True, # registered with the server right now "lock": {"holder": str, "expires_at": float, "reason": str} | None, "pending_request": {"requester": str, "reason": str, "duration_s": int | float} | None, "chat": {"busy": bool, "awaiting_approval": str | None}, "activity": [ {"command": str, "ok": bool, "ms": float, "at": float, "error": str}, ], # newest last, at most 20 "updated_at": float, # time.time() of this publish}| Field | Meaning |
|---|---|
| connected | The add-on’s bus client is registered with the server at this moment. |
| lock | The cooperative control lock: who holds it (their client label, or the start of their UUID), when it ends as a Unix time, and the reason they gave. None when nobody holds it. |
| pending_request | A control request waiting for the user’s Allow or Deny in the sidebar, with the requester, their reason and the seconds they asked for. None when nothing waits. |
| chat.busy | A Chat panel turn is running. |
| chat.awaiting_approval | While the Chat panel waits on Allow or Deny, the action being asked about in a few words, such as "run this Python". None otherwise. |
| activity | The commands this Blender ran for the bus, oldest first. ms is how long the command took, at when it finished (Unix time), error the first line of a failure, empty on success. |
Guarantees
Section titled “Guarantees”- The whole dict is replaced on every publish. The add-on builds a new dict and assigns it; it never changes one it already published. A reference you hold is a stable snapshot, and the activity entries are copies, so changing them affects nothing.
- A missing key means the add-on isn’t running. The add-on removes the key when it is disabled or unregistered, so a leftover snapshot never claims to be connected.
- An
expires_atin the past means no lock. The add-on doesn’t always clear an expired lock right away; compareexpires_atwithtime.time()yourself. - Ignore keys you don’t know, and skip a higher
v. New keys can appear within v1, so read only the ones you need. Only a breaking change, such as a removed or retyped key, bumpsv; skip a snapshot whose version is newer than the one you were written for rather than guessing at it. - Look the key up each time. Blender gives
bpy.app.driver_namespacea fresh dict whenever a file loads, and the add-on republishes into the new one. Holding on to the namespace dict itself leaves you reading the old file’s.
Updates arrive when a command finishes, the lock or a control request changes, the client connects or disconnects, a chat turn starts or ends, an approval starts or is answered, and after every file load. There is no change notification: read it from your draw callback or a timer. updated_at tells you how fresh it is.
Displaying it
Section titled “Displaying it”- Draw
command,ok,ms,at,holder,requester,duration_sand thechatfields freely. - Draw the
reasonfields (inlockandpending_request) as plain text, cut to about 60 characters. - Don’t draw
errortext. Show a failure mark forok: falseinstead; the full error is in the BlenderMCP sidebar.
A Supported Systems Joint
BlenderMCP is built and maintained by Supported Systems, a small studio that writes software for people who have to keep it running afterwards.
Visit supported.systems