An MCP server that gives coding agents a pre-built symbol graph of a codebase — find_path,
get_dependencies, get_dependents, get_symbol, search_symbols, search_source,
get_index_coverage — instead of exploring it blind with grep/cat/read.
Point it at a repo and it indexes automatically (tree-sitter parses every symbol and its
call/import/attribute edges into a compact bundle); the agent then queries that graph directly
instead of re-deriving it turn by turn from raw file reads. The bundle stays current as the repo
changes — incrementally, via a content-hash cache — instead of going stale the moment someone
edits code. The server also tells connecting agents (via the MCP instructions field and per-tool
descriptions) to prefer these tools over bash/grep/read for any file the graph covers.
Requires Node 18+.
git clone <this-repo-url>
cd code-index
npm install
npm run buildRegister it with Claude Code, from the code-index directory you just built:
# Per-project — registers code-index for the repo you run this from.
node packages/indexer/dist/cli.js register
# Global — same server, registered once, available in every project you open.
node packages/indexer/dist/cli.js register --scope userThat's it — the server auto-indexes whichever repo your Claude Code session is in on first
connect, and the agent now has 7 tools available (see Tools below). Undo with
claude mcp remove code-index -s <scope>.
Registration scopes, and how the server picks a repo
register runs claude mcp add for you with one of three scopes:
| Scope | Stored in | Shared? | Available in |
|---|---|---|---|
local (default) |
~/.claude.json, keyed to this project |
No, private to you | This project only |
project |
.mcp.json in the repo root |
Yes, via git | This project only (for everyone who clones it) |
user |
~/.claude.json |
No, private to you | Every project you open |
Pass a path as the first argument (e.g. register /path/to/target/repo) to pin the registration
to one specific repo regardless of scope — the same effect as an explicit --repo flag on the
server.
--repo is optional; when omitted, the server resolves which repo to index in this order:
--repo <path> if given, then CLAUDE_PROJECT_DIR (the current session's project root, set by
Claude Code for every spawned MCP server — this is what makes a --scope user registration follow
whichever project you're actually in), then the server process's working directory as a last
resort for clients that don't set CLAUDE_PROJECT_DIR.
Register with any other MCP client
Point it directly at the repo you want indexed:
{
"mcpServers": {
"code-index": {
"command": "node",
"args": [
"/absolute/path/to/code-index/packages/server/dist/main.js",
"--repo", "${CLAUDE_PROJECT_DIR:-.}",
"--watch"
]
}
}
}${CLAUDE_PROJECT_DIR:-.} is Claude Code-specific .mcp.json variable expansion; other clients
generally don't support it, so swap in an explicit absolute path for those.
--repo auto-indexes the target repo into <repo>/.code-index/ on first connect and reuses that
bundle on later starts if the repo hasn't changed; add --watch to keep it updated in the
background for the life of the server process. Prefer a static, separately-managed bundle instead?
Index explicitly and point --okf at the output dir (--repo and --okf are mutually exclusive):
node packages/indexer/dist/cli.js index /path/to/target/repo --out ./okf
node packages/server/dist/main.js --okf ./okf| Tool | Purpose |
|---|---|
get_index_coverage() |
What fraction of the repo is covered by the graph — check this before falling back to grep/read. |
find_path(source, target) |
Shortest dependency/call path between two symbols. |
get_dependencies(symbol, depth?) |
What a symbol depends on: its imports and calls (fan-out). |
get_dependents(symbol, depth?) |
What uses a symbol: callers and importers (fan-in). |
get_symbol(name) |
Full source + metadata for one or more named symbols. |
search_symbols(query) |
Search for symbols by name substring. |
search_source(pattern) |
Full-text search across source bodies and raw file text. |
Full parameter/response reference: docs/ARCHITECTURE.md.
--watchon the server — hot-reloads the in-memory graph after every debounced reindex, no restart needed.code-index watch <repo>— the same watch loop as a standalone process, for keeping a bundle fresh without a server attached.code-index install-hook <repo>— installs a gitpost-commithook that reindexes after every commit.uninstall-hookremoves it.
All three share the same incremental cache — only changed files get re-parsed. See docs/ARCHITECTURE.md for how it works.
- Indexer (
packages/indexer) — parses every supported file with tree-sitter and resolves references (imports, calls,self/this-scoped access) into real edges between symbols. - OKF bundle — one Markdown file per symbol (YAML frontmatter with
depends_on/calls/called_by/dependentsedges, plus a source-code body), anindex.jsonmanifest, and ameta.jsonwith coverage stats. - Server (
packages/server) — loads the bundle into an in-memory graph and answers structural queries over MCP (stdio), hot-reloading the same graph instance as the repo changes.
Full format spec and pipeline details: docs/ARCHITECTURE.md.
An agent exploring a codebase with only bash-style tools pays a traversal tax: it re-derives
the same call graph a static analyzer already knows, one exploratory grep/read round-trip at a
time. Pre-computing the graph once and exposing it as typed navigation tools turns that
rediscovery into direct structural lookups — "what calls this," "what does this depend on," "path
from A to B" — answered in a single call instead of several rounds of blind search.
This pays off most on codebases fragmented across many small files, where bash needs many round-trips to piece together what the graph already knows in one. On monolithic, few-large-file codebases it's merely competitive, not a clean win, since bash can often grep its way to an answer in one or two calls — a real, measured limitation, not a hidden one (see Performance).
Measured against baseline (Claude Code's built-in bash/grep/read tools, no graph) and Graphify, a third-party competing code-graph MCP server — 261 runs, 29 real GitHub-issue-fix instances across django/pytest/requests × 3 reps each, Claude Sonnet:
| resolved rate | avg cost | avg tokens | |
|---|---|---|---|
| baseline | 78% | $0.5749 | 895,776 |
| Graphify | 79% | $0.4555 | 520,726 |
| code-index | 74% | $0.3096 | 346,829 |
code-index vs baseline: −46.1% cost, −61.3% tokens. code-index vs Graphify: −32.0% cost, −33.4% tokens. code-index is the clear cost winner; resolved rate trails by 4-5 points in this pass, concentrated in the requests/pytest repos (django ties at 83% across all three arms) — every below-both-baselines instance was independently re-graded from its exact patch and traced back to genuine model misses on hard instances, not a defect in the tool surface.
Full methodology, per-repo breakdown, and that investigation: docs/BENCHMARKS.md.
- Languages: Python, TypeScript/JavaScript, Java, and Svelte only —
get_index_coveragereports exactly which files that covers for a given repo. Svelte coverage is script-block + template-binding only; the template's own markup/control-flow structure isn't indexed. - Bare-name call resolution (e.g.
obj.start()) falls back to a repo-wide same-name lookup when nothing more precise is available, preferring same-language candidates first. - Cross-language HTTP route links (
api_calls/api_called_by) are discovered by matching HTTP method + URL path, scoped to Flask/FastAPI/Express-style backends andfetch/axiosfrontend calls — no GraphQL, WebSocket, gRPC, or shared-schema matching. - TS/JS tsconfig support:
pathswildcards work anywhere in a pattern, and a roottsconfig.json'sextendschain is followed (string form only). Per-package tsconfig files (one per npm workspace package) aren't discovered yet. - Multi-repo:
--repo/--okfaccept multiple occurrences to merge several repos into one graph, including cross-repo HTTP route linking.
See docs/ARCHITECTURE.md for the mechanics behind each of these.