123 lines
3.4 KiB
Python
123 lines
3.4 KiB
Python
"""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")
|