Skip to content

ClientInfo

ClientInfo is the per-client record held on each UserMessageBus. One instance per registered client.

Module: blender_mcp.message_bus.

@dataclass
class 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
FieldTypeNotes
uuidstrSticky identifier provided by the client. Generated by addon/identity.py for Blender peers.
client_typestrConvention: "blender" for addon peers, "llm" for LLM-side clients. Any string is valid.
is_persistentboolPersistent clients live in bus.persistent_clients; non-persistent in bus.ephemeral_clients. Bucket is fixed at first registration.
capabilitieslist[str]Free-form. See Use capabilities.
group_id`strNone`
connected_atfloatUnix timestamp of first registration.
last_seenfloatBumped on every re-registration and bus.touch().
sessionAnyLive MCP ServerSession handle. Used by the forwarding handler to address THIS client’s MCP transport. Never sent over the wire.

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:

message_bus.py
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,
}

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.

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.register
if 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 existing

Fields 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).

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.

register_client -> ClientInfo created, added to bus.{persistent,ephemeral}_clients
register_client -> existing ClientInfo updated in place (same bucket)
send_message -> bus.last_activity bumped; ClientInfo unchanged
job_update -> bus.last_activity bumped; ClientInfo unchanged
unregister_client -> ClientInfo removed from bucket

The bus itself is in-memory only — a server restart drops every ClientInfo. Persistent clients re-register on reconnect.

  • Bus toolsregister_client writes the fields, list_available_clients reads them
  • Use capabilities — the capabilities field in practice
  • Routing modesgroup_id and client_type feed group and type_filter routing