Files
AI-Profile-Router/platform/mcp/sync-clients.py
T

178 lines
6.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate Hermes and OpenWebUI MCP registrations from one JSON registry."""
from __future__ import annotations
import argparse
import json
import os
import pathlib
import sqlite3
import time
BEGIN = "# BEGIN MANAGED MCP SERVERS"
END = "# END MANAGED MCP SERVERS"
CLIENT_TOKEN = ""
def env_file(path: str) -> dict[str, str]:
values: dict[str, str] = {}
source = pathlib.Path(path)
if not source.is_file():
return values
for raw in source.read_text(encoding="utf-8", errors="replace").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
values[key.strip()] = value.strip().strip('"').strip("'")
return values
def enabled(item: dict) -> bool:
required = item.get("required_file")
if required and not pathlib.Path(required).is_file():
return False
source = item.get("env_file")
if source:
values = env_file(source)
url_ready = bool(item.get("url")) or bool(values.get(item.get("url_env", "")))
key_ready = (not item.get("key_env")
or bool(values.get(item["key_env"]))
or (item.get("key_env") == "MCPHUB_BEARER_TOKEN"
and bool(CLIENT_TOKEN)))
return url_ready and key_ready
return True
def resolved(item: dict) -> tuple[str, str]:
if item.get("env_file"):
values = env_file(item["env_file"])
url = item.get("url") or values[item["url_env"]]
key = values.get(item.get("key_env", ""), "")
if not key and item.get("key_env") == "MCPHUB_BEARER_TOKEN":
key = CLIENT_TOKEN
return url, key
return item["url"], ""
def active(registry: pathlib.Path, client: str) -> list[dict]:
document = json.loads(registry.read_text(encoding="utf-8"))
if document.get("version") != 1 or not isinstance(document.get("servers"), list):
raise SystemExit("Unsupported MCP registry schema")
return [item for item in document["servers"] if client in item.get("clients", []) and enabled(item)]
def yaml_quote(value: str) -> str:
return json.dumps(value, ensure_ascii=False)
def hermes_block(items: list[dict]) -> str:
lines = [BEGIN, "mcp_servers:"]
for item in items:
url, key = resolved(item)
lines.extend([
f" {item.get('hermes_id', item['id'])}:",
f" url: {yaml_quote(url)}",
])
if key:
lines.extend([" headers:", f" Authorization: {yaml_quote('Bearer ' + key)}"])
if "tool_include" in item:
lines.append(" tools:")
lines.append(" include:")
for tool in item["tool_include"]:
lines.append(f" - {yaml_quote(str(tool))}")
elif "tool_exclude" in item:
lines.append(" tools:")
lines.append(" exclude:")
for tool in item["tool_exclude"]:
lines.append(f" - {yaml_quote(str(tool))}")
lines.extend([
f" timeout: {int(item.get('timeout', 300))}",
" connect_timeout: 30",
" supports_parallel_tool_calls: false",
])
lines.append(END)
return "\n".join(lines) + "\n"
def update_hermes(path: pathlib.Path, block: str) -> None:
if not path.is_file():
return
text = path.read_text(encoding="utf-8")
if BEGIN in text and END in text:
prefix, rest = text.split(BEGIN, 1)
_, suffix = rest.split(END, 1)
text = prefix.rstrip() + "\n\n" + block + suffix.lstrip("\n")
else:
marker = "\nmcp_servers:"
if marker in text:
text = text.split(marker, 1)[0].rstrip() + "\n\n" + block
else:
text = text.rstrip() + "\n\n" + block
path.write_text(text, encoding="utf-8")
def openwebui_connection(item: dict) -> dict:
url, key = resolved(item)
config = {"enable": True, "access_grants": []}
if item.get("functions"):
config["function_name_filter_list"] = item["functions"]
elif item.get("tool_include"):
config["function_name_filter_list"] = ",".join(item["tool_include"])
return {
"url": url, "path": "", "type": "mcp",
"auth_type": item.get("auth_type", "none"), "headers": None,
"key": key, "config": config,
"info": {"id": item["id"], "name": item["name"], "description": item["description"]},
}
def update_openwebui(db: pathlib.Path, items: list[dict]) -> None:
con = sqlite3.connect(db)
now = int(time.time())
row = con.execute("select value from config where key=?", ("tool_server.connections",)).fetchone()
old = json.loads(row[0]) if row else []
if not isinstance(old, list):
raise SystemExit("Unexpected OpenWebUI tool_server.connections format")
managed_ids = {
"athena-platform", "athena-operator-local", "web-general-local", "github-local",
"homeassistant-local", "arr-local", "navidrome-local", "mua",
"mua-readonly-local", "athena-terminal-local", "unraid-readonly-local", "web-local",
}
keep = [entry for entry in old if str((entry.get("info") or {}).get("id", "")) not in managed_ids]
keep.extend(openwebui_connection(item) for item in items)
with con:
con.execute(
"""insert into config (key,value,updated_at) values (?,?,?)
on conflict(key) do update set value=excluded.value,updated_at=excluded.updated_at""",
("tool_server.connections", json.dumps(keep, ensure_ascii=False), now),
)
con.close()
def main() -> None:
global CLIENT_TOKEN
parser = argparse.ArgumentParser()
parser.add_argument("--registry", type=pathlib.Path, required=True)
parser.add_argument("--hermes", type=pathlib.Path, action="append", default=[])
parser.add_argument("--openwebui-db", type=pathlib.Path)
parser.add_argument("--mcphub-token-file", type=pathlib.Path)
args = parser.parse_args()
if args.mcphub_token_file:
CLIENT_TOKEN = args.mcphub_token_file.read_text(encoding="utf-8").strip()
if not CLIENT_TOKEN:
raise SystemExit("MCPHub token file is empty")
if args.hermes:
block = hermes_block(active(args.registry, "hermes"))
for path in args.hermes:
update_hermes(path, block)
if args.openwebui_db:
update_openwebui(args.openwebui_db, active(args.registry, "openwebui"))
print("MCP_CLIENT_SYNC_OK")
if __name__ == "__main__":
main()