repo2graph
A GraphRAG engine and MCP server that hands an AI agent the right code — cited, budget-capped, and nothing else.
- Status:
- Active
- Stack
- 10
- Architecture notes
- 11
Overview
An AI agent asked about a codebase usually gets handed whichever files matched a keyword, and pays for all of them. repo2graph parses the repository with tree-sitter into a graph of every folder, file, function, class, import, call and inheritance, then serves retrieval over that graph to the agent directly as an MCP server — five read-only tools over stdio or authenticated HTTP, wired into Claude Code, Claude Desktop or Cursor with one command. Every answer comes back stamped with the file and line range it came from, and inside a hard token ceiling, so the model gets the function plus its callers and callees rather than a folder — and the context window stops being the thing that runs out first.
Architecture Breakdown
How it works
Core mechanics, failure recovery paths, and system design decisions.
- 01
MCP server, five read-only tools
repo_map sketches the repository, repo_search retrieves cited chunks, repo_neighbours walks one hop from a node, and repo_cache_stats and repo_build_status expose the server's own state. All five are annotated readOnlyHint and idempotentHint, and each description says when not to use the tool as well as when to — an agent picking the wrong tool is a wasted round trip, paid for in tokens.
- 02
Two transports, one dispatcher
stdio for a local editor and JSON-RPC over POST /mcp for a hosted one, both driven by the same dispatch(). HTTP carries bearer-token or OIDC auth with stdlib-only JWT verification — the algorithm comes from the key rather than from the token, and tokens compare in constant time. Binding past loopback with no auth configured is refused at startup rather than warned about.
- 03
The token budget is a ceiling, not a hint
repo_search clamps any requested budget to 12,000 tokens and then re-measures the rendered result before returning it, so an overshoot cannot pass silently. repo_neighbours stops at 50 rows and says that it truncated; k caps at 50 and hops at 4, because one expensive call on a single event loop would otherwise stall every connected client.
- 04
Chunks that carry their neighbourhood
Roughly one chunk per function or class, cut at about 4,000 characters with eight lines of overlap so nothing is lost at a seam, and each one opens with its callers and its callees. That header is the difference between a model reading a function and a model placing it — and it removes the follow-up fetches that would have cost far more than the header did.
- 05
Retrieval with citations
BM25 lexical scoring with an identifier boost, optionally fused with dense MiniLM vectors, then a one-hop walk across the graph. Every block in the pack carries its file and line range and the reason it is there — seed, or the CALLS edge that pulled it in — so a claim about the code can be checked instead of trusted. Embeddings stay opt-in, so the default path never downloads a 90MB model, and a fusion that quietly degrades to lexical-only says so rather than pretending.
- 06
Parsing, and where it stops
tree-sitter reads real syntax trees for 17 languages, and every other file still lands on the map in its folder. Calls are matched by name: an ambiguous one draws up to five candidate edges each carrying 1/n confidence, same-file definitions win ties, and only a 1.0 is certain. Files over 1.5 MB, binaries, vendor folders and .gitignore'd paths are skipped outright.
- 07
Incremental builds that stay identical
--incremental reuses a cached parse only when both the file hash and the language match, but always recomputes the resolution phase — the global name index, call confidences, inheritance, entrypoints and reach. The output is byte-identical to a full rebuild rather than merely close, which is the only version of that promise worth making.
- 08
Audit log with redaction by shape
One JSON line per tool call to stderr, at none, errors or all. Sensitive values are redacted both by field name and by what they look like, keeping a length and a short fingerprint so two calls can be correlated without the secret ever being written down. Tool output excludes credential-store paths unconditionally, not behind an opt-in flag.
- 09
Zero graph dependencies
Degree counting, the force layout and GraphML generation are pure Python — no networkx. human/ gets a self-contained graph.html that settles its layout in batches across animation frames instead of one blocking loop; agent/ gets JSONL nodes, edges and chunks, an idempotent Cypher loader for Neo4j, and a manifest describing every other file.
- 10
Shipped every way an agent might reach it
pip install repo2graph from PyPI, npx from npm, a published Docker image, uvx --from "repo2graph[mcp]" repo2graph-mcp with nothing installed at all, and Srinivasan-78/repo2graph@v1 on the GitHub Marketplace as a workflow step. Releases publish over PyPI Trusted Publishing with OIDC, so there is no long-lived token anywhere to leak.
- 11
Blast-radius analysis on a pull request
The GitHub Action walks the graph outward from the files a PR touches and reports what else depends on them, so a reviewer sees the reach of a change before approving it rather than discovering it in production. It needs no LLM and no API key — the analysis is graph traversal, not inference.
Key Takeaways
Engineering Highlights
- An MCP server AI agents talk to directly — five read-only tools over stdio or authenticated HTTP, one command to add in Claude Code, Claude Desktop or Cursor
- Token budgets are enforced rather than requested: a 12,000-token ceiling re-measured after rendering, a 50-row neighbour cap, and bounded k and hops
- Every retrieved block carries its file and line range and the edge that pulled it in, so an agent's claim about the code is checkable
- Chunks ship with their callers and callees in the header, which is what stops a model fetching three more files to understand one function
- Dense retrieval is opt-in, so the default install never downloads a 90MB embedding model — and a silent fall back to lexical-only warns instead
- Incremental rebuilds are byte-identical to full ones, because only parsing is cached and resolution is always recomputed
- HTTP refuses to bind past loopback without auth, JWT verification is stdlib-only, and audit logs redact by shape as well as by field name
- Published to PyPI, npm, Docker, the GitHub Marketplace and the MCP Registry, released over OIDC with no stored secrets
- MIT licensed, created and maintained by Srinivasan — tree-sitter parsing across 17 languages with no LLM or API key required
- It indexes itself: a workflow rebuilds the graph on every push to main and publishes it to a branch
Honest aboutapproximation: calls are matched by name with a confidence score, and a missing edge never proves a missing call
Stack & Tools
- Python 3.10+
- MCP (stdio + HTTP)
- LLM / GraphRAG
- tree-sitter
- sentence-transformers
- OIDC / JWT auth
- Neo4j / Cypher
- GraphML
- PyPI
- GitHub Actions