mcp server done

This commit is contained in:
Danilo Reyes
2026-02-01 10:36:54 -06:00
parent ecf058aacf
commit 8946ade5e8
15 changed files with 214 additions and 190 deletions

View File

@@ -2,14 +2,12 @@
from __future__ import annotations
import json
import os
import sys
from collections.abc import Mapping
from typing import Any
from mcp_server.docs_sync import check_catalog_parity
from mcp_server.tools import invoke_tool, list_tools_payload
from mcp.server.fastmcp import FastMCP
from mcp_server import tools
def _is_local_only() -> bool:
@@ -20,73 +18,53 @@ def _guard_local() -> None:
if not _is_local_only():
return
if os.environ.get("SSH_CONNECTION"):
_write_response({"error": {"code": -32099, "message": "Remote access denied"}})
sys.stderr.write("Remote access denied. Unset SSH_CONNECTION or set MCP_ALLOW_REMOTE.\n")
sys.exit(1)
def _write_response(payload: dict[str, Any]) -> None:
sys.stdout.write(json.dumps(payload) + "\n")
sys.stdout.flush()
mcp = FastMCP("NixOS Repo MCP", json_response=True)
def handle_request(request: Mapping[str, Any]) -> dict[str, Any]:
"""Dispatch a JSON-RPC request to the appropriate MCP handler."""
method = request.get("method")
params = request.get("params") or {}
if method == "listTools":
return {"result": list_tools_payload()}
if method == "invokeTool":
name = params.get("name") or ""
args = params.get("args") or {}
try:
return {"result": invoke_tool(name, args)}
except Exception as exc: # noqa: BLE001
return {
"result": {
"status": "failed",
"output": f"Service unavailable: {exc}",
"actions": ["retry locally", "call listTools to verify availability"],
"docsAnchor": {},
}
}
if method == "syncDocs":
return {"result": check_catalog_parity()}
return {
"error": {
"code": -32601,
"message": (
f"Method '{method}' not found. Call listTools to discover supported methods."
),
}
}
@mcp.tool(name="show-constitution")
def show_constitution() -> dict[str, object]:
"""Display repository AI constitution for rule lookup."""
return tools.invoke_tool("show-constitution", {})
@mcp.tool(name="list-playbooks")
def list_playbooks() -> dict[str, object]:
"""List available playbooks under docs/playbooks."""
return tools.invoke_tool("list-playbooks", {})
@mcp.tool(name="show-reference")
def show_reference() -> dict[str, object]:
"""Show docs/reference/index.md for navigation guidance."""
return tools.invoke_tool("show-reference", {})
@mcp.tool(name="search-docs")
def search_docs(query: str) -> dict[str, object]:
"""Search docs for a query string."""
return tools.invoke_tool("search-docs", {"query": query})
@mcp.tool(name="list-mcp-tasks")
def list_mcp_tasks() -> dict[str, object]:
"""Show MCP feature task list from specs."""
return tools.invoke_tool("list-mcp-tasks", {})
@mcp.tool(name="sync-docs")
def sync_docs() -> dict[str, object]:
"""Compare tool catalog against documented anchors."""
return tools.invoke_tool("sync-docs", {})
def main() -> None:
"""Run the MCP server in stdio mode."""
_guard_local()
for line in sys.stdin:
if not line.strip():
continue
try:
request = json.loads(line)
except json.JSONDecodeError:
_write_response({"error": {"code": -32700, "message": "Parse error"}})
continue
try:
response = handle_request(request)
except Exception as exc: # noqa: BLE001
_write_response(
{
"result": {
"status": "failed",
"output": f"Service unavailable: {exc}",
"actions": ["retry locally", "call listTools to verify availability"],
"docsAnchor": {},
}
}
)
continue
_write_response(response)
mcp.run(transport="stdio")
if __name__ == "__main__":

View File

@@ -6,7 +6,20 @@ from collections.abc import Callable, Mapping
from dataclasses import dataclass
from pathlib import Path
RepoPath = Path(__file__).resolve().parents[3]
def _find_repo_root() -> Path:
# Prefer cwd so local dev runs from the repo root without packaging.
candidates = (Path.cwd(), Path(__file__).resolve())
for base in candidates:
for parent in (base, *base.parents):
if (parent / "docs" / "constitution.md").exists():
return parent
if (parent / ".git").exists():
return parent
return Path(__file__).resolve().parents[3]
RepoPath = _find_repo_root()
DocsPath = RepoPath / "docs"
@@ -50,7 +63,7 @@ class Tool:
description: str
inputs: tuple[InputParam, ...]
docs_anchor: DocsAnchor
handler: Callable[[Mapping[str, str]], tuple[str, str, list[str]]]
handler: Callable[[Mapping[str, str]], tuple[str, object, list[str]]]
def as_dict(self) -> dict[str, object]:
"""Serialize tool metadata for transport."""
@@ -92,33 +105,51 @@ def _search_docs(term: str) -> str:
return "\n".join(matches[:20])
def _tool_handlers() -> dict[str, Callable[[Mapping[str, str]], tuple[str, str, list[str]]]]:
def show_constitution(_: Mapping[str, str]) -> tuple[str, str, list[str]]:
text = _read_text(DocsPath / "constitution.md")
return ("ok", text or "Constitution not found.", [])
def show_constitution(_: Mapping[str, str]) -> tuple[str, str, list[str]]:
"""Return the AI constitution contents."""
text = _read_text(DocsPath / "constitution.md")
return ("ok", text or "Constitution not found.", [])
def list_playbooks(_: Mapping[str, str]) -> tuple[str, str, list[str]]:
return ("ok", _list_playbooks(), [])
def show_reference(_: Mapping[str, str]) -> tuple[str, str, list[str]]:
return ("ok", _list_reference_topics(), [])
def list_playbooks(_: Mapping[str, str]) -> tuple[str, str, list[str]]:
"""Return the list of available playbooks."""
return ("ok", _list_playbooks(), [])
def search_docs(params: Mapping[str, str]) -> tuple[str, str, list[str]]:
term = params.get("query", "")
if not term:
return ("invalid_input", "Missing query", [])
return ("ok", _search_docs(term), [])
def list_tasks(_: Mapping[str, str]) -> tuple[str, str, list[str]]:
tasks_file = RepoPath / "specs" / "001-mcp-server" / "tasks.md"
return ("ok", _read_text(tasks_file) or "Tasks not found.", [])
def show_reference(_: Mapping[str, str]) -> tuple[str, str, list[str]]:
"""Return the reference index contents."""
return ("ok", _list_reference_topics(), [])
def search_docs(params: Mapping[str, str]) -> tuple[str, str, list[str]]:
"""Search docs for a query string and return matches."""
term = params.get("query", "")
if not term:
return ("invalid_input", "Missing query", [])
return ("ok", _search_docs(term), [])
def list_tasks(_: Mapping[str, str]) -> tuple[str, str, list[str]]:
"""Return MCP task list contents."""
tasks_file = RepoPath / "specs" / "001-mcp-server" / "tasks.md"
return ("ok", _read_text(tasks_file) or "Tasks not found.", [])
def sync_docs(_: Mapping[str, str]) -> tuple[str, object, list[str]]:
"""Return catalog vs docs drift report."""
from mcp_server.docs_sync import check_catalog_parity
return ("ok", check_catalog_parity(), [])
def _tool_handlers() -> dict[str, Callable[[Mapping[str, str]], tuple[str, object, list[str]]]]:
return {
"show-constitution": show_constitution,
"list-playbooks": list_playbooks,
"show-reference": show_reference,
"search-docs": search_docs,
"list-mcp-tasks": list_tasks,
"sync-docs": sync_docs,
}
@@ -150,6 +181,11 @@ def tool_catalog() -> tuple[Tool, ...]:
anchor="tasks",
summary="Implementation tasks for MCP feature",
)
anchor_sync = DocsAnchor(
path=DocsPath / "reference" / "mcp-server.md",
anchor="sync-docs",
summary="Compare tool catalog against documented anchors",
)
return (
Tool(
name="show-constitution",
@@ -186,11 +222,18 @@ def tool_catalog() -> tuple[Tool, ...]:
docs_anchor=anchor_tasks,
handler=handlers["list-mcp-tasks"],
),
Tool(
name="sync-docs",
description="Compare tool catalog against docs reference anchors.",
inputs=(),
docs_anchor=anchor_sync,
handler=handlers["sync-docs"],
),
)
def list_tools_payload() -> dict[str, object]:
"""Render tool catalog payload for listTools."""
"""Render tool catalog payload for tool discovery."""
return {"tools": [tool.as_dict() for tool in tool_catalog()]}
@@ -202,7 +245,7 @@ def invoke_tool(name: str, args: Mapping[str, str]) -> dict[str, object]:
return {
"status": "unsupported",
"output": f"Tool '{name}' is not available.",
"actions": ["call listTools to see supported tools"],
"actions": ["call tools/list to see supported tools"],
"docsAnchor": {},
}
status, output, actions = tool.handler(args)