"""Small State Hub MCP surface for Codex repository sessions. The full dev-hub MCP server intentionally exposes administrative and catalog operations. Codex repository work needs a much smaller coordination surface; keeping it separate reduces tool discovery cost and makes the contract clear. """ from __future__ import annotations import json import os from typing import Any import httpx from fastmcp import FastMCP from mcp_server.server import get_domain_summary as _full_get_domain_summary API_BASE = os.environ.get("API_BASE", "http://127.0.0.1:8000").rstrip("/") mcp = FastMCP( name="dev-hub-codex", instructions=( "Slim State Hub coordination surface for Codex repository sessions. " "Start with get_domain_summary, check the repository inbox, and record " "progress when work closes. Workplan files remain the source of truth." ), ) def _request(method: str, path: str, body: dict[str, Any] | None = None) -> Any: with httpx.Client( base_url=API_BASE, timeout=15.0, follow_redirects=True, trust_env=False, ) as client: response = client.request(method, path, json=body) response.raise_for_status() return response.json() def _json(value: Any) -> str: return json.dumps(value, indent=2) @mcp.tool() def get_domain_summary(domain_slug: str) -> str: """Return actionable State Hub orientation scoped to one domain.""" return _full_get_domain_summary(domain_slug) @mcp.tool() def get_messages(to_agent: str, unread_only: bool = True) -> str: """Get coordination messages for one repository agent.""" suffix = "true" if unread_only else "false" return _json(_request("GET", f"/messages/?to_agent={to_agent}&unread_only={suffix}")) @mcp.tool() def mark_message_read(message_id: str) -> str: """Mark one coordination message as read.""" return _json(_request("PATCH", f"/messages/{message_id}/read", {})) @mcp.tool() def add_progress_event( summary: str, author: str = "codex", workplan_id: str | None = None, task_id: str | None = None, ) -> str: """Record a State Hub progress note for completed or significant work.""" body: dict[str, Any] = { "summary": summary, "event_type": "note", "author": author, } if workplan_id: body["workplan_id"] = workplan_id if task_id: body["task_id"] = task_id return _json(_request("POST", "/progress/", body)) @mcp.tool() def record_decision( title: str, description: str, topic_id: str, proposed_by: str = "codex", workplan_id: str | None = None, ) -> str: """Record a pending decision linked to a topic and optionally a workplan.""" body: dict[str, Any] = { "title": title, "description": description, "topic_id": topic_id, "decision_type": "pending", "status": "open", "proposed_by": proposed_by, } if workplan_id: body["workplan_id"] = workplan_id return _json(_request("POST", "/decisions/", body)) @mcp.tool() def update_task_status( task_id: str, status: str, blocking_reason: str | None = None, ) -> str: """Update a task to wait, todo, progress, done, or cancel.""" body: dict[str, Any] = {"status": status} if blocking_reason is not None: body["blocking_reason"] = blocking_reason return _json(_request("PATCH", f"/tasks/{task_id}", body)) if __name__ == "__main__": mcp.run(transport="stdio")