154 lines
5.4 KiB
Python
Executable File
154 lines
5.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"
|
|
|
|
|
|
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"]))
|
|
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", ""), "")
|
|
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)}"])
|
|
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"]
|
|
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", "deemix-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:
|
|
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)
|
|
args = parser.parse_args()
|
|
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()
|