fix(claude_desktop): correct HA MCP endpoint, harden config perms and chmod scope

Home Assistant's MCP Server integration serves stateless Streamable HTTP at
/api/mcp; mcp-proxy defaults to SSE, so the previous registration (SSE at
/mcp_server/sse) could never attach. Pass --transport=streamablehttp
--stateless and default ha_mcp_url to /api/mcp.

Match managed MCP entries by binary basename outside $HOME so a base-image
path change still updates them, while user-installed binaries under $HOME
remain untouched. Resolve tokensave via command -v like the others.

Write Claude config files 0600 (they hold the HA long-lived token in clear
text) and scope the build-time chmod +x pass to the shipped script dirs.

Docs: dashboard reachability wording, stale /config/data HOME, and the
custom-script filename (claude_desktop.sh, per the $slug.sh template).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
alexbelgium
2026-07-14 16:45:50 +02:00
parent b56be1f57d
commit 13a4548372
6 changed files with 42 additions and 23 deletions

View File

@@ -5,7 +5,9 @@
- Fix the Headroom dashboard being unreachable at `http://<host>:8787/dashboard`: the supervised proxy only listened on `127.0.0.1`; it now binds `0.0.0.0` so the mapped port works.
- Fix dispatch/remote sessions and sign-in persistence: install the missing `gnome-keyring` package. The existing keyring bootstrap silently no-oped without it, leaving Electron `safeStorage` unavailable ("cannot store allowlist cache"), so auth tokens and dispatch permission grants were lost on restart.
- Add the tokensave code-intelligence MCP server (pinned 7.2.0, built from source like RTK), registered for both Claude Desktop and Claude Code; disable with `install_tokensave: false`.
- Implement the Home Assistant MCP bridge for real: `enable_ha_mcp` plus new `ha_mcp_url`/`ha_mcp_token` options register Home Assistant's MCP Server integration in Claude through `mcp-proxy`.
- Implement the Home Assistant MCP bridge for real: `enable_ha_mcp` plus new `ha_mcp_url`/`ha_mcp_token` options register Home Assistant's MCP Server integration in Claude through `mcp-proxy`, using the integration's stateless Streamable HTTP endpoint (`/api/mcp`).
- Write the Claude configuration files with `0600` permissions, since they hold the Home Assistant access token in clear text.
- Restrict the build-time `chmod +x` pass to the directories the add-on actually ships scripts in instead of traversing the whole image.
- Register add-on-managed MCP servers in Claude Code's `~/.claude.json` as well as Claude Desktop's config, without clobbering user-customized entries.
- Install `uv` and use it for the `additional_pip` option for much faster package installs.

View File

@@ -69,9 +69,11 @@ RUN curl -fsSL --retry 3 --retry-delay 2 \
# 3 Install apps #
##################
# Add rootfs
# Add rootfs. Only the directories this add-on ships scripts in are traversed, so the chmod
# cannot alter executables elsewhere in the image.
COPY rootfs/ /
RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \;
RUN find /etc/cont-init.d /etc/s6-overlay /defaults /usr/local/bin -type f \
\( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \;
# Uses /bin for compatibility purposes
# hadolint ignore=DL4005

View File

@@ -96,10 +96,10 @@ The Headroom dashboard is available at:
http://<home-assistant-host>:8787/dashboard
```
through the default `8787/tcp` port mapping. Treat this endpoint as sensitive:
it serves your local network only — do not expose it directly to the public
internet, and unmap the port in the add-on **Network** section if you do not
want it reachable at all.
through the default `8787/tcp` port mapping. The dashboard is unauthenticated
and is reachable wherever Home Assistant publishes that port, so treat it as
sensitive: do not expose it directly to the public internet, and unmap the port
in the add-on **Network** section if you do not want it reachable at all.
## Home Assistant MCP bridge
@@ -111,9 +111,9 @@ To let Claude query and control Home Assistant:
3. Set `enable_ha_mcp: true` and paste the token into `ha_mcp_token` in the
add-on configuration, then restart the add-on.
The add-on bridges Claude to the integration's SSE endpoint with `mcp-proxy`.
Override `ha_mcp_url` only if your Home Assistant instance is not reachable as
`homeassistant:8123` from add-ons.
The add-on bridges Claude to the integration's stateless Streamable HTTP
endpoint (`/api/mcp`) with `mcp-proxy`. Override `ha_mcp_url` only if your Home
Assistant instance is not reachable as `homeassistant:8123` from add-ons.
## Custom scripts

View File

@@ -54,7 +54,7 @@ magic link into the in-session Chromium (not a phone).
- Add-on Configuration → `additional_apps: chromium`, restart (installed by
`rootfs/etc/cont-init.d/80-configuration.sh`).
- Add the two `xdg-settings`/`xdg-mime` commands to the custom script
`/addon_configs/db21ed7f_claude-desktop/claude-desktop.sh` (the image ships no standalone
`/addon_configs/db21ed7f_claude-desktop/claude_desktop.sh` (the image ships no standalone
terminal).
---
@@ -98,14 +98,14 @@ Claude Desktop uses. No extra `dbus-launch` is needed.
then exposes the Secret Service and exports `GNOME_KEYRING_CONTROL`/`SSH_AUTH_SOCK`.
- `--password-store=gnome-libsecret` forces Electron to use the libsecret backend instead
of falling back to plaintext.
3. Persistence: the keyring DB lives in `$HOME/.local/share/keyrings/` and `HOME=/config/data`
3. Persistence: the keyring DB lives in `$HOME/.local/share/keyrings/` and `HOME=/data/data`
(persistent add-on storage), so the empty-password login keyring survives restarts and is
re-unlocked automatically each boot by the same `autostart` line — the sign-in then sticks.
### User-side workaround (no rebuild)
- Add-on Configuration → `additional_apps: gnome-keyring, libsecret-1-0, dbus-x11`, restart.
- Add the keyring-start lines above to the custom script
`/addon_configs/db21ed7f_claude-desktop/claude-desktop.sh`, and relaunch Claude Desktop
`/addon_configs/db21ed7f_claude-desktop/claude_desktop.sh`, and relaunch Claude Desktop
with `--password-store=gnome-libsecret` (e.g. edit the in-session openbox autostart).
---

View File

@@ -42,7 +42,7 @@ options:
auto_update: true
github_email: ""
enable_ha_mcp: false
ha_mcp_url: http://homeassistant:8123/mcp_server/sse
ha_mcp_url: http://homeassistant:8123/api/mcp
ha_mcp_token: ""
github_token: ""
github_username: ""

View File

@@ -48,7 +48,7 @@ HA_MCP_ENABLED=false
HA_MCP_URL=""
HA_MCP_TOKEN=""
if bashio::config.true 'enable_ha_mcp'; then
HA_MCP_URL="$(bashio::config 'ha_mcp_url' 'http://homeassistant:8123/mcp_server/sse')"
HA_MCP_URL="$(bashio::config 'ha_mcp_url' 'http://homeassistant:8123/api/mcp')"
if bashio::config.has_value 'ha_mcp_token'; then
HA_MCP_TOKEN="$(bashio::config 'ha_mcp_token')"
fi
@@ -63,7 +63,7 @@ if bashio::config.true 'enable_ha_mcp'; then
fi
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="/usr/local/bin/tokensave" \
TOKENSAVE_ENABLED="$TOKENSAVE_ENABLED" TOKENSAVE_BIN="$(command -v tokensave || echo tokensave)" \
HA_MCP_ENABLED="$HA_MCP_ENABLED" HA_MCP_URL="$HA_MCP_URL" HA_MCP_TOKEN="$HA_MCP_TOKEN" \
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
@@ -72,10 +72,10 @@ import json
import os
from pathlib import Path
MANAGED_COMMANDS = {
"headroom": {os.environ["HEADROOM_BIN"], "headroom"},
"tokensave": {os.environ["TOKENSAVE_BIN"], "tokensave"},
"homeassistant": {os.environ["MCP_PROXY_BIN"], "mcp-proxy"},
MANAGED_BASENAMES = {
"headroom": "headroom",
"tokensave": "tokensave",
"homeassistant": "mcp-proxy",
}
desired = {}
@@ -84,14 +84,27 @@ if os.environ["HEADROOM_ENABLED"] == "true":
if os.environ["TOKENSAVE_ENABLED"] == "true":
desired["tokensave"] = {"command": os.environ["TOKENSAVE_BIN"], "args": ["serve"]}
if os.environ["HA_MCP_ENABLED"] == "true":
# Home Assistant's MCP Server integration speaks stateless Streamable HTTP on /api/mcp;
# mcp-proxy defaults to SSE, so the transport flags are required.
desired["homeassistant"] = {
"command": os.environ["MCP_PROXY_BIN"],
"args": [os.environ["HA_MCP_URL"]],
"args": ["--transport=streamablehttp", "--stateless", os.environ["HA_MCP_URL"]],
"env": {"API_ACCESS_TOKEN": os.environ["HA_MCP_TOKEN"]},
}
# An entry is add-on-managed when its command is one of our binaries living outside the
# persistent home. Matching on the basename (rather than the exact path recorded at write
# time) keeps entries updatable when a base-image upgrade moves the binary, while commands
# under $HOME stay untouched because those are user-installed.
HOME_PREFIX = os.path.expanduser("~") + os.sep
def is_managed(name, entry):
return isinstance(entry, dict) and entry.get("command") in MANAGED_COMMANDS[name]
if not isinstance(entry, dict):
return False
command = entry.get("command")
if not isinstance(command, str) or command.startswith(HOME_PREFIX):
return False
return os.path.basename(command) == MANAGED_BASENAMES[name]
for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_CONFIG", True)):
path = Path(os.environ[config_var])
@@ -107,7 +120,7 @@ for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_C
if not isinstance(servers, dict):
servers = {}
changed = False
for name in MANAGED_COMMANDS:
for name in MANAGED_BASENAMES:
existing = servers.get(name)
if name in desired:
entry = dict(desired[name])
@@ -128,6 +141,8 @@ for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_C
data.pop("mcpServers", None)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, indent=2) + "\n")
# The Home Assistant long-lived access token is stored here in clear text.
path.chmod(0o600)
PY
# Guide Claude to actually use the headroom compression tools so the MCP integration produces