Commit 76708ad
* feat: expose the in-process server toolset as a Home Assistant LLM API (#1745)
While the in-process server entry is running, its full toolset is
registered as an LLM API (homeassistant.helpers.llm), so any Home
Assistant conversation agent (OpenAI, Google, Ollama, Anthropic, ...)
can select it under "Control Home Assistant" and drive ha-mcp from the
Assist chat UI, the companion apps, and voice satellites - the
LLM-API direction settled in the issue discussion.
The API talks MCP to the server over loopback streamable HTTP,
mirroring HA core's own mcp integration: tools are listed fresh each
conversation turn (runtime-registered custom tools included), schemas
converted via voluptuous_openapi, and each tool call opens a stateless
loopback session. The mcp client SDK ships with the runtime-installed
server package, so its imports stay lazy; a build without it skips the
feature with a warning instead of failing the running server.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XH65D5pHe6k4EDLetdPqDv
* fix: catch unwrapped httpx and McpError transport failures in the LLM API
Bare httpx errors and protocol-level McpError can escape a session call
without the ExceptionGroup wrapper, bypassing the transport-error map
and crashing the conversation turn with a raw traceback (Gemini review
finding). The except target is now built at exception time - httpx and
the mcp SDK arrive with the runtime-installed server package, so they
cannot be named in a module-level constant. Mirrors HA core's mcp
integration, which catches both the same way. Genuine bugs (ValueError,
TypeError, ...) still propagate; regression tests added for all three.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XH65D5pHe6k4EDLetdPqDv
* fix: propagate group-wrapped bugs and harden LLM API registration containment
Multi-agent review findings on the LLM API:
- except ExceptionGroup treated ANY group as a transport failure, but the
SDK's anyio task groups wrap in-session bugs (TypeError, KeyError, ...)
identically - a genuine bug was relabeled as "could not reach the
server" and lost its loud traceback. A group now maps to
HomeAssistantError only when every leaf (nested groups included) is a
transport error; otherwise it propagates.
- async_register_llm_api documented "never raises" but only contained
ImportError and HomeAssistantError; anything else leaked into the
bring-up's outer except Exception, which tears the ALREADY-RUNNING
server down and files a "start" repair issue for a cosmetic failure.
The whole registration body is now contained (warn + skip),
cancellation still propagates.
- HaMcpTool.parameters typed vol.Schema (matches the base contract and
HA core's mcp integration reference).
Tests: registration-failure containment (both exception classes),
group-wrapped/mixed/nested-group bug propagation on both call paths,
and the call-path timeout mapping.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XH65D5pHe6k4EDLetdPqDv
* test: prove the LLM API e2e against the real server and schema catalog
Review roll-in (no follow-up-PR policy):
- Embedded e2e now drives the exact client stack llm_api.py uses — real
mcp SDK streamable-HTTP session through the webhook, initialize
instructions non-empty (the api_prompt source), tools/list, and
convert_to_voluptuous over EVERY real tool schema asserting ZERO
failures. At runtime an unconvertible schema is skipped per-tool with
only a warning, so a systemic voluptuous_openapi incompatibility would
silently shrink the toolset; this fails loudly instead. Plus a real
call_tool dumped the way HaMcpTool.async_call returns it, and a
home-assistant.log assertion that the bring-up actually registered the
LLM API inside real HA. voluptuous-openapi added to the dev group for
this (HA core provides it at runtime; unit tests stub it).
- Unit test for the pre-rename SDK fallback: fakes the transport module
at the sys.modules level so _mcp_session's real import selection runs
and wires the deprecated streamablehttp_client name identically.
Deliberately NOT added: an aggregate "registered X of N tools" warning —
every skipped tool is already individually logged with name + traceback,
so the aggregate line would add noise without new signal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XH65D5pHe6k4EDLetdPqDv
* fix: raise component log level in the embedded e2e seed and bump skip ceilings
The LLM-API registration proof asserts on an INFO line, but the seeded
e2e config writes only WARNING+ to home-assistant.log (CI-observed) - a
scoped logger block in the container's private config copy surfaces just
this component's INFO lines. The two new container_only tests also add
exactly +2 marker-skips on each of the haos / haos_inaddon / embedded /
haos_embedded lanes; ceilings bumped per the smoke test's own protocol
for intentional marker-gated additions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XH65D5pHe6k4EDLetdPqDv
* chore: bump custom component to 1.0.5 for the LLM API feature
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XH65D5pHe6k4EDLetdPqDv
* feat: add an option to enable/disable the conversation-agent LLM API
Owner request from live testing: the toolset's LLM API registration is
now gated on a server-entry option (default on), with a usage-guide link
in the option description. Turning it off removes the API from every
conversation agent's selector on the next reload.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XH65D5pHe6k4EDLetdPqDv
* fix: inject the LLM API docs link as a description placeholder
hassfest forbids literal URLs in strings.json - the options form now
fills {llm_api_docs_url} from description_placeholders.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XH65D5pHe6k4EDLetdPqDv
* feat: per-tool LLM API exposure with tool-search mode and settings UI control
Owner-directed exposure control for the conversation-agent LLM API:
- Server: new llm_exposure module stamps every tools/list entry with
_meta.ha_mcp = {llm_api_exposed, pinned}, read live from tool_config's
new llm_api overrides map — deny-by-default for beta-tagged tools,
ha_dev_* tools, and ha_restart/ha_reload_core/ha_manage_backup. The
settings UI grows a fourth per-tool toggle (LLM API) that applies on
the agent's next message, no restart; regular MCP clients are
untouched (stamp is additive metadata).
- Component: llm_api_exposure option — tool_search (default) registers
a compact API (pinned tools + ha_search_tools/ha_call_tool
meta-tools), full mirrors the whole exposed catalog, both registers
the two side by side (per-agent choice via HA's own selector; one
server serves all of it). Both modes filter on the stamp; ha_call_tool
re-checks exposure at call time and answers a hidden name exactly like
a nonexistent one, so hiding cannot be skirted and existence never
leaks. Unstamped (older) servers get a conservative built-in
deny-list.
- Security parity: global disable stays enforced by the server for all
clients at list AND call time; policy gate / read-only middleware
apply to forwarded agent calls exactly as to any MCP client; e2e
asserts ha_restart is present on the raw MCP surface while stamped
hidden.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XH65D5pHe6k4EDLetdPqDv
* fix: keep the 1815 toggle placement contract after the merge
The startup-notification and sidebar-panel toggles stay immediately
after enable_webhook (their test pins that order); the LLM API pair
follows them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XH65D5pHe6k4EDLetdPqDv
* fix: restart_required compares default-pinned-padded states
The GET handler pads its states response with the default-pinned tools;
the JS echoes that padded map back, so an unpadded compare flagged every
first save as a states change (live-found while validating the LLM-API
toggle). Pad both sides of the compare; LLM-API-exposure-only saves now
correctly report restart_required=false.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XH65D5pHe6k4EDLetdPqDv
* fix: hand HA's shared httpx client to the MCP SDK sessions
Without it the SDK builds its own httpx client per session, whose SSL
setup loads the CA bundle synchronously inside HA's event loop - HA's
blocking-call monitor flagged the exact line during live voice-agent
testing. HA core's mcp integration passes a hass-managed client for the
same reason; the pre-rename SDK fallback has no http_client kwarg and
keeps the old behavior.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XH65D5pHe6k4EDLetdPqDv
* test: forwarder assertion includes hass after the shared-client change
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XH65D5pHe6k4EDLetdPqDv
* fix: harden exposure fail-direction, stub beta rendering, and mode default
Second multi-agent review round, all findings independently verified:
- LlmExposureMiddleware now serves the last-known-good settings when a
read fails instead of caching pure defaults - defaults would re-EXPOSE
tools the user explicitly hid from agents (fail-open on an exposure
control); pure defaults only apply when no read ever succeeded. Plus
a non-dict guard on the tool_config payload, and the module docstring
no longer overstates 'live on every list' (2s coalescing cache).
- Settings UI stub rows for feature-gated tools now render the LLM API
toggle hidden-by-default: the previous or-fallback was dead code
(stub tags are never empty), so the 7 beta stubs rendered as exposed
while the real stamp says hidden.
- HaMcpLlmApi.mode defaults to tool_search and unknown modes fall
through to the compact shape - the previous full-catalog default made
an omitted mode maximally exposed, opposite the option default.
- HaMcpTool forwards self.name rather than the (equal) dispatch name.
Tests: JSDOM coverage for the LLM API toggle (render, override capture
in the POSTed body, live-apply vs restart-required messaging); GET
llm_api map incl. the stub-beta default; POST non-dict llm_api 400; TTL
expiry; serve-stale-on-read-failure; omitted/unknown mode defaults; and
a META-key equality test that mechanically enforces the cross-module
stamp contract the runtime import ban forces us to duplicate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XH65D5pHe6k4EDLetdPqDv
* fix: use from-import for settings_ui module in stub-beta test
CodeQL quality gate: the module was imported with both 'import' and
'import from' styles in the same file; match the file's existing
'from ha_mcp import settings_ui as ...' pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XH65D5pHe6k4EDLetdPqDv
---------
Co-authored-by: kingpanther13 <kingpanther13@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 753a155 commit 76708ad
23 files changed
Lines changed: 2805 additions & 17 deletions
File tree
- custom_components/ha_mcp_tools
- translations
- docs
- src/ha_mcp
- settings_ui
- tests/src
- e2e
- basic
- workflows/embedded
- unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
| 51 | + | |
| 52 | + | |
51 | 53 | | |
52 | 54 | | |
53 | 55 | | |
| |||
56 | 58 | | |
57 | 59 | | |
58 | 60 | | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
59 | 65 | | |
60 | 66 | | |
61 | 67 | | |
| 68 | + | |
62 | 69 | | |
63 | 70 | | |
64 | 71 | | |
65 | 72 | | |
| 73 | + | |
66 | 74 | | |
67 | 75 | | |
68 | 76 | | |
| |||
277 | 285 | | |
278 | 286 | | |
279 | 287 | | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
280 | 304 | | |
281 | 305 | | |
282 | 306 | | |
| |||
312 | 336 | | |
313 | 337 | | |
314 | 338 | | |
| 339 | + | |
315 | 340 | | |
316 | 341 | | |
317 | 342 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
288 | 288 | | |
289 | 289 | | |
290 | 290 | | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
291 | 308 | | |
292 | 309 | | |
293 | 310 | | |
| |||
335 | 352 | | |
336 | 353 | | |
337 | 354 | | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
338 | 358 | | |
339 | 359 | | |
340 | 360 | | |
| |||
379 | 399 | | |
380 | 400 | | |
381 | 401 | | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
382 | 411 | | |
383 | 412 | | |
384 | 413 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
| 42 | + | |
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
| |||
49 | 50 | | |
50 | 51 | | |
51 | 52 | | |
| 53 | + | |
52 | 54 | | |
53 | 55 | | |
54 | 56 | | |
| |||
60 | 62 | | |
61 | 63 | | |
62 | 64 | | |
| 65 | + | |
63 | 66 | | |
64 | 67 | | |
65 | 68 | | |
| |||
125 | 128 | | |
126 | 129 | | |
127 | 130 | | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
128 | 143 | | |
129 | 144 | | |
130 | 145 | | |
| |||
153 | 168 | | |
154 | 169 | | |
155 | 170 | | |
156 | | - | |
| 171 | + | |
| 172 | + | |
157 | 173 | | |
158 | 174 | | |
159 | 175 | | |
160 | 176 | | |
161 | 177 | | |
| 178 | + | |
162 | 179 | | |
163 | 180 | | |
164 | 181 | | |
| |||
0 commit comments