feat(capabilities): add write router factory and MCP composition (HUB-WP-0002)

Add create_capability_request_write_router with host workflow callbacks,
CapabilityRequestReroute schema, HubCoreMCPServer.attach_to() with CORE_TOOL_NAMES
exclude filtering, tests, and mark HUB-WP-0002 finished.
This commit is contained in:
2026-06-22 19:52:22 +02:00
parent b1be2ad788
commit af28282861
8 changed files with 428 additions and 225 deletions

View File

@@ -8,6 +8,36 @@ from fastmcp import FastMCP
from hub_core.utils.routing import normalize_trailing_slash
CORE_TOOL_NAMES = frozenset({
"get_state_summary",
"list_domains",
"get_domain_summary",
"get_domain",
"send_message",
"get_messages",
"mark_message_read",
"reply_to_message",
"register_capability",
"list_capabilities",
"request_capability",
"accept_capability_request",
"update_capability_request_status",
"list_capability_requests",
"get_capability_request",
"register_repo",
"update_repo_path",
"list_domain_repos",
"check_repo_doi",
"get_doi_summary",
"register_service",
"list_services",
"ingest_tpsc_tool",
"get_gdpr_report",
"get_risks",
"get_alerts",
"append_progress",
})
class HubCoreMCPServer:
"""FastMCP base server for generic FOS hub tools.
@@ -32,24 +62,46 @@ class HubCoreMCPServer:
if register_tools:
self.register_core_tools()
def register_core_tools(self) -> None:
@self.mcp.tool()
def attach_to(
self,
host_mcp: FastMCP,
*,
exclude: frozenset[str] | None = None,
) -> HubCoreMCPServer:
"""Register generic hub-core tools on an existing host MCP server."""
self.mcp = host_mcp
self.register_core_tools(exclude=exclude)
return self
def _register_tool(self, name: str, excluded: frozenset[str]):
def decorator(fn):
if name not in excluded:
self.mcp.tool()(fn)
return fn
return decorator
def register_core_tools(self, *, exclude: frozenset[str] | None = None) -> None:
excluded = exclude or frozenset()
register = lambda name: self._register_tool(name, excluded) # noqa: E731
@register("get_state_summary")
def get_state_summary() -> str:
return self._json(self._get("/state/summary/"))
@self.mcp.tool()
@register("list_domains")
def list_domains(status: str | None = None) -> str:
return self._json(self._get("/domains/", {"status": status}))
@self.mcp.tool()
@register("get_domain_summary")
def get_domain_summary(domain_slug: str) -> str:
return self._json(self._get(f"/domains/{domain_slug}/"))
@self.mcp.tool()
@register("get_domain")
def get_domain(domain_slug: str) -> str:
return self._json(self._get(f"/domains/{domain_slug}/"))
@self.mcp.tool()
@register("send_message")
def send_message(
from_agent: str,
to_agent: str,
@@ -70,7 +122,7 @@ class HubCoreMCPServer:
)
)
@self.mcp.tool()
@register("get_messages")
def get_messages(
to_agent: str | None = None,
from_agent: str | None = None,
@@ -89,11 +141,11 @@ class HubCoreMCPServer:
)
)
@self.mcp.tool()
@register("mark_message_read")
def mark_message_read(message_id: str) -> str:
return self._json(self._patch(f"/messages/{message_id}/read/", {}))
@self.mcp.tool()
@register("reply_to_message")
def reply_to_message(message_id: str, from_agent: str, body: str) -> str:
return self._json(
self._post(
@@ -102,7 +154,7 @@ class HubCoreMCPServer:
)
)
@self.mcp.tool()
@register("register_capability")
def register_capability(
domain: str,
capability_type: str,
@@ -125,7 +177,7 @@ class HubCoreMCPServer:
)
)
@self.mcp.tool()
@register("list_capabilities")
def list_capabilities(
domain: str | None = None,
capability_type: str | None = None,
@@ -138,7 +190,7 @@ class HubCoreMCPServer:
)
)
@self.mcp.tool()
@register("request_capability")
def request_capability(
title: str,
capability_type: str,
@@ -165,7 +217,7 @@ class HubCoreMCPServer:
)
)
@self.mcp.tool()
@register("accept_capability_request")
def accept_capability_request(
request_id: str,
fulfilling_agent: str,
@@ -181,7 +233,7 @@ class HubCoreMCPServer:
)
)
@self.mcp.tool()
@register("update_capability_request_status")
def update_capability_request_status(
request_id: str,
status: str,
@@ -194,7 +246,7 @@ class HubCoreMCPServer:
)
)
@self.mcp.tool()
@register("list_capability_requests")
def list_capability_requests(
domain: str | None = None,
status: str | None = None,
@@ -207,11 +259,11 @@ class HubCoreMCPServer:
)
)
@self.mcp.tool()
@register("get_capability_request")
def get_capability_request(request_id: str) -> str:
return self._json(self._get(f"/capability-requests/{request_id}/"))
@self.mcp.tool()
@register("register_repo")
def register_repo(
domain_slug: str,
slug: str,
@@ -236,17 +288,17 @@ class HubCoreMCPServer:
)
)
@self.mcp.tool()
@register("update_repo_path")
def update_repo_path(repo_slug: str, host: str, path: str) -> str:
return self._json(
self._post(f"/repos/{repo_slug}/paths/", {"host": host, "path": path})
)
@self.mcp.tool()
@register("list_domain_repos")
def list_domain_repos(domain_slug: str) -> str:
return self._json(self._get("/repos/", {"domain": domain_slug}))
@self.mcp.tool()
@register("check_repo_doi")
def check_repo_doi(repo_slug: str, force_refresh: bool = False) -> str:
return self._json(
self._get(
@@ -255,11 +307,11 @@ class HubCoreMCPServer:
)
)
@self.mcp.tool()
@register("get_doi_summary")
def get_doi_summary() -> str:
return self._json(self._get("/repos/doi/summary/"))
@self.mcp.tool()
@register("register_service")
def register_service(
slug: str,
name: str,
@@ -284,7 +336,7 @@ class HubCoreMCPServer:
)
)
@self.mcp.tool()
@register("list_services")
def list_services(
gdpr_maturity: str | None = None,
category: str | None = None,
@@ -301,7 +353,7 @@ class HubCoreMCPServer:
)
)
@self.mcp.tool()
@register("ingest_tpsc_tool")
def ingest_tpsc_tool(repo_slug: str, source_file: str, entries: list[dict[str, Any]]) -> str:
return self._json(
self._post(
@@ -310,11 +362,11 @@ class HubCoreMCPServer:
)
)
@self.mcp.tool()
@register("get_gdpr_report")
def get_gdpr_report() -> str:
return self._json(self._get("/tpsc/report/gdpr/"))
@self.mcp.tool()
@register("get_risks")
def get_risks(
since: str | None = None,
limit: int = 100,
@@ -327,7 +379,7 @@ class HubCoreMCPServer:
)
)
@self.mcp.tool()
@register("get_alerts")
def get_alerts(
since: str | None = None,
limit: int = 100,
@@ -340,7 +392,7 @@ class HubCoreMCPServer:
)
)
@self.mcp.tool()
@register("append_progress")
def append_progress(
event_type: str,
summary: str,