#!/usr/bin/env python3 """Replace demo MCPHub entries with CasaDeRoll's declarative server set. The existing users, bearer keys, prompts and resources are preserved. Secret values are read locally and written only to the runtime settings file. """ from __future__ import annotations import argparse import json import os import pathlib import secrets import tempfile import uuid def env_file(path: pathlib.Path) -> dict[str, str]: values: dict[str, str] = {} if not path.is_file(): return values for raw in path.read_text(encoding="utf-8").splitlines(): line = raw.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) key, value = key.strip(), value.strip() if key.startswith("export "): key = key[7:].strip() if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'": value = value[1:-1] values[key] = value 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}") return value def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("settings", type=pathlib.Path) parser.add_argument("secrets", type=pathlib.Path) 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() 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, }, } if args.web_backend: servers["web"] = { "type": "stdio", "command": "python3", "args": ["/opt/casaderoll/mcps/web_search_mcp.py"], "env": { "TINYSEARCH_MCP_URL": args.web_backend, "SEARXNG_URL": args.searxng, }, "enabled": True, } settings["mcpServers"] = servers settings.setdefault("users", []) token_path = args.settings.parent / "client-token" keys = settings.setdefault("bearerKeys", []) client_key = next((item for item in keys if item.get("name") == "casaderoll-clients"), None) if client_key is None: token = secrets.token_urlsafe(48) client_key = { "id": str(uuid.uuid4()), "name": "casaderoll-clients", "token": token, "enabled": True, "kind": "system", "accessType": "all", "allowedGroups": [], "allowedServers": [], } keys.append(client_key) else: token = str(client_key["token"]) client_key["enabled"] = True client_key["accessType"] = "all" token_path.write_text(token + "\n", encoding="utf-8") os.chmod(token_path, 0o600) settings.setdefault("prompts", []) settings.setdefault("resources", []) 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: json.dump(settings, handle, indent=2, ensure_ascii=False) handle.write("\n") os.chmod(temporary, 0o600) os.replace(temporary, args.settings) finally: if os.path.exists(temporary): os.unlink(temporary) print("MCPHUB_SETTINGS_CONFIGURED") if __name__ == "__main__": main()