generated from coulomb/repo-seed
refactor(hub-core): mount capability write router and compose MCP tools
Use create_capability_request_write_router with dev-hub callbacks and attach generic HubCoreMCPServer tools while keeping enriched local overrides.
This commit is contained in:
@@ -17,6 +17,7 @@ from uuid import UUID
|
||||
|
||||
import httpx
|
||||
from fastmcp import FastMCP
|
||||
from hub_core.mcp import HubCoreMCPServer
|
||||
|
||||
API_BASE = os.environ.get("API_BASE", "http://127.0.0.1:8000").rstrip("/")
|
||||
|
||||
@@ -31,6 +32,24 @@ mcp = FastMCP(
|
||||
),
|
||||
)
|
||||
|
||||
# Generic hub tools from hub-core; exclude dev-hub overrides with richer contracts.
|
||||
_HUB_CORE_MCP_EXCLUDE = frozenset({
|
||||
"get_state_summary",
|
||||
"get_domain_summary",
|
||||
"list_domains",
|
||||
"list_domain_repos",
|
||||
"register_repo",
|
||||
"update_repo_path",
|
||||
"request_capability",
|
||||
"register_service",
|
||||
"ingest_tpsc_tool",
|
||||
})
|
||||
HubCoreMCPServer(
|
||||
name="state-hub",
|
||||
api_base=API_BASE,
|
||||
register_tools=False,
|
||||
).attach_to(mcp, exclude=_HUB_CORE_MCP_EXCLUDE)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HTTP helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -2224,124 +2243,10 @@ def get_repo_dispatch(repo_slug: str) -> str:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Agent Inbox (inter-agent message passing)
|
||||
# Capability Catalog & Requests (dev-hub extensions)
|
||||
# Messaging and catalog CRUD/list tools come from hub_core.mcp.HubCoreMCPServer.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@mcp.tool()
|
||||
def send_message(from_agent: str, to_agent: str, subject: str, body: str, thread_id: str | None = None) -> str:
|
||||
"""Send a message from one agent to another (or 'broadcast' for all).
|
||||
|
||||
Use this to coordinate with other Claude instances — e.g. a worker agent
|
||||
reporting status back to the orchestrator, or the hub agent dispatching
|
||||
instructions to a domain agent.
|
||||
|
||||
Args:
|
||||
from_agent: Sender identifier (e.g. 'hub', 'marki-docx', 'railiance')
|
||||
to_agent: Recipient identifier or 'broadcast' for all agents
|
||||
subject: Short subject line (max 500 chars)
|
||||
body: Full message body (markdown supported)
|
||||
thread_id: UUID of the root message to create a thread (optional)
|
||||
"""
|
||||
payload: dict = {"from_agent": from_agent, "to_agent": to_agent, "subject": subject, "body": body}
|
||||
if thread_id:
|
||||
payload["thread_id"] = thread_id
|
||||
msg = _post("/messages/", payload)
|
||||
return json.dumps(msg, indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_messages(to_agent: str | None = None, from_agent: str | None = None, unread_only: bool = False, limit: int = 20) -> str:
|
||||
"""List messages in the agent inbox.
|
||||
|
||||
Call this at session start to check for pending coordination messages.
|
||||
|
||||
Args:
|
||||
to_agent: Filter by recipient (your agent name, or omit for all)
|
||||
from_agent: Filter by sender (optional)
|
||||
unread_only: Return only unread messages (default: False)
|
||||
limit: Maximum number of messages to return (default: 20)
|
||||
"""
|
||||
params: dict = {"limit": limit, "unread_only": unread_only}
|
||||
if to_agent:
|
||||
params["to_agent"] = to_agent
|
||||
if from_agent:
|
||||
params["from_agent"] = from_agent
|
||||
return json.dumps(_get("/messages/", params), indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def mark_message_read(message_id: str) -> str:
|
||||
"""Mark an inbox message as read.
|
||||
|
||||
Args:
|
||||
message_id: UUID of the message to mark as read
|
||||
"""
|
||||
return json.dumps(_patch(f"/messages/{message_id}/read", {}), indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def reply_to_message(message_id: str, from_agent: str, body: str) -> str:
|
||||
"""Reply to a message. Marks the original as read and creates a reply in the same thread.
|
||||
|
||||
Args:
|
||||
message_id: UUID of the message to reply to
|
||||
from_agent: Your agent identifier
|
||||
body: Reply body (markdown supported)
|
||||
"""
|
||||
return json.dumps(_post(f"/messages/{message_id}/reply", {"from_agent": from_agent, "body": body}), indent=2)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Capability Catalog & Requests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@mcp.tool()
|
||||
def register_capability(
|
||||
domain: str,
|
||||
capability_type: str,
|
||||
title: str,
|
||||
description: str | None = None,
|
||||
keywords: list[str] | None = None,
|
||||
repo_slug: str | None = None,
|
||||
) -> str:
|
||||
"""Register a capability that a domain can provide. Used for routing requests.
|
||||
|
||||
Args:
|
||||
domain: Domain slug (e.g. 'railiance', 'markitect')
|
||||
capability_type: Category (e.g. 'infrastructure', 'api', 'data', 'security', 'documentation')
|
||||
title: Short title for this capability
|
||||
description: Longer description (optional)
|
||||
keywords: List of keywords for routing (e.g. ['cluster', 'k8s', 'privacy'])
|
||||
repo_slug: Optional repo slug to attribute this capability to a specific repo
|
||||
"""
|
||||
entry = _post("/capability-catalog", {
|
||||
"domain": domain,
|
||||
"capability_type": capability_type,
|
||||
"title": title,
|
||||
"description": description,
|
||||
"keywords": keywords or [],
|
||||
"repo_slug": repo_slug,
|
||||
})
|
||||
return json.dumps(entry, indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_capabilities(
|
||||
domain: str | None = None,
|
||||
capability_type: str | None = None,
|
||||
) -> str:
|
||||
"""Browse the capability catalog — what domains can provide.
|
||||
|
||||
Args:
|
||||
domain: Filter by domain slug (optional)
|
||||
capability_type: Filter by type (optional)
|
||||
"""
|
||||
return json.dumps(_get("/capability-catalog", {
|
||||
"domain": domain,
|
||||
"capability_type": capability_type,
|
||||
}), indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_capability_profile(domain_slug: str | None = None) -> str:
|
||||
"""Full capability registry: domain → repos (with description) → capabilities.
|
||||
@@ -2470,48 +2375,6 @@ def request_capability(
|
||||
return json.dumps(req, indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def accept_capability_request(
|
||||
request_id: str,
|
||||
fulfilling_agent: str,
|
||||
fulfilling_workstream_id: str | None = None,
|
||||
) -> str:
|
||||
"""Accept a capability request. Assigns yourself as the fulfilling agent.
|
||||
|
||||
Args:
|
||||
request_id: UUID of the capability request
|
||||
fulfilling_agent: Your agent identifier (e.g. 'railiance-worker')
|
||||
fulfilling_workstream_id: UUID of your workstream for this work (optional)
|
||||
"""
|
||||
result = _post(f"/capability-requests/{request_id}/accept", {
|
||||
"fulfilling_agent": fulfilling_agent,
|
||||
"fulfilling_workstream_id": fulfilling_workstream_id,
|
||||
})
|
||||
return json.dumps(result, indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def update_capability_request_status(
|
||||
request_id: str,
|
||||
status: str,
|
||||
note: str | None = None,
|
||||
) -> str:
|
||||
"""Advance a capability request through its lifecycle.
|
||||
|
||||
On 'completed': auto-unblocks the blocking task if one was set.
|
||||
|
||||
Args:
|
||||
request_id: UUID of the capability request
|
||||
status: in_progress | ready_for_review | completed | rejected | withdrawn
|
||||
note: Optional note (required for rejection, recommended for completion)
|
||||
"""
|
||||
result = _patch(f"/capability-requests/{request_id}/status", {
|
||||
"status": status,
|
||||
"note": note,
|
||||
})
|
||||
return json.dumps(result, indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def patch_capability_request(
|
||||
request_id: str,
|
||||
@@ -2552,36 +2415,6 @@ def patch_capability_request(
|
||||
return json.dumps(_patch(f"/capability-requests/{request_id}", body), indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_capability_requests(
|
||||
domain: str | None = None,
|
||||
status: str | None = None,
|
||||
capability_type: str | None = None,
|
||||
) -> str:
|
||||
"""List capability requests with optional filters.
|
||||
|
||||
Args:
|
||||
domain: Filter by requesting OR fulfilling domain slug
|
||||
status: Filter by status (requested/accepted/in_progress/ready_for_review/completed/rejected/withdrawn)
|
||||
capability_type: Filter by capability type
|
||||
"""
|
||||
return json.dumps(_get("/capability-requests", {
|
||||
"domain": domain,
|
||||
"status": status,
|
||||
"capability_type": capability_type,
|
||||
}), indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_capability_request(request_id: str) -> str:
|
||||
"""Get a single capability request by ID.
|
||||
|
||||
Args:
|
||||
request_id: UUID of the capability request
|
||||
"""
|
||||
return json.dumps(_get(f"/capability-requests/{request_id}"), indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def dispute_capability_routing(
|
||||
request_id: str,
|
||||
@@ -2688,30 +2521,6 @@ def register_service(
|
||||
}), indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def list_services(
|
||||
gdpr_maturity: str | None = None,
|
||||
category: str | None = None,
|
||||
pricing_model: str | None = None,
|
||||
) -> str:
|
||||
"""Browse the Third-Party Services Catalog (TPSC).
|
||||
|
||||
Returns services with their GDPR maturity level and gdpr_warning flag
|
||||
(True when maturity is unknown, non_compliant, or initial — may limit
|
||||
use in corporate/GDPR-regulated environments).
|
||||
|
||||
Args:
|
||||
gdpr_maturity: Filter by maturity level (unknown/non_compliant/initial/developing/defined/managed/certified)
|
||||
category: Filter by category (e.g. 'llm_inference', 'storage')
|
||||
pricing_model: Filter by pricing model (free/paid/freemium/usage_based/unknown)
|
||||
"""
|
||||
return json.dumps(_get("/tpsc/catalog", {
|
||||
"gdpr_maturity": gdpr_maturity,
|
||||
"category": category,
|
||||
"pricing_model": pricing_model,
|
||||
}), indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def ingest_tpsc_tool(repo_slug: str) -> str:
|
||||
"""Ingest tpsc.yaml service dependency declarations for a repo.
|
||||
@@ -2750,53 +2559,6 @@ def ingest_tpsc_tool(repo_slug: str) -> str:
|
||||
return output.strip()
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_gdpr_report() -> str:
|
||||
"""Get an aggregated GDPR compliance report across all repos' latest TPSC snapshots.
|
||||
|
||||
Returns a warning summary for services with gdpr_maturity in:
|
||||
unknown | non_compliant | initial
|
||||
|
||||
These may limit usability in GDPR-regulated / corporate environments.
|
||||
Services at 'developing' or above have at least a DPA available.
|
||||
"""
|
||||
return json.dumps(_get("/tpsc/report/gdpr"), indent=2)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Repository Definition of Integrated (DoI)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@mcp.tool()
|
||||
def check_repo_doi(repo_slug: str) -> str:
|
||||
"""Evaluate the 14 DoI criteria for a repo and return a full report.
|
||||
|
||||
Criteria are grouped into three tiers:
|
||||
Core (C1–C4): registered, domain, path, remote URL
|
||||
Standard (C5–C9): SCOPE.md, CLAUDE.md, workplan, SBOM, TPSC
|
||||
Full (C10–C14): repo goal, capabilities, agents, clean consistency, host paths
|
||||
|
||||
Status values: pass | fail | warn | skip
|
||||
|
||||
The 'tier' field shows the highest tier where ALL criteria pass or warn:
|
||||
none | core | standard | full
|
||||
|
||||
Args:
|
||||
repo_slug: Registered repo slug (e.g. 'llm-connect', 'the-custodian')
|
||||
"""
|
||||
return json.dumps(_get(f"/repos/{repo_slug}/doi"), indent=2)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_doi_summary() -> str:
|
||||
"""Return DoI tier for all active repos, sorted worst-first.
|
||||
|
||||
Useful at session start to spot repos that need integration work.
|
||||
Tiers: none (red) → core → standard → full (green).
|
||||
"""
|
||||
return json.dumps(_get("/repos/doi/summary"), indent=2)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Interactive / ad-hoc task recording
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user