simplify Athena runtime and centralize MCP management

This commit is contained in:
Mikei386 committed 2026-08-26 09:57:38 +02:00
1 parent 104c9904a5
commit ab671a6396
31 files changed
+918 -419

No files matched your search

+14 -3
View File
@@ -2,11 +2,13 @@ FROM ghcr.io/github/github-mcp-server@sha256:1817b57d43916532dc002bdc5f344d639bd
FROM ghcr.io/blakeem/navidrome-mcp:2.2.0@sha256:047f911a5a8f7cc8f185bb4d6e7ca6c435542edefff4694a00c2f718ab0ee7f5 AS navidrome
FROM samanhappy/mcphub:1.0.32
FROM samanhappy/mcphub:1.0.32@sha256:df34df85e639743d0bf4b64182fc2f47586bf544beb08c85a5e5d693891daad3
ARG MCP_VERSION=1.29.0
ARG ARR_MCP_VERSION=1.0.1
ARG YT_DLP_VERSION=2026.7.4
ARG FRITZ_MCP_VERSION=0.8.0
ARG FRITZ_MCP_SHA256=47f4e2b5595a522aeda4aed9f5c65a57e500c3589c9f4262b8fbf72bd8c72bd4
USER root
@@ -18,13 +20,21 @@ RUN python3 -m pip install --no-cache-dir \
"arr-mcp[mcp]==${ARR_MCP_VERSION}" \
"yt-dlp==${YT_DLP_VERSION}"
# Released, checksum-pinned Go binary. Keeping the download in the reproducible
# image build prevents MCPHub upgrades from silently dropping the Fritz tools.
RUN mkdir -p /opt/casaderoll \
&& python3 -c 'import sys,urllib.request; v=sys.argv[1]; urllib.request.urlretrieve(f"https://github.com/kambriso/fritzbox-mcp-server/releases/download/v{v}/fritz-mcp-linux-amd64", "/opt/casaderoll/fritz-mcp")' "${FRITZ_MCP_VERSION}" \
&& echo "${FRITZ_MCP_SHA256} /opt/casaderoll/fritz-mcp" | sha256sum -c - \
&& chmod 0755 /opt/casaderoll/fritz-mcp
COPY --from=github /server/github-mcp-server /usr/local/bin/github-mcp-server
COPY --from=navidrome /app /opt/casaderoll/navidrome
COPY platform/mcp/deemix_mcp.py /opt/casaderoll/mcps/deemix_mcp.py
COPY platform/web-search/web_search_mcp.py /opt/casaderoll/mcps/web_search_mcp.py
COPY platform/mcp/patches/mcp_sonarr.py /usr/local/lib/python3.13/site-packages/arr_mcp/mcp/mcp_sonarr.py
COPY platform/mcp/patches/mcp_radarr.py /usr/local/lib/python3.13/site-packages/arr_mcp/mcp/mcp_radarr.py
COPY config/mcp-registry.json /opt/casaderoll/config/mcp-registry.json
COPY platform/mcphub/configure-settings.py /opt/casaderoll/configure-settings.py
COPY platform/mcphub/run-with-env.py /usr/local/bin/run-with-env
COPY platform/mcphub/casaderoll-entrypoint.sh /usr/local/bin/casaderoll-mcphub-entrypoint
@@ -36,7 +46,8 @@ RUN node -e 'const fs=require("node:fs"); const p="/opt/casaderoll/navidrome/dis
ENV MCPHUB_SETTING_PATH=/app/data/ \
REQUEST_TIMEOUT=120000 \
NODE_ENV=production
NODE_ENV=production \
FRITZ_MCP_VERSION=${FRITZ_MCP_VERSION}
ENTRYPOINT ["/usr/local/bin/casaderoll-mcphub-entrypoint"]
CMD ["/usr/local/bin/entrypoint.sh", "pnpm", "start"]
+6 -6
View File
@@ -9,8 +9,8 @@ sie sich einen Docker-Container und ein Appdata-Backup teilen.
- ARR, Deemix, Navidrome und GitHub laufen als lokale stdio-Unterprozesse.
- Home Assistant und MUA/Unraid sind vorhandene HTTP-MCP-Endpunkte und werden
vom Hub direkt weitergereicht.
- Der Web-Adapter kann hier laufen; SearXNG/TinySearch dürfen getrennte
Backend-Dienste bleiben.
- Allgemeine Webrecherche bleibt ein eingebautes Hermes-Werkzeug. Der alte
Athena-Webadapter sowie SearXNG/TinySearch gehören nicht zum MCPHub-Image.
- Athenas administrativer Operator ist hostgebunden und bleibt auf Athena.
MCPHub reicht den vorhandenen, nur über WireGuard erreichbaren HTTP-Endpunkt
`http://192.168.1.212:8202/mcp` als `/mcp/athena-operator` weiter. Dadurch
@@ -64,7 +64,7 @@ read-only-Aufruf prüfen und erst danach Clients auf
`http://UNRAID-IP:8787/mcp/{server}` umstellen. Der alte Athena-Container wird
erst gestoppt, wenn Hermes und OpenWebUI nachweislich über MCPHub funktionieren.
Aktueller Stand: Athena Operator, ARR, Deemix, Navidrome, GitHub, Home Assistant
und MUA/Unraid sind auf MCPHub registriert. Alte portable Athena-MCP-Container
bleiben vorläufig als ausgeschaltetes Rückfallnetz bestehen. Der Web-Adapter
ist der letzte noch offene Migrationspunkt.
Aktueller Stand: Athena Operator, ARR, Deemix, Navidrome, GitHub, Home Assistant,
MUA/Unraid und FRITZ!Box sind auf MCPHub registriert. Alte portable
Athena-MCP-Container bleiben ausgeschaltet als kurzfristiges Rückfallnetz
bestehen. Der frühere Webadapter ist nicht mehr Bestandteil des Images.
+9
View File
@@ -18,4 +18,13 @@ fi
JWT_SECRET=$(cat "$jwt_file")
export JWT_SECRET
# Reconcile the persistent MCPHub state with the versioned registry on every
# start. Existing users, bearer tokens and per-server enabled flags survive.
# This makes image upgrades reproducible instead of relying on manual edits in
# MCPHub's database/UI.
settings_file="$state_dir/mcp_settings.json"
python3 /opt/casaderoll/configure-settings.py \
"$settings_file" /run/secrets/mcphub \
--registry /opt/casaderoll/config/mcp-registry.json
exec "$@"
+53 -58
View File
@@ -11,6 +11,7 @@ import argparse
import json
import os
import pathlib
import re
import secrets
import tempfile
import uuid
@@ -34,75 +35,70 @@ def env_file(path: pathlib.Path) -> dict[str, str]:
return values
def required(values: dict[str, str], key: str, source: pathlib.Path) -> str:
value = values.get(key, "").strip()
if not value:
raise SystemExit(f"{key} is missing in {source}")
PLACEHOLDER = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
def expand(value: object, values: dict[str, str], source: pathlib.Path) -> object:
"""Resolve secret placeholders without ever logging their values."""
if isinstance(value, str):
def replace(match: re.Match[str]) -> str:
key = match.group(1)
resolved = values.get(key, "").strip()
if not resolved:
raise SystemExit(f"{key} is missing in {source}")
return resolved
return PLACEHOLDER.sub(replace, value)
if isinstance(value, list):
return [expand(item, values, source) for item in value]
if isinstance(value, dict):
return {key: expand(item, values, source) for key, item in value.items()}
return value
def registry_servers(registry: pathlib.Path, secrets_dir: pathlib.Path,
existing: dict[str, object]) -> dict[str, object]:
document = json.loads(registry.read_text(encoding="utf-8"))
if document.get("version") != 1:
raise SystemExit("Unsupported MCP registry schema")
result: dict[str, object] = {}
for item in document.get("servers", []):
spec = item.get("hub")
if not isinstance(spec, dict):
continue
server_id = str(item.get("hermes_id") or item["id"])
spec = dict(spec)
secret_name = str(spec.pop("secret_file", ""))
secret_path = secrets_dir / secret_name if secret_name else secrets_dir
values = env_file(secret_path) if secret_name else {}
rendered = expand(spec, values, secret_path)
if isinstance(rendered, dict) and isinstance(rendered.get("url"), str):
rendered["url"] = re.sub(r"(?<!:)//+", "/", rendered["url"])
previous = existing.get(server_id)
if isinstance(previous, dict) and "enabled" in previous:
rendered["enabled"] = bool(previous["enabled"])
result[server_id] = rendered
return result
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("settings", type=pathlib.Path)
parser.add_argument("secrets", type=pathlib.Path)
parser.add_argument(
"--registry", type=pathlib.Path,
default=pathlib.Path("/opt/casaderoll/config/mcp-registry.json"),
)
parser.add_argument("--web-backend", default="", help="TinySearch MCP URL; empty keeps web disabled")
parser.add_argument("--searxng", default="", help="SearXNG base URL")
args = parser.parse_args()
args.settings.parent.mkdir(parents=True, exist_ok=True)
settings = json.loads(args.settings.read_text(encoding="utf-8")) if args.settings.exists() else {}
ha_path = args.secrets / "homeassistant.env"
mua_path = args.secrets / "mua.env"
ha = env_file(ha_path)
mua = env_file(mua_path)
servers: dict[str, object] = {
"athena-operator": {
"type": "streamable-http",
"url": "http://192.168.1.212:8202/mcp",
"owner": "admin",
"enabled": True,
},
"arr": {
"type": "stdio",
"command": "/usr/local/bin/run-with-env",
"args": ["/run/secrets/mcphub/arr.env", "--", "arr-mcp", "--transport", "stdio", "--auth-type", "none"],
"enabled": True,
},
"deemix": {
"type": "stdio",
"command": "/usr/local/bin/run-with-env",
"args": ["/run/secrets/mcphub/deemix.env", "--", "python3", "/opt/casaderoll/mcps/deemix_mcp.py"],
"env": {"MCP_TRANSPORT": "stdio"},
"enabled": True,
},
"navidrome": {
"type": "stdio",
"command": "/usr/local/bin/run-with-env",
"args": ["/run/secrets/mcphub/navidrome.env", "--", "node", "/opt/casaderoll/navidrome/dist/index.js"],
"env": {"MCP_TRANSPORT": "stdio", "MCP_HTTP_EXPOSE": "false", "WEBUI_ENABLED": "false"},
"enabled": True,
},
"github": {
"type": "stdio",
"command": "/usr/local/bin/run-with-env",
"args": ["/run/secrets/mcphub/github.env", "--", "/usr/local/bin/github-mcp-server", "stdio", "--read-only", "--tools", "search_repositories,get_file_contents,search_code"],
"enabled": True,
},
"homeassistant": {
"type": "streamable-http",
"url": required(ha, "HASS_URL", ha_path).rstrip("/") + "/api/hass_mcp",
"headers": {"Authorization": "Bearer " + required(ha, "HASS_TOKEN", ha_path)},
"owner": "admin",
"enabled": True,
},
"unraid": {
"type": "streamable-http",
"url": required(mua, "MUA_MCP_URL", mua_path),
"headers": {"Authorization": "Bearer " + required(mua, "MUA_MCP_BEARER_TOKEN", mua_path)},
"owner": "admin",
"enabled": True,
},
}
servers = registry_servers(
args.registry,
args.secrets,
settings.get("mcpServers", {}) if isinstance(settings.get("mcpServers"), dict) else {},
)
if args.web_backend:
servers["web"] = {
"type": "stdio",
@@ -144,7 +140,6 @@ def main() -> None:
system = settings.setdefault("systemConfig", {})
system.setdefault("routing", {})["skipAuth"] = False
args.settings.parent.mkdir(parents=True, exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix=".mcp-settings-", dir=args.settings.parent)
try:
with os.fdopen(fd, "w", encoding="utf-8") as handle: