My AI assistant kept giving me confident wrong answers about my own codebase.

Not wrong in an obvious way. Wrong in the way where the answer sounds right, covers the surface, misses three critical connections, and you only realize it was wrong when you’re two hours into a change that shouldn’t have taken twenty minutes.

The specific failure: ask “what handles auth token expiry?” and it would grep for “token” and “expiry”, find some files, miss the actual handler buried behind two layers of delegation, and present the whole thing like it knew exactly what was going on. It saw files. It had no idea how they connected.

I started thinking about what actually helps a new engineer understand an unfamiliar codebase fast. It’s not reading files one by one. It’s building a mental map: this service calls that one, this class extends that base, changing this interface will ripple through fourteen other places. That structural knowledge is what I wanted to give my AI.

So I built codesynapse.

What It Does#

codesynapse parses your codebase, builds a graph of symbols and relationships, and exposes it as 32 MCP tools that your AI can query directly.

Nodes are classes, functions, structs, files. Edges are calls, extends, implements, contains. Multiple repos merge into a single global graph, so cross-module questions work the same as in-module ones. The AI doesn’t grep files anymore. It asks the graph.

graph TD A[Source code: any repo] --> B[tree-sitter AST extraction: 30+ languages] B --> C[Per-module graph.json: nodes and edges] C --> D[Global graph: prefix IDs, merge modules] D --> E[Embeddings: Model2Vec potion-code-16M] D --> F[BM25 index] E --> G[MCP server: 32 tools] F --> G G --> H[Claude Code, Cursor, Windsurf, any MCP client] style A fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style B fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style C fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style D fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style E fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style F fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style G fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff style H fill:#000000,stroke:#00ff00,stroke-width:2px,color:#fff

Extraction runs in parallel with rayon. Graphs merge by prefixing node IDs with module names, so auth::TokenValidator and billing::TokenValidator stay distinct in the global graph. Everything lives in sled, an embedded key-value store: no server process, no infrastructure.

The Search Layer#

I expected the graph to be the valuable part. Turns out the search layer made the bigger practical difference, and I didn’t expect that.

The search is hybrid: BM25 for lexical matching, dense vector embeddings for semantic similarity, fused with RRF. The embeddings use Model2Vec potion-code-16M, a static model that runs entirely on CPU with no transformer forward pass at query time. About 1.5ms per query. The model is 64MB, downloads once on first setup.

Here’s why this matters. Ask “what handles the 404 response payload” and lexical search finds nothing useful because none of the relevant function names contain “404”. Dense embeddings map the query to core_exception_handler based on semantic similarity. The graph then surfaces its callers and callees. Combined, you get both the right answer and full context.

Neither alone is enough. BM25 alone misses semantic queries. Dense alone misses exact symbol names. RRF fusion gives you both.

The Hard Part#

Edge extraction. Grammar coverage across 30+ languages was straightforward with tree-sitter. Getting the call edges right was not.

Dynamic dispatch, trait implementations, field injection in Spring: all of these create relationships that don’t look like direct function calls in the AST. Each language needed heuristics. It’s still not perfect. Java field-injected dependencies don’t produce edges the same way method calls do. It’s a known gap.

Binary size was also a surprise: 83MB uncompressed, because 30+ tree-sitter grammars compile in as C. UPX brings Linux and Windows down to ~8MB. macOS ARM stays large because UPX doesn’t support aarch64 Mach-O.

Try It#

Rust binary, fully local, no cloud API needed.

curl -fsSL https://raw.githubusercontent.com/sohilladhani/codesynapse/master/install.sh | sh
codesynapse setup
codesynapse module add myrepo /path/to/repo

Works with Claude Code, Cursor, Windsurf, Codex CLI, Kiro, or any MCP client. Supports 30+ languages. Source at github.com/sohilladhani/codesynapse.

What I’m Learning#

The structural graph is useful but it answers structural questions. The semantic search is what closes the gap between “what’s the name of the thing” and “what does the thing do”. Building both and fusing them is what actually made my AI sessions more useful.

Have you run into the same problem with AI assistants losing context on large codebases? Curious what approaches others have tried.