033 - Home Assistant Role with MCP Server Surface¶
User Story¶
As a platform administrator of Infinito.Nexus, I want a web-app-homeassistant role that deploys Home Assistant and exposes its native MCP server, so that agent employees (Hermes on a Pi, 032) and other MCP clients can control home devices as scoped MCP tools without any host access, making the “isolated home assistant” scenario concrete.
Background¶
Home Assistant is the open-source home-automation platform. It ships a native Model Context Protocol Server integration (mcp_server) that implements Streamable HTTP and exposes Home Assistant’s Assist API to MCP clients, with per-entity access control (only explicitly exposed entities are reachable). It can control lights, climate, covers, switches, sensors, media players, fans, locks, vacuums, and cameras.
This makes Home Assistant a natural 025 MCP server role and the concrete backend for the isolated-home-assistant scenario in 032: the agent stays kernel-isolated and reaches the home only through this MCP surface, never through host access. Home Assistant runs natively on arm64/Raspberry Pi, so it co-locates with the Pi agent story.
Confirmed Decisions¶
These choices are settled at requirement creation time and bound the implementation. Re-opening any of them MUST be recorded in the implementing PR.
web-app-homeassistantdeploys Home Assistant in Container mode. The role uses the officialhome-assistant/home-assistantcontainer (no Supervisor, no add-on store, not privileged), following the standardweb-app-*contract (meta/*). Add-on management is out of scope; themcp_servercore integration does not need the Supervisor.MCP surface is the native
mcp_serverintegration under the 025 contract. The role’smeta/services.ymldeclaresmcpwithdirection: server,implementation: native,transport: streamable_http, served under the role’s canonical origin per 025. No third-party MCP add-on is used when the native integration covers the need.Access is entity-scoped and read-mostly by default. Only entities explicitly exposed to the Assist API are reachable via MCP. Mutating/actuating tools (locks, covers, switches) are off by default and require operator opt-in, per 025’s mutating-tools rule.
Consumers discover it via 025. Agent clients (032) and other 025 client roles reach it through the
roles_with_servicelookup; the endpoint is not hard-coded in any consumer.Auth caveat is documented. Home Assistant uses its own auth (users + long-lived tokens +
trusted_proxies), not full OIDC. The MCP endpoint is authenticated with a Home Assistant credential stored inmeta/secrets.ymlcredentials:; if platform SSO in front of the HA UI is wanted, the trusted-proxy header approach and its limits MUST be documented in the role README.arm64/Raspberry Pi is supported. The role runs on
arm64so it can sit next to the Pi agent from 032.The UI is published, the MCP surface is not. (Revised during implementation; the original decision was internal-only for both.) Home Assistant follows the
web-app-*contract and serves its own UI on a canonical domain through the reverse proxy, because every part of that contract —meta/domains.yml,meta/csp.yml, the guest Playwright persona — assumes a public vhost, and a role without one would have to be shaped as asvc-*role instead. The hub’s own auth provider guards that surface. The MCP endpoint staysexposure: internal: it is reachable in-cluster only, an unauthenticated probe of the public origin is refused, andfiles/playwright/test-mcp-guest.jsasserts exactly that. A platform-SSO gate in front of the UI remains a documented follow-up.
Architecture¶
flowchart LR
user([Household]) -->|UI / voice| ha
subgraph node["Node (arm64 / Pi ok)"]
ha["web-app-homeassistant<br/>+ native mcp_server (025)"]
devices["exposed entities:<br/>lights, climate, locks,<br/>cameras, sensors..."]
ha --- devices
end
agent["web-app-hermes (032)<br/>isolated MCP client"]
agent -->|"MCP over Streamable HTTP (025)"| ha
Where each guarantee is enforced¶
Guarantee |
Enforced by |
|---|---|
Only Assist-exposed entities are reachable |
|
Mutating tools stay off |
the MCP long-lived token belongs to a dedicated account pinned to Home Assistant’s |
The endpoint refuses an anonymous caller |
|
The stored bearer still authenticates |
|
The advertised tool contract matches what the hub serves |
files/test/test.sh, which drives the shared MCP contract harness inside the provider’s own network |
The pinned image runs on arm64 |
Acceptance Criteria¶
[x] A
web-app-homeassistantrole deploys Home Assistant in Container mode (no Supervisor/add-ons, not privileged) and it comes up healthy; its MCP endpoint is not published, per the revised Decision 7.[x] The native
mcp_serverintegration is enabled and declared inmeta/services.ymlasdirection: server,implementation: native,transport: streamable_http, conforming to the 025 schema and lint.[x] The MCP endpoint is authenticated; an unauthenticated probe is rejected, and the credential lives in the platform token store —
meta/mcp.ymlcredential.source: token_store, written bysys-token-store— never in a README, an env file or a Playwright trace. (Revised: 025 and 035 moved MCP bearers out ofmeta/secrets.ymlinto the token store after this criterion was written.)[x] Only entities explicitly exposed to Assist are reachable via MCP; mutating tools are off unless the operator opts in.
[ ] An MCP client (e.g.
web-app-hermesfrom 032) discovers the role viaroles_with_serviceand reads an exposed entity’s state through MCP.[x] The role is added to 025’s MCP audit as a
serverrole.[x] The role runs on
arm64.[ ] The role comes up green in both compose and swarm modes.
[x] A Playwright spec exercises the Home Assistant surface and is green.
Cross-linking¶
Implementing PR: to be linked.
See Also¶
MCP contract: 025-mcp-role-integration.md
Agent that consumes it (isolated home-assistant scenario): 032-agent-employees-firecracker.md
Home Assistant MCP Server: home-assistant.io/integrations/mcp_server