note · evergreen

MCP in the same process

search_code, read_code, list_repos on one retriever; signals not filters; reads only indexed files.

The consumer is an agent — Claude Code, Cursor — so the retriever is exposed as three MCP tools mounted at /mcp inside the same FastAPI process.

tools
search_codequery, repo, path prefix, category
read_codeindexed file, line range
list_reposfreshness

Design choices

  • Same process, same retriever. No stdio sidecar loading the embedding model twice; blocking work (embedding, Milvus, disk) runs in threads so the MCP session's event loop keeps flowing.
  • Output is data, not instructions. Code returned to the agent is wrapped as untrusted content.
  • Signals travel with the hits. Weak-match band, DOC label, "N candidates dropped", last index time per repo.
  • read_code reads only indexed files. The path must be in the manifest — so the agent cannot read node_modules or .env, and cannot be told "that file exists" when it does not. If the file on disk no longer matches the indexed hash, the slice comes back flagged stale.
GotchaFix
mounted sub-app lifespans never runsession_manager.run() wraps the parent app's lifespan
transport rejects foreign Host headers with 421 (DNS rebinding)extra hosts via config
/mcp → /mcp/ is a 307; redirect-averse clients lose the POST bodya pure-ASGI middleware rewrites the path instead

The agent gets candidates and evidence, then reads. Retrieval that tries to be the last word is retrieval that hallucinates on the agent's behalf.

#mcp #agent #signals

See this note on the whiteboard →