publish MCPs through a filtered Hermes group

This commit is contained in:
Mikei386
2026-08-26 11:34:15 +02:00
parent 2595bab17b
commit 167448d778
6 changed files with 54 additions and 7 deletions
+1 -1
View File
@@ -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 sind der produktive Zustand. Oberfläche und offizielle API ändern genau
diese Datei; Container-Updates überschreiben sie nicht. diese Datei; Container-Updates überschreiben sie nicht.
`config/mcp-registry.json` ist nur der Neuinstallations-Seed. `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 aktivierte Server erscheinen dadurch nach **MCP neu laden**, ohne dass pro
MCP eine weitere Hermes-Konfiguration geschrieben werden muss. MCP eine weitere Hermes-Konfiguration geschrieben werden muss.
- Modelle und Athena-Backups liegen auf `/data`. Hermes liegt vollständig unter - Modelle und Athena-Backups liegen auf `/data`. Hermes liegt vollständig unter
+3 -2
View File
@@ -6,7 +6,7 @@
"hermes_id": "mcphub", "hermes_id": "mcphub",
"name": "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.", "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"], "clients": ["hermes"],
"env_file": "/etc/mike-ai/mcphub-client.env", "env_file": "/etc/mike-ai/mcphub-client.env",
"key_env": "MCPHUB_BEARER_TOKEN", "key_env": "MCPHUB_BEARER_TOKEN",
@@ -26,7 +26,8 @@
"args": ["/opt/casaderoll/mcps/mcphub_admin_mcp.py"], "args": ["/opt/casaderoll/mcps/mcphub_admin_mcp.py"],
"env": { "env": {
"MCPHUB_API_URL": "http://127.0.0.1:3000/api", "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 "enabled": true
} }
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<Container version="2"> <Container version="2">
<Name>MCPHub</Name> <Name>MCPHub</Name>
<Repository>casaderoll/mcphub:1.2.2</Repository> <Repository>casaderoll/mcphub:1.2.3</Repository>
<Registry>https://hub.docker.com/r/samanhappy/mcphub</Registry> <Registry>https://hub.docker.com/r/samanhappy/mcphub</Registry>
<Network>bridge</Network> <Network>bridge</Network>
<MyIP/> <MyIP/>
+1 -1
View File
@@ -95,7 +95,7 @@ docker run --rm --entrypoint python \
-v "$HERMES_DATA_DIR:/hermes:rw" \ -v "$HERMES_DATA_DIR:/hermes:rw" \
-v "$mcphub_registry:/run/input/mcp-registry.json:ro" \ -v "$mcphub_registry:/run/input/mcp-registry.json:ro" \
"${token_mount[@]}" \ "${token_mount[@]}" \
casaderoll/mcphub:1.2.2 \ casaderoll/mcphub:1.2.3 \
/stack/platform/mcp/sync-clients.py "${sync_args[@]}" /stack/platform/mcp/sync-clients.py "${sync_args[@]}"
"$STACK_DIR/platform/hermes/install-skills.sh" "$STACK_DIR/platform/hermes/install-skills.sh"
+1 -1
View File
@@ -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 er die Aktivierung technisch. Ein Neustart des MCPHub-Containers ist nicht
erforderlich. 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 neu aktivierte Server nach **MCP neu laden** sichtbar, ohne pro MCP eine weitere
Hermes-Konfiguration zu erzeugen. Der interne Server `mcphub-admin` stellt die Hermes-Konfiguration zu erzeugen. Der interne Server `mcphub-admin` stellt die
üblichen Installationswege für HTTP-, npm- und Python-MCPs als Werkzeuge bereit. üblichen Installationswege für HTTP-, npm- und Python-MCPs als Werkzeuge bereit.
+47 -1
View File
@@ -13,6 +13,7 @@ import os
import pathlib import pathlib
import re import re
import urllib.error import urllib.error
import urllib.parse
import urllib.request import urllib.request
from typing import Any 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("/") 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")) 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}$") 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._+~-]+)?$") 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._+~-]+)?$") 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): 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") raise HubError(f"Server already exists: {name}; inspect or update it instead")
result = _request("POST", "/servers", {"name": name, "config": config}) result = _request("POST", "/servers", {"name": name, "config": config})
group_result = _add_to_group(name)
verified = _request("GET", f"/servers/{name}") verified = _request("GET", f"/servers/{name}")
data = verified.get("data", verified) if isinstance(verified, dict) else verified 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() @mcp.tool()
@@ -197,6 +225,24 @@ def mcphub_admin_reload(name: str) -> str:
return json.dumps(_redact(result), ensure_ascii=False) 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() @mcp.tool()
def mcphub_admin_remove(name: str, confirmation: str) -> str: 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:<name>.""" """Permanently remove a server only after the user explicitly requested it. confirmation must exactly equal REMOVE:<name>."""