feat(mcp): FastMCP server exposing read-only KB tools (stdio)

Implements F-14: thin FastMCP wrapper over existing codex domain modules.
Seven read-only tools: search, ask, wiki_read, wiki_list, discover_leads,
provenance_verify, synthesis_browse. All optional-feature tools degrade
gracefully to {"error": "feature not available"} instead of crashing.
Adds mcp[cli]>=1.0 dependency and codex-mcp console_script entry-point.
28 new tests across test_server, test_search, test_readonly, test_graceful,
test_http_auth — all green; 0 regressions in existing 172 tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-06-14 08:23:22 +02:00
parent 59763105a5
commit b803811701
10 changed files with 1484 additions and 0 deletions

49
tests/mcp/test_server.py Normal file
View File

@@ -0,0 +1,49 @@
"""test_server.py — verify the MCP server instantiates and all tools are registered."""
from __future__ import annotations
import asyncio
# ---------------------------------------------------------------------------
# Expected tools (must match EXACTLY what mcp_server.py registers)
# ---------------------------------------------------------------------------
EXPECTED_TOOLS = {
"search",
"ask",
"wiki_read",
"wiki_list",
"discover_leads",
"provenance_verify",
"synthesis_browse",
}
def test_server_instance_exists() -> None:
"""The module-level ``mcp`` FastMCP instance must exist."""
from codex.mcp_server import mcp
assert mcp is not None
assert mcp.name == "codex"
def test_all_tools_registered() -> None:
"""Every expected tool name must be registered on the server."""
from codex.mcp_server import mcp
tools = asyncio.run(mcp.list_tools())
registered = {t.name for t in tools}
missing = EXPECTED_TOOLS - registered
assert not missing, f"Tools not registered: {missing}"
def test_no_extra_unexpected_write_tools() -> None:
"""No tool outside the expected set should be registered (belt-and-suspenders)."""
from codex.mcp_server import mcp
tools = asyncio.run(mcp.list_tools())
registered = {t.name for t in tools}
unexpected = registered - EXPECTED_TOOLS
assert not unexpected, f"Unexpected tools registered: {unexpected}"