For about a year, my AI code review setup looked like this: AI gets a PR, AI greps for related code, AI reads way too many files, AI says "looks fine."
It mostly worked. Until the bugs that didn't show up in grep started shipping.
The problem wasn't the model. It was the retrieval. Vector search and keyword grep are great at finding files that mention auth.py
. They're terrible at finding files that depend on auth.py
through three import hops, an event bus, and a decorator. That's where the bugs live.
I rewired the retrieval layer with a code knowledge graph plugged in through MCP. Three bugs surfaced in the first week that vector search had been quietly missing. Here's what changed and the bugs themselves.
Vector search retrieves by semantic similarity. "Find code about authentication" finds auth.py
, login.py
, password_validator.py
. Useful.
Knowledge graphs retrieve by structural relationship. "What depends on auth.py
?" returns the call graph -- including event_handlers/login_event.py
, which never mentions auth in its variable names but listens to a login event whose payload changes when auth.py
changes.
Both are valid. They answer different questions. The bugs that ship to production tend to live in the second question.
The Model Context Protocol (MCP), released by Anthropic in late 2024, lets you expose tools to a model in a standard way. By 2026 it's supported by Claude Code, Cursor, Windsurf, Zed, VS Code, and (as of GA in May 2025) the official MCP Registry hosts hundreds of servers.
I used code-review-graph, an open-source tool that builds a property graph of your codebase and exposes it as an MCP server. The setup is a three-line ritual:
pip install code-review-graph
code-review-graph build ./my-project
code-review-graph install # auto-detects Claude Code / Cursor / Windsurf
The graph contains nodes for files, classes, functions, and tests, with edges for imports, calls, inheritance, decorates, listens-to, and tested-by. Once it's wired in, the AI can call MCP tools like:
| Tool | What it answers |
|---|---|
blast_radius(file) |
|
| Every file that depends on this one (N hops) | |
flow_trace(func) |
|
| Where a function's output flows | |
semantic_search(query) |
|
| Hybrid: vector + graph proximity | |
community_detect() |
|
| Tightly-coupled modules | |
risk_score(diff) |
|
| Numerical risk of a change | |
dead_code() |
|
| Unreachable from any entry point |
Anthropic's MCP rollout in 2025 also brought OAuth, prompts, and resource subscriptions, so the graph can push updates when files change instead of being re-queried each turn. That detail matters at scale -- code KGs are not cheap to walk, and stale snapshots are how teams ship bugs.
Before the graph, my AI reviewer was getting context like this:
PR diff: auth.py + 1 file
Reviewer context: grep for "auth" -> 50 related files
Tokens: ~150,000
After the graph, it gets this:
PR diff: auth.py + 1 file
Reviewer context: blast_radius("auth.py", hops=2) -> 7 files
Tokens: ~18,000
The first time I ran it, the AI answered the review question in two seconds with a 7-file context. I had spent 30 minutes the day before grepping the same answer by hand. That moment of "what was I doing with my career" is, I think, the actual product of harness engineering.
But cheaper context isn't the interesting part. The interesting part is which bugs the graph surfaced that grep + vector had missed.
The diff was small. auth.py
added a device_id
field to its login event payload.
Vector search retrieved login.py
, auth_test.py
, password_validator.py
-- the obvious neighbors. The reviewer approved.
The graph retrieved one extra file: event_handlers/audit_log.py
. It listens to login_event
and serializes the payload to a fixed schema in S3. Adding a new field broke the schema validator on every login. Production caught fire 90 minutes after merge.
Why grep missed it: audit_log.py
doesn't import auth.py
. It listens to an event bus. There's no string match on "auth" in the file.
What the graph saw: auth.py --emits--> login_event --consumed-by--> audit_log.py
. Three hops, zero string matches, but a clean structural path.
A code review pass that doesn't follow event subscriptions is a code review pass that doesn't review event-driven systems.
A teammate refactored @with_retry
to add a backoff parameter. The default value was the same, so existing callers were "unaffected." Reviewer approved on the strength of the unit tests.
Vector search retrieved files that explicitly imported the decorator. About a dozen.
The graph retrieved 31 files. The 19 the graph added were files that applied @with_retry
to functions that, three calls deep, ended up calling a function whose retry behavior had subtly changed under load.
One of those callers was a payments webhook handler. Under retry, it now waited an extra 800ms before raising. That 800ms put it past the webhook timeout. We started losing about 0.4% of webhook deliveries silently.
Why grep missed it: a decorator's effect propagates to every callsite of every decorated function. That's structural, not lexical.
What the graph saw: with_retry --decorates--> {19 functions} --called-by--> {31 files}
. The caller of a decorated function inherits the decorator's behavior, even if it never mentions the decorator's name.
A migration changed the way one ID was hashed. The PR included a test that asserted the new hash. CI was green.
The graph showed something the test runner didn't: that test was in a file that no longer ran in CI because it had been moved out of the tests/
directory three weeks earlier and nobody had updated the path glob in the CI config. The test passed because the test file was never executed. The PR landed with a broken hash that corrupted the migration's first 8,000 rows.
The graph had dead_code()
for unreachable functions and an inverse query for unreachable test files. I'd never asked it. After this PR, I added "run dead_code()
on test files" to the postflight check.
Why grep missed it: grep doesn't know what CI runs. A test file's existence and a test file's execution are different facts.
What the graph saw: test_user_id.py
had no incoming edge from any CI config and no tested-by
edges from production code. It was a green file in a green repo that didn't actually test anything.
I don't want to oversell this. Three things the graph is bad at, and you should keep using vector search or grep for:
The right setup is hybrid: vector search for "find related concepts," graph for "find dependents," grep for "find this exact string." MCP makes that hybrid trivial because the model picks the tool per question.
Three things have changed since I started doing this in late 2024:
@workspace
queries instead of relying purely on file embeddings.repomap
for project-wide structural context. Not a graph, technically, but the same idea: structural retrieval beats lexical retrieval for cross-file changes.The pattern is converging. By the end of 2026, "AI code review" without a structural retrieval layer is going to look the way "AI code review without a vector store" looked in 2023. Quaint.
If you don't already have something like this, the pragmatic order is:
code-review-graph
or equivalent.blast_radius
to the postflight checkI had the graph for two weeks before the first bug it caught -- the audit log schema break. I don't think I would have shipped that bug without it. I do know I shipped six versions of it across my career before I had this tool.
For the last decade, code search has meant "embed the file and find similar embeddings." That's a fine answer to half the question. The other half -- "what does this change break?" -- is structural, and embeddings don't see it.
The graph sees it. MCP makes the graph addressable. Together they collapse most cross-file retrieval into a single, cheap query. The bugs that used to live in those gaps don't, anymore.
Want the full rationale and more graph patterns?I cover graph schema design, GraphRAG vs vector RAG, and code-as-graph patterns in[Knowledge Graph Practical Guide: From RAG Limits to Graph-Native AI].