fix MCPHub group publishing and recovery
This commit is contained in:
@@ -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/>
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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", [])
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user