Skip to content
BlenderMCP

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 running
elif snap.get("v") != 1:
... # a newer contract than this reader knows: skip it
{
"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. |

  • 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_at in the past means no lock. The add-on doesn’t always clear an expired lock right away; compare expires_at with time.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, bumps v; 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_namespace a 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.

  • Draw command, ok, ms, at, holder, requester, duration_s and the chat fields freely.
  • Draw the reason fields (in lock and pending_request) as plain text, cut to about 60 characters.
  • Don’t draw error text. Show a failure mark for ok: false instead; the full error is in the BlenderMCP sidebar.