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
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
+3 -2
View File
@@ -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
}
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<Container version="2">
<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>
<Network>bridge</Network>
<MyIP/>
+1 -1
View File
@@ -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"
+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
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.
+47 -1
View File
@@ -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:<name>."""