Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pyrefly-mcp

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.

Why this exists

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.

Architecture

┌─────────────┐   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...)  │
                                          └──────────────────────┘
  • oracle spawns one pyrefly lsp subprocess 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 seed didOpen + optional warm-seed file list) so the first agent query isn't a cold-start type-check. It also ships a Find RPC backed by ripgrep for raw text/regex search, used both internally (to recover bound-method callers pyrefly's static analysis can miss) and exposed directly to clients.
  • mcp is 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.

Quick start

# 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.

Configuration (environment variables)

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

MCP tools

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.

Engineering notes

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.

What's cut from the public extraction

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.

Development

cargo build --workspace        # build both crates
cargo test --workspace         # 44 unit tests, no oracle process required
cargo build --release --workspace

An 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.py

License

Apache-2.0 — see LICENSE.

About

A warm gRPC oracle + MCP client for Python symbol navigation, powered by Meta's pyrefly LSP

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages