ADR 0008 — MCP server as the interactive interface layer
Context
The pipeline was CLI-only: python -m ingestion.cli <cmd>. That works for
cron and one-off terminal use, but the daily human loop — review the
shortlist, dig into one role, draft, record a verdict — is conversational
by nature, and Kelyn lives in MCP clients (Claude Code, Cowork). Roadmap
item 1.3 also needed a delivery surface beyond an HTML file on disk.
Options considered: a web dashboard (Streamlit — Tier 6, deferred), a Slack bot, or an MCP server exposing the existing runner functions.
Decision
Add mcp_server/ — an MCP server on the official mcp Python SDK (2.x,
MCPServer), stdio transport, exposing tools that cover every CLI verb:
ingest, sweep, discover, qualify, digest, shortlist, get_job,
pipeline_stats, next_actions, funnel_stats, draft, record_verdict,
set_status, add_stage, record_outcome, remember, recall.
Key structural choices:
- The server is a thin adapter. Tools call the same functions the CLI
calls (
orchestrator.run,qualify_pending,draft_for_job,Storagemethods). No business logic lives inmcp_server/— if a tool needs a query, the query goes onStorage(that’s whyStorage.get_jobwas promoted out of the CLI). - Process-wide Storage singleton, lazily connected. The in-memory
fallback only works if state survives across tool calls; a per-call
connect would silently discard everything between calls. Tests inject
via
use_storage(). - Seams for LLM and resume.
_make_qualifierand_load_resumeare module-level functions so tests (and embedders) can swap in fakes without touching env vars ordata/resume.md. - Expected failures raise
ToolError. The 2.x SDK masks arbitrary exceptions with a generic message;ToolErrorpasses the message through. Bad job id, invalid verdict/status/outcome, bad digest format are user-input errors the client should see verbatim. - SDK pinned
mcp>=2. We build against the 2.x API (mcp.server.mcpserver.MCPServer); a 1.x resolution would break at import (FastMCPrename).
Consequences
- Positive: the daily loop is drivable from any MCP client; the
project’s own
.mcp.jsonauto-wires it into Claude Code sessions. - Positive:
server.call_toolgives tests the full validation + execution + serialization path with no transport, so the MCP surface is cheap to cover deterministically. - Negative:
qualifyanddraftare billed operations now reachable by an LLM client. Mitigation: tool docstrings say “billed” explicitly and defaults are conservative (limit=20,agent_mode=False). - Negative: one more surface to keep in sync with the CLI. Mitigation:
both are thin over the same runners; new verbs should land in
Storage/ runners first, then both surfaces. - Neutral: stdio only for now. The SDK supports streamable-http if a remote/hosted mode is ever needed.