A warm, gRPC-backed Python symbol index for AI coding agents, built on pyrefly — Meta's Rust-based Python type checker and LSP server.
pyrefly-mcp wraps a single long-lived pyrefly lsp process in a tonic
gRPC server (oracle) and exposes it to any MCP-speaking agent (Claude
Code, Cursor, Codex, etc.) through a thin stdio MCP client (mcp) with
four tools: analyze_symbol, call_graph, find, and grep.
Agentic coding tools are good at reading files but bad at navigating
large Python codebases the way a human with "Go to Definition" and "Find
References" would. Grepping for a name gives you every string match, not
every real reference. A stdio-spawned LSP per query is slow (cold
type-check on every call). pyrefly-mcp keeps pyrefly warm in the
background and gives agents compiler-precise navigation as an MCP tool
call — no editor required.
┌─────────────┐ stdio (LSP JSON-RPC) ┌──────────────────┐
│ pyrefly lsp │ ◄──────────────────────► │ oracle (tonic) │
│ (subprocess)│ │ gRPC :50052 │
└─────────────┘ └────────┬─────────┘
│ gRPC
┌─────────▼─────────┐
│ mcp (rmcp) │
│ stdio MCP server │
└─────────┬──────────┘
│ MCP (stdio JSON-RPC)
┌─────────▼──────────┐
│ Agent / editor │
│ (Claude, Cursor...) │
└──────────────────────┘
oraclespawns onepyrefly lspsubprocess per workspace, drives it over stdio LSP, and re-exposes a subset of LSP as a streaming gRPC service (proto/Index.proto). It pre-warms the index on boot (a seeddidOpen+ optional warm-seed file list) so the first agent query isn't a cold-start type-check. It also ships aFindRPC backed byripgrepfor raw text/regex search, used both internally (to recover bound-method callers pyrefly's static analysis can miss) and exposed directly to clients.mcpis a stateless stdio MCP server. Each tool call opens a fresh gRPC connection to the oracle, translates the MCP arguments into one or more RPCs, and renders the streamed results as agent-readable text. It holds no index state of its own — restart it anytime without losing anything.
# 1. Build
cargo build --release --workspace
# 2. Install pyrefly (the actual Python type checker/LSP)
pip install pyrefly
# 3. Start the oracle against your Python workspace
PYREFLY_WORKSPACE_ROOT=/path/to/your/python/project \
./target/release/oracle
# gRPC listens on 0.0.0.0:50052 by default
# 4. Point an MCP client at the mcp binary (stdio transport), e.g. in
# Claude Code's mcp config:
# { "command": "/path/to/target/release/mcp",
# "env": { "PYREFLY_INDEX_ADDR": "http://127.0.0.1:50052",
# "PYREFLY_WORKSPACE_ROOT": "/path/to/your/python/project" } }A minimal example project lives in examples/sample-python/
— a Calculator/ScientificCalculator class hierarchy with tests, enough
to exercise all four tools end to end.
| Variable | Component | Default | Purpose |
|---|---|---|---|
GRPC_LISTEN |
oracle | 0.0.0.0:50052 |
gRPC listen address |
PYREFLY_WORKSPACE_ROOT |
oracle, mcp | . |
Python workspace root to index / display paths relative to |
PYREFLY_IMPORT_ROOTS |
oracle | (empty) | comma-separated extra import roots stripped from module paths, beyond the workspace root |
PYREFLY_BIN |
oracle | pyrefly |
path to the pyrefly binary |
PYREFLY_CONFIG |
oracle | (none) | path to a pyrefly.toml when it can't live at the workspace root |
PYREFLY_RG_BIN |
oracle | rg (resolved via PATH) |
ripgrep binary for the Find RPC |
PYREFLY_RUNTIME_STUBS |
oracle | (none) | extra typeshed-style stub root(s) |
PYREFLY_INDEX_ADDR |
mcp | http://127.0.0.1:50052 |
oracle gRPC endpoint |
PYREFLY_ORACLE_MAX_SESSIONS |
oracle | 8 |
concurrent editor-overlay sessions cap |
RUST_LOG |
both | info |
tracing filter |
| Tool | Purpose |
|---|---|
analyze_symbol |
Full dossier for a symbol: definition, pyrefly-resolved type/signature, references, class hierarchy, and a blast-radius risk classification (prod vs. test callers). |
call_graph |
Bidirectional call graph — who calls this, and what does it call — to a configurable depth. |
find |
Combined symbol-index + ripgrep search, merged with Reciprocal Rank Fusion (RRF) when both sources return hits. |
grep |
Raw regex/fixed-string search across the indexed workspace (ripgrep-backed, no symbol resolution). |
All four are read-only, idempotent, and safe to call speculatively — none of them mutate the workspace.
RRF fusion in find. Symbol-index hits (precise, compiler-verified)
and text-search hits (fuzzy, recall-oriented) answer different questions.
Concatenating them either buries good symbol hits under text noise or
loses fuzzy recall entirely. find runs both passes concurrently and, by
default, merges them with Reciprocal Rank
Fusion keyed on
symbol FQN (or path:line for text hits) — the two ranked lists are
combined without needing a shared score scale.
Tokenizer / kind-priority ranking. Bare-name queries ("Node") are
ambiguous across the module and class namespace. resolver.rs assigns a
kind priority depending on whether the query is dotted (Class.method
favors methods/functions) or bare (favors classes/functions over modules),
then breaks ties by reference count — the more-referenced symbol usually
is the one you meant.
LSP-first, ripgrep as a recovery net. Every navigation primitive
(Resolve, References, CallHierarchy*, Hover) is answered by
pyrefly's real type checker, not a text index — this is what makes results
compiler-precise instead of grep-precise. Ripgrep is used deliberately in
two narrow places: (1) as an internal recovery path when
CallHierarchyIncoming returns suspiciously few hits (pyrefly sometimes
under-resolves bound-method call sites), and (2) as the standalone Find/
grep tools for the cases an agent genuinely wants raw text search.
Why a gRPC oracle instead of spawning pyrefly per call. pyrefly lsp
does a real type-check pass on startup; for a nontrivial codebase that's
seconds, not milliseconds. Keeping one warm process behind a gRPC server
means every subsequent MCP tool call is a few dozen milliseconds instead
of a cold-start type-check.
This repo is a clean-room extraction from a larger internal project. Two
gRPC RPCs — Lint (ran an internal lint engine) and CitCoverage
(reverse-mapped test files to an internal CI system) — depended entirely
on internal infrastructure with no public equivalent and were removed
from the proto, the oracle server, and the MCP tool surface. See
BUILD_NOTES.md for the full list of engineering
decisions made during extraction.
cargo build --workspace # build both crates
cargo test --workspace # 44 unit tests, no oracle process required
cargo build --release --workspaceAn end-to-end smoke test (scripts/e2e_test.py) drives the real mcp
binary over stdio MCP and the real oracle binary over gRPC against
examples/sample-python, exercising all four tools with live pyrefly
data. Requires pyrefly and ripgrep installed and a running oracle
process:
PYREFLY_WORKSPACE_ROOT=examples/sample-python GRPC_LISTEN=127.0.0.1:50052 \
./target/debug/oracle &
python3 scripts/e2e_test.pyApache-2.0 — see LICENSE.