ClientInfo
ClientInfo is the per-client record held on each UserMessageBus. One instance per registered client.
Module: blender_mcp.message_bus.
Fields
Section titled “Fields”@dataclassclass ClientInfo: uuid: str client_type: str # "blender" | "llm" | other is_persistent: bool = False capabilities: list[str] = field(default_factory=list) group_id: Optional[str] = None connected_at: float = field(default_factory=time.time) last_seen: float = field(default_factory=time.time) session: Any = None # MCP ServerSession; not serialized| Field | Type | Notes |
|---|---|---|
uuid | str | Sticky identifier provided by the client. Generated by addon/identity.py for Blender peers. |
client_type | str | Convention: "blender" for addon peers, "llm" for LLM-side clients. Any string is valid. |
is_persistent | bool | Persistent clients live in bus.persistent_clients; non-persistent in bus.ephemeral_clients. Bucket is fixed at first registration. |
capabilities | list[str] | Free-form. See Use capabilities. |
group_id | `str | None` |
connected_at | float | Unix timestamp of first registration. |
last_seen | float | Bumped on every re-registration and bus.touch(). |
session | Any | Live MCP ServerSession handle. Used by the forwarding handler to address THIS client’s MCP transport. Never sent over the wire. |
to_dict() — the wire shape
Section titled “to_dict() — the wire shape”list_available_clients returns a list of ClientInfo.to_dict():
{ "uuid": "blender-abc123", "client_type": "blender", "is_persistent": true, "capabilities": ["python_execution", "rendering"], "group_id": "render-cluster", "connected_at": 1706200000.0, "last_seen": 1706203789.45}Note: no session field. The MCP ServerSession holds asyncio.Future objects that can’t be copy.deepcopy’d (and shouldn’t be serialized in any case — it’s a live transport handle). to_dict() is hand-rolled rather than dataclasses.asdict() specifically for this reason:
def to_dict(self) -> dict[str, Any]: # Build manually — asdict() deep-copies all fields including `session`, # which holds an MCP ServerSession with asyncio.Future objects that # cannot be pickled/deepcopied. return { "uuid": self.uuid, "client_type": self.client_type, "is_persistent": self.is_persistent, "capabilities": list(self.capabilities), "group_id": self.group_id, "connected_at": self.connected_at, "last_seen": self.last_seen, }Why session is stripped
Section titled “Why session is stripped”The forwarding handler in oauth_server.py looks up each target’s session to push the MCP notification to the right transport. That object is local to the server process and meaningless to remote clients. Exposing it would leak internal handles and break JSON serialization at the same time.
Re-registration semantics
Section titled “Re-registration semantics”Calling blender_register_client again with the same uuid updates the existing record in place — it does not create a duplicate:
# message_bus.py UserMessageBus.registerif client_info.uuid in bucket: existing = bucket[client_info.uuid] existing.client_type = client_info.client_type existing.capabilities = client_info.capabilities existing.group_id = client_info.group_id existing.last_seen = time.time() if client_info.session is not None: existing.session = client_info.session self.last_activity = time.time() return existingFields updated on re-registration: client_type, capabilities, group_id, last_seen, and session (if a non-None one was provided).
Fields NOT updated: uuid (the lookup key), is_persistent (bucket assignment is sticky), connected_at (preserves the original join time).
Touching last_seen
Section titled “Touching last_seen”bus.touch(client_uuid) bumps last_seen without touching anything else. The server doesn’t currently call this from a heartbeat path; last_seen advances naturally via re-registration and dispatch activity.
Lifecycle
Section titled “Lifecycle”register_client -> ClientInfo created, added to bus.{persistent,ephemeral}_clientsregister_client -> existing ClientInfo updated in place (same bucket)send_message -> bus.last_activity bumped; ClientInfo unchangedjob_update -> bus.last_activity bumped; ClientInfo unchangedunregister_client -> ClientInfo removed from bucketThe bus itself is in-memory only — a server restart drops every ClientInfo. Persistent clients re-register on reconnect.
Reference
Section titled “Reference”- Bus tools —
register_clientwrites the fields,list_available_clientsreads them - Use capabilities — the
capabilitiesfield in practice - Routing modes —
group_idandclient_typefeedgroupandtype_filterrouting