HA Client
The WebSocket layer that connects the server to Home Assistant, with command queueing and event subscription.
Every tool that needs to talk to Home Assistant goes through a single client in src/hass/. It’s a long-lived WebSocket connection to HA’s /api/websocket endpoint, with a small command queue, an event subscription manager, and a state cache.
The connection lifecycle
- Lazy connect. The client is created at process start but doesn’t open the WebSocket until the first tool call that needs it. This keeps Smithery and
tools/listdiscovery fast. - Auth. On connect, HA sends an
auth_requiredmessage. The client replies withauthcarrying theHASS_TOKEN. HA repliesauth_okand the connection becomes usable. - Subscribe to events. Immediately after auth, the client sends
subscribe_eventsso every tool that asks “what changed?” can be answered from the in-process event log without re-querying HA. - Idle keepalive. If no traffic for 30s, the client sends a
pingand expects apongwithin 5s. Three missed pongs and the connection is treated as dead and reconnected. - Reconnect. On disconnect (any reason: HA restart, network blip, server restart), the client waits 1s, 2s, 4s, 8s, …, capped at 60s, and tries again. It preserves all subscriptions on reconnect.
Command queue
Tools call client.sendCommand(type, payload) to invoke a service or read state. The client assigns a numeric id, sends the message, and returns a Promise<unknown> that resolves when HA replies with the matching id.
The queue ensures that if 50 tools call sendCommand at the same time, they get 50 distinct IDs and the responses are matched back to the right caller. There’s no per-connection mutex — the WebSocket is multiplexed.
The promise has a 30s timeout. If HA hasn’t replied in that window, the promise rejects with HASSErrorCode.TIMEOUT and the caller can decide whether to retry.
Event subscription
The HA client subscribes once to subscribe_events at startup. Every event HA emits (state change, service call, component loaded, …) flows into an internal EventEmitter. Tools can subscribe to:
- All events:
client.events.on('event', handler) - One event type:
client.events.on('state_changed', handler)
The HTTP+WS entry point bridges this EventEmitter to its own /mcp/ws endpoint, so a connected MCP client can subscribe_events and get a live stream.
State cache
client.getStates() is the workhorse. It calls HA’s get_states command and caches the result. The cache invalidates on any state_changed event for the duration of the process, so getStates() after a state change returns the fresh data without a round-trip.
If you need to force a re-fetch, call client.refreshStates().
Where the rest of the codebase touches it
Tools get a HassClient from their ToolContext. The shape is defined in src/types/index.ts. The custom HTTP entry point (src/index.ts) is the only place the client is instantiated at startup; the other entry points create it lazily on first tool call.
Error handling
HA errors arrive as {"type": "result", "success": false, "error": {...}}. The client maps them to a typed HassError:
| HA error code | HassError.code |
|---|---|
invalid_format | INVALID_FORMAT |
not_found | NOT_FOUND |
unauthorized | UNAUTHORIZED |
home_assistant_error | HOME_ASSISTANT_ERROR |
Tools that need fine-grained error handling can instanceof HassError and switch on code. The default is to rethrow — the MCP error-handling middleware turns it into a JSON-RPC error response.
Next
- Tool System — how the client is exposed to tools.
- Configuration > Environment — the
HASS_HOST/HASS_TOKENsettings that drive the client.