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.
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,
DOClabel, "N candidates dropped", last index time per repo. read_codereads only indexed files. The path must be in the manifest — so the agent cannot readnode_modulesor.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 flaggedstale.
| Gotcha | Fix |
|---|---|
| mounted sub-app lifespans never run | session_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 body | a 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.