From 167448d778c4c27a4f19642883688574175253d3 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:34:15 +0200 Subject: [PATCH] publish MCPs through a filtered Hermes group --- README.md | 2 +- config/mcp-registry.json | 5 +-- config/unraid-templates/my-MCPHub.xml | 2 +- platform/hermes/install-profiles.sh | 2 +- platform/mcphub/README.md | 2 +- platform/mcphub/mcphub_admin_mcp.py | 48 ++++++++++++++++++++++++++- 6 files changed, 54 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7f4c0b3..e202f5b 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Hermes Agent und MCPHub laufen auf Unraid und werden dort mit Appdata gesichert. sind der produktive Zustand. Oberfläche und offizielle API ändern genau diese Datei; Container-Updates überschreiben sie nicht. `config/mcp-registry.json` ist nur der Neuinstallations-Seed. -- Hermes verbindet sich einmal mit MCPHubs gemeinsamem `/mcp`-Endpunkt. Neue +- Hermes verbindet sich einmal mit MCPHubs gefiltertem `/mcp/hermes`-Endpunkt. Neue aktivierte Server erscheinen dadurch nach **MCP neu laden**, ohne dass pro MCP eine weitere Hermes-Konfiguration geschrieben werden muss. - Modelle und Athena-Backups liegen auf `/data`. Hermes liegt vollständig unter diff --git a/config/mcp-registry.json b/config/mcp-registry.json index 419cb7a..2075538 100644 --- a/config/mcp-registry.json +++ b/config/mcp-registry.json @@ -6,7 +6,7 @@ "hermes_id": "mcphub", "name": "MCPHub", "description": "Zentraler Zugang zu allen auf Unraid aktivierten MCP-Servern. Neue Server erscheinen nach einem MCP-Reload ohne Änderung der Hermes-Konfiguration.", - "url": "http://192.168.1.2:8787/mcp", + "url": "http://192.168.1.2:8787/mcp/hermes", "clients": ["hermes"], "env_file": "/etc/mike-ai/mcphub-client.env", "key_env": "MCPHUB_BEARER_TOKEN", @@ -26,7 +26,8 @@ "args": ["/opt/casaderoll/mcps/mcphub_admin_mcp.py"], "env": { "MCPHUB_API_URL": "http://127.0.0.1:3000/api", - "MCPHUB_API_TOKEN_FILE": "/app/data/client-token" + "MCPHUB_API_TOKEN_FILE": "/app/data/client-token", + "MCPHUB_CLIENT_GROUP": "hermes" }, "enabled": true } diff --git a/config/unraid-templates/my-MCPHub.xml b/config/unraid-templates/my-MCPHub.xml index bc753bd..a543b19 100644 --- a/config/unraid-templates/my-MCPHub.xml +++ b/config/unraid-templates/my-MCPHub.xml @@ -1,7 +1,7 @@ MCPHub - casaderoll/mcphub:1.2.2 + casaderoll/mcphub:1.2.3 https://hub.docker.com/r/samanhappy/mcphub bridge diff --git a/platform/hermes/install-profiles.sh b/platform/hermes/install-profiles.sh index c8df4ae..22e5341 100755 --- a/platform/hermes/install-profiles.sh +++ b/platform/hermes/install-profiles.sh @@ -95,7 +95,7 @@ docker run --rm --entrypoint python \ -v "$HERMES_DATA_DIR:/hermes:rw" \ -v "$mcphub_registry:/run/input/mcp-registry.json:ro" \ "${token_mount[@]}" \ - casaderoll/mcphub:1.2.2 \ + casaderoll/mcphub:1.2.3 \ /stack/platform/mcp/sync-clients.py "${sync_args[@]}" "$STACK_DIR/platform/hermes/install-skills.sh" diff --git a/platform/mcphub/README.md b/platform/mcphub/README.md index 749369c..852c55d 100644 --- a/platform/mcphub/README.md +++ b/platform/mcphub/README.md @@ -80,7 +80,7 @@ deaktiviert. Fehlt die dedizierte Secret-Datei oder ein Pflichtfeld, verweigert er die Aktivierung technisch. Ein Neustart des MCPHub-Containers ist nicht erforderlich. -Hermes verbindet sich nur mit dem gemeinsamen Endpunkt `/mcp`. Dadurch werden +Hermes verbindet sich nur mit der verwalteten Gruppe `/mcp/hermes`. Dadurch werden neu aktivierte Server nach **MCP neu laden** sichtbar, ohne pro MCP eine weitere Hermes-Konfiguration zu erzeugen. Der interne Server `mcphub-admin` stellt die üblichen Installationswege für HTTP-, npm- und Python-MCPs als Werkzeuge bereit. diff --git a/platform/mcphub/mcphub_admin_mcp.py b/platform/mcphub/mcphub_admin_mcp.py index bab5858..725bea2 100644 --- a/platform/mcphub/mcphub_admin_mcp.py +++ b/platform/mcphub/mcphub_admin_mcp.py @@ -13,6 +13,7 @@ import os import pathlib import re import urllib.error +import urllib.parse import urllib.request from typing import Any @@ -21,6 +22,7 @@ from mcp.server.fastmcp import FastMCP API_BASE = os.environ.get("MCPHUB_API_URL", "http://127.0.0.1:3000/api").rstrip("/") TOKEN_FILE = pathlib.Path(os.environ.get("MCPHUB_API_TOKEN_FILE", "/app/data/client-token")) +CLIENT_GROUP = os.environ.get("MCPHUB_CLIENT_GROUP", "hermes").strip() NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$") NPM_PACKAGE_RE = re.compile(r"^(?:@[A-Za-z0-9._-]+/)?[A-Za-z0-9._-]+(?:@[A-Za-z0-9._+~-]+)?$") PYTHON_PACKAGE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*(?:==[A-Za-z0-9._+~-]+)?$") @@ -135,9 +137,35 @@ def _install(name: str, config: dict[str, Any]) -> str: if any(isinstance(row, dict) and row.get("name") == name for row in rows): raise HubError(f"Server already exists: {name}; inspect or update it instead") result = _request("POST", "/servers", {"name": name, "config": config}) + group_result = _add_to_group(name) verified = _request("GET", f"/servers/{name}") data = verified.get("data", verified) if isinstance(verified, dict) else verified - return json.dumps({"installed": True, "server": _redact(data), "api_result": _redact(result)}, ensure_ascii=False) + return json.dumps({ + "installed": True, + "published_to": CLIENT_GROUP, + "server": _redact(data), + "api_result": _redact(result), + "group_result": _redact(group_result), + }, ensure_ascii=False) + + +def _add_to_group(name: str, tools: list[str] | None = None) -> Any: + if not CLIENT_GROUP: + return {"skipped": "no-client-group"} + group = urllib.parse.quote(CLIENT_GROUP, safe="") + configs = _request("GET", f"/groups/{group}/server-configs") + rows = configs.get("data", []) if isinstance(configs, dict) else [] + present = any(isinstance(row, dict) and row.get("name") == name for row in rows) + result: Any = {"already_present": True} + if not present: + result = _request("POST", f"/groups/{group}/servers", {"serverName": name}) + if tools is not None: + result = _request( + "PUT", + f"/groups/{group}/server-configs/{urllib.parse.quote(name, safe='')}/tools", + {"tools": tools}, + ) + return result @mcp.tool() @@ -197,6 +225,24 @@ def mcphub_admin_reload(name: str) -> str: return json.dumps(_redact(result), ensure_ascii=False) +@mcp.tool() +def mcphub_admin_publish_to_hermes(name: str, tools: list[str] | None = None) -> str: + """Publish one installed server to the central Hermes group. Optionally expose only the named tools; omit tools to expose all.""" + name = _name(name) + selected = None if tools is None else _args(tools) + result = _add_to_group(name, selected) + return json.dumps({"published": name, "group": CLIENT_GROUP, "result": _redact(result)}, ensure_ascii=False) + + +@mcp.tool() +def mcphub_admin_unpublish_from_hermes(name: str) -> str: + """Remove one server from the central Hermes group without uninstalling or disabling the server itself.""" + name = _name(name) + group = urllib.parse.quote(CLIENT_GROUP, safe="") + result = _request("DELETE", f"/groups/{group}/servers/{urllib.parse.quote(name, safe='')}") + return json.dumps({"unpublished": name, "group": CLIENT_GROUP, "result": _redact(result)}, ensure_ascii=False) + + @mcp.tool() def mcphub_admin_remove(name: str, confirmation: str) -> str: """Permanently remove a server only after the user explicitly requested it. confirmation must exactly equal REMOVE:."""