fix MCPHub group publishing and recovery

This commit is contained in:
Mikei386 committed 2026-08-26 11:42:39 +02:00
1 parent 167448d778
commit 9ae7d22618
5 files changed
+67 -4

No files matched your search

+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.3</Repository> <Repository>casaderoll/mcphub:1.2.4</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/>
+13
View File
@@ -76,6 +76,19 @@ class MCPHubSettingsTests(unittest.TestCase):
with self.assertRaises(SystemExit): with self.assertRaises(SystemExit):
self.module.registry_servers(self.registry, self.secrets, {}) self.module.registry_servers(self.registry, self.secrets, {})
def test_hermes_group_is_recoverable_and_bounded(self):
settings = {"groups": [{"id": "keep", "name": "other", "servers": []}]}
self.module.ensure_hermes_group(settings)
self.module.ensure_hermes_group(settings)
groups = settings["groups"]
self.assertEqual(len([group for group in groups if group["name"] == "hermes"]), 1)
hermes = next(group for group in groups if group["name"] == "hermes")
fritzbox = next(item for item in hermes["servers"] if item["name"] == "fritzbox")
self.assertEqual(
fritzbox["tools"],
["list_services", "list_actions", "describe_action", "call_action"],
)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+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.3 \ casaderoll/mcphub:1.2.4 \
/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"
+37
View File
@@ -82,6 +82,42 @@ def registry_servers(registry: pathlib.Path, secrets_dir: pathlib.Path,
return result return result
def ensure_hermes_group(settings: dict[str, object]) -> None:
"""Seed the bounded Hermes aggregate route on fresh installs/recovery."""
server_names = [
"mcphub-admin", "athena-operator", "github", "arr", "navidrome",
"deemix", "unraid",
]
members: list[dict[str, object]] = [
{"name": name, "tools": "all", "prompts": "all", "resources": "all"}
for name in server_names
]
members.append({
"name": "fritzbox",
"tools": ["list_services", "list_actions", "describe_action", "call_action"],
"prompts": [],
"resources": [],
})
groups = settings.setdefault("groups", [])
if not isinstance(groups, list):
groups = []
settings["groups"] = groups
current = next(
(group for group in groups
if isinstance(group, dict) and group.get("name") == "hermes"),
None,
)
if current is None:
current = {
"id": str(uuid.uuid4()),
"owner": "system",
"name": "hermes",
}
groups.append(current)
current["description"] = "Gefilterte zentrale MCP-Auswahl für alle Hermes-Clients"
current["servers"] = members
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("settings", type=pathlib.Path) parser.add_argument("settings", type=pathlib.Path)
@@ -114,6 +150,7 @@ def main() -> None:
} }
settings["mcpServers"] = servers settings["mcpServers"] = servers
ensure_hermes_group(settings)
settings.setdefault("users", []) settings.setdefault("users", [])
token_path = args.settings.parent / "client-token" token_path = args.settings.parent / "client-token"
keys = settings.setdefault("bearerKeys", []) keys = settings.setdefault("bearerKeys", [])
+15 -2
View File
@@ -149,10 +149,23 @@ def _install(name: str, config: dict[str, Any]) -> str:
}, ensure_ascii=False) }, ensure_ascii=False)
def _group_id(name: str) -> str:
result = _request("GET", "/groups")
rows = result.get("data", []) if isinstance(result, dict) else []
match = next(
(row for row in rows
if isinstance(row, dict) and (row.get("name") == name or row.get("id") == name)),
None,
)
if not isinstance(match, dict) or not match.get("id"):
raise HubError(f"MCPHub group does not exist: {name}")
return str(match["id"])
def _add_to_group(name: str, tools: list[str] | None = None) -> Any: def _add_to_group(name: str, tools: list[str] | None = None) -> Any:
if not CLIENT_GROUP: if not CLIENT_GROUP:
return {"skipped": "no-client-group"} return {"skipped": "no-client-group"}
group = urllib.parse.quote(CLIENT_GROUP, safe="") group = urllib.parse.quote(_group_id(CLIENT_GROUP), safe="")
configs = _request("GET", f"/groups/{group}/server-configs") configs = _request("GET", f"/groups/{group}/server-configs")
rows = configs.get("data", []) if isinstance(configs, dict) else [] rows = configs.get("data", []) if isinstance(configs, dict) else []
present = any(isinstance(row, dict) and row.get("name") == name for row in rows) present = any(isinstance(row, dict) and row.get("name") == name for row in rows)
@@ -238,7 +251,7 @@ def mcphub_admin_publish_to_hermes(name: str, tools: list[str] | None = None) ->
def mcphub_admin_unpublish_from_hermes(name: str) -> str: def mcphub_admin_unpublish_from_hermes(name: str) -> str:
"""Remove one server from the central Hermes group without uninstalling or disabling the server itself.""" """Remove one server from the central Hermes group without uninstalling or disabling the server itself."""
name = _name(name) name = _name(name)
group = urllib.parse.quote(CLIENT_GROUP, safe="") group = urllib.parse.quote(_group_id(CLIENT_GROUP), safe="")
result = _request("DELETE", f"/groups/{group}/servers/{urllib.parse.quote(name, 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) return json.dumps({"unpublished": name, "group": CLIENT_GROUP, "result": _redact(result)}, ensure_ascii=False)