simplify Athena runtime and centralize MCP management
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user