mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-08-16 01:52:30 +02:00
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 Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,13 @@
|
|||||||
## 1.17 (14-07-2026)
|
## 1.18 (14-07-2026)
|
||||||
|
|
||||||
- **Breaking:** remove the standalone Claude Code web terminal (ttyd/tmux service, port `7681`, and the `enable_terminal`, `terminal_username`, `terminal_password`, `terminal_workspace` options). The add-on is now built purely around Claude Desktop; Claude Code remains installed and powers Desktop cowork/dispatch sessions with the RTK hook, Caveman, and MCP servers intact. If the add-on refuses to start after the update, open its Configuration tab and re-save to drop the removed options.
|
- **Breaking:** remove the standalone Claude Code web terminal (ttyd/tmux service, port `7681`, and the `enable_terminal`, `terminal_username`, `terminal_password`, `terminal_workspace` options). The add-on is now built purely around Claude Desktop; Claude Code remains installed and powers Desktop cowork/dispatch sessions with the RTK hook, Caveman, and MCP servers intact. If the add-on refuses to start after the update, open its Configuration tab and re-save to drop the removed options.
|
||||||
- Remove the `claude-direct` and `claude-headroom` terminal wrapper scripts and the unused `ha_smart_context` and `dangerously_skip_permissions` options.
|
- Remove the `claude-direct` and `claude-headroom` terminal wrapper scripts and the unused `ha_smart_context` and `dangerously_skip_permissions` options.
|
||||||
- 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 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.
|
- 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`.
|
- 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.
|
- 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.
|
- Install `uv` and use it for the `additional_pip` option for much faster package installs.
|
||||||
|
|
||||||
|
|||||||
@@ -69,9 +69,11 @@ RUN curl -fsSL --retry 3 --retry-delay 2 \
|
|||||||
# 3 Install apps #
|
# 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/ /
|
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
|
# Uses /bin for compatibility purposes
|
||||||
# hadolint ignore=DL4005
|
# hadolint ignore=DL4005
|
||||||
|
|||||||
@@ -96,10 +96,10 @@ The Headroom dashboard is available at:
|
|||||||
http://<home-assistant-host>:8787/dashboard
|
http://<home-assistant-host>:8787/dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
through the default `8787/tcp` port mapping. Treat this endpoint as sensitive:
|
through the default `8787/tcp` port mapping. The dashboard is unauthenticated
|
||||||
it serves your local network only — do not expose it directly to the public
|
and is reachable wherever Home Assistant publishes that port, so treat it as
|
||||||
internet, and unmap the port in the add-on **Network** section if you do not
|
sensitive: do not expose it directly to the public internet, and unmap the port
|
||||||
want it reachable at all.
|
in the add-on **Network** section if you do not want it reachable at all.
|
||||||
|
|
||||||
## Home Assistant MCP bridge
|
## 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
|
3. Set `enable_ha_mcp: true` and paste the token into `ha_mcp_token` in the
|
||||||
add-on configuration, then restart the add-on.
|
add-on configuration, then restart the add-on.
|
||||||
|
|
||||||
The add-on bridges Claude to the integration's SSE endpoint with `mcp-proxy`.
|
The add-on bridges Claude to the integration's stateless Streamable HTTP
|
||||||
Override `ha_mcp_url` only if your Home Assistant instance is not reachable as
|
endpoint (`/api/mcp`) with `mcp-proxy`. Override `ha_mcp_url` only if your Home
|
||||||
`homeassistant:8123` from add-ons.
|
Assistant instance is not reachable as `homeassistant:8123` from add-ons.
|
||||||
|
|
||||||
## Custom scripts
|
## Custom scripts
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ magic link into the in-session Chromium (not a phone).
|
|||||||
- Add-on Configuration → `additional_apps: chromium`, restart (installed by
|
- Add-on Configuration → `additional_apps: chromium`, restart (installed by
|
||||||
`rootfs/etc/cont-init.d/80-configuration.sh`).
|
`rootfs/etc/cont-init.d/80-configuration.sh`).
|
||||||
- Add the two `xdg-settings`/`xdg-mime` commands to the custom script
|
- 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).
|
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`.
|
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
|
- `--password-store=gnome-libsecret` forces Electron to use the libsecret backend instead
|
||||||
of falling back to plaintext.
|
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
|
(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.
|
re-unlocked automatically each boot by the same `autostart` line — the sign-in then sticks.
|
||||||
|
|
||||||
### User-side workaround (no rebuild)
|
### User-side workaround (no rebuild)
|
||||||
- Add-on Configuration → `additional_apps: gnome-keyring, libsecret-1-0, dbus-x11`, restart.
|
- Add-on Configuration → `additional_apps: gnome-keyring, libsecret-1-0, dbus-x11`, restart.
|
||||||
- Add the keyring-start lines above to the custom script
|
- 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).
|
with `--password-store=gnome-libsecret` (e.g. edit the in-session openbox autostart).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ options:
|
|||||||
auto_update: true
|
auto_update: true
|
||||||
github_email: ""
|
github_email: ""
|
||||||
enable_ha_mcp: false
|
enable_ha_mcp: false
|
||||||
ha_mcp_url: http://homeassistant:8123/mcp_server/sse
|
ha_mcp_url: http://homeassistant:8123/api/mcp
|
||||||
ha_mcp_token: ""
|
ha_mcp_token: ""
|
||||||
github_token: ""
|
github_token: ""
|
||||||
github_username: ""
|
github_username: ""
|
||||||
@@ -92,5 +92,5 @@ slug: claude_desktop
|
|||||||
tmpfs: true
|
tmpfs: true
|
||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
url: https://github.com/alexbelgium/hassio-addons
|
||||||
version: "1.17"
|
version: "1.18"
|
||||||
video: true
|
video: true
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ HA_MCP_ENABLED=false
|
|||||||
HA_MCP_URL=""
|
HA_MCP_URL=""
|
||||||
HA_MCP_TOKEN=""
|
HA_MCP_TOKEN=""
|
||||||
if bashio::config.true 'enable_ha_mcp'; then
|
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
|
if bashio::config.has_value 'ha_mcp_token'; then
|
||||||
HA_MCP_TOKEN="$(bashio::config 'ha_mcp_token')"
|
HA_MCP_TOKEN="$(bashio::config 'ha_mcp_token')"
|
||||||
fi
|
fi
|
||||||
@@ -63,7 +63,7 @@ if bashio::config.true 'enable_ha_mcp'; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
HEADROOM_ENABLED="$HEADROOM_ENABLED" HEADROOM_BIN="$(command -v headroom || echo headroom)" \
|
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" \
|
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)" \
|
MCP_PROXY_BIN="$(command -v mcp-proxy || echo mcp-proxy)" \
|
||||||
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
|
CLAUDE_DESKTOP_CONFIG="$CLAUDE_DESKTOP_CONFIG" CLAUDE_CODE_CONFIG="$CLAUDE_CODE_CONFIG" \
|
||||||
@@ -72,10 +72,10 @@ import json
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
MANAGED_COMMANDS = {
|
MANAGED_BASENAMES = {
|
||||||
"headroom": {os.environ["HEADROOM_BIN"], "headroom"},
|
"headroom": "headroom",
|
||||||
"tokensave": {os.environ["TOKENSAVE_BIN"], "tokensave"},
|
"tokensave": "tokensave",
|
||||||
"homeassistant": {os.environ["MCP_PROXY_BIN"], "mcp-proxy"},
|
"homeassistant": "mcp-proxy",
|
||||||
}
|
}
|
||||||
|
|
||||||
desired = {}
|
desired = {}
|
||||||
@@ -84,14 +84,27 @@ if os.environ["HEADROOM_ENABLED"] == "true":
|
|||||||
if os.environ["TOKENSAVE_ENABLED"] == "true":
|
if os.environ["TOKENSAVE_ENABLED"] == "true":
|
||||||
desired["tokensave"] = {"command": os.environ["TOKENSAVE_BIN"], "args": ["serve"]}
|
desired["tokensave"] = {"command": os.environ["TOKENSAVE_BIN"], "args": ["serve"]}
|
||||||
if os.environ["HA_MCP_ENABLED"] == "true":
|
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"] = {
|
desired["homeassistant"] = {
|
||||||
"command": os.environ["MCP_PROXY_BIN"],
|
"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"]},
|
"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):
|
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)):
|
for config_var, stdio_type in (("CLAUDE_DESKTOP_CONFIG", False), ("CLAUDE_CODE_CONFIG", True)):
|
||||||
path = Path(os.environ[config_var])
|
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):
|
if not isinstance(servers, dict):
|
||||||
servers = {}
|
servers = {}
|
||||||
changed = False
|
changed = False
|
||||||
for name in MANAGED_COMMANDS:
|
for name in MANAGED_BASENAMES:
|
||||||
existing = servers.get(name)
|
existing = servers.get(name)
|
||||||
if name in desired:
|
if name in desired:
|
||||||
entry = dict(desired[name])
|
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)
|
data.pop("mcpServers", None)
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
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
|
PY
|
||||||
|
|
||||||
# Guide Claude to actually use the headroom compression tools so the MCP integration produces
|
# Guide Claude to actually use the headroom compression tools so the MCP integration produces
|
||||||
|
|||||||
Reference in New Issue
Block a user