Skip to main content
Glama
README.md
# repo-map

**A navigable map of your codebase for coding agents — so they stop reading whole files just to find their bearings.**

`repo-map` is a [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server. It parses a repository into a symbol graph, ranks every symbol by importance (a tuned PageRank), and exposes a handful of tools that let an agent like Claude Code *locate* code and read *only the symbol it needs* — instead of loading entire files to orient itself.

Think of it as an "Obsidian for your codebase": an outline + a reference graph + an importance sort, queryable without opening the files.

## Why

Reading whole files to understand where a piece of logic lives is the single biggest source of wasted context in agentic coding. On a real exploration workflow over an unfamiliar Python repo, routing that navigation through `repo-map` instead of raw file reads measured:

| Metric | Result |
| --- | --- |
| Total workflow token cost | **−33 %** (up to **−52 %** cold) |
| Context loaded into the model | **−64 %** |
| `outline` vs a full file read | **−96 %** |

The gains come from never paying for a file body you don't end up editing.

## Tools

| Tool | What it does |
| --- | --- |
| `index(path)` | (Re)target the server on a repo and build its map. |
| `where_is(query, repo=None)` | Find where a symbol is defined, by name (substring, case-insensitive), ranked by contextual PageRank. |
| `grep_code(pattern, max_results=40, repo=None)` | Regex search by *content* — each hit **situated in its enclosing symbol** (`def`/`class`/`<module>`), not a bare line. |
| `outline(file, repo=None)` | Table of contents of a file: class/function signatures + line ranges. ~95 % fewer tokens than reading it. |
| `get_symbol(file, name, repo=None)` | The full body of a single symbol — the only thing you actually read to start coding. |
| `who_references(name, repo=None)` | Who calls a symbol (what a signature change might break). |
| `what_it_uses(name, depth=1, max_results=60, repo=None)` | The mirror: what a symbol *depends on*. Accepts a symbol, a file (everything it defines and imports) or an exact `file::symbol`. `depth=0` walks the full transitive closure — all the code actually reached from an entry point. |
| `refresh(repo=None)` | Rebuild a target's map (incrementally) **without moving the default target** — use it after editing files. |

### One repo per call (`repo=`)

Every read tool takes an optional `repo=` (always the **last** parameter, so existing positional calls keep working). It sets the target **for that single call**:

```
index("/work/backend")          # default target for this server
outline("api/routes.py")        # → /work/backend
outline("src/App.tsx", repo="/work/dashboard")   # → /work/dashboard, built on the fly
where_is("Router")              # → still /work/backend
```

Why it matters: the server is started **once per session** and several agents can query it in parallel on different repos. Each target keeps its own graph and its own ranking context (recently touched files / mentioned symbols), so one agent's exploration can't silently answer another agent's question with the wrong repo's map. A few targets are kept in memory at once (LRU, see `GRAPHS_CAP`); an evicted one is rebuilt from the on-disk cache when it comes back — or fully rebuilt if that cache is missing or stale.

A map does **not** refresh itself: once built, a target's map stays as it is until something rebuilds it. `index(path)` rebuilds, but it also moves the default target — which would steal it from another agent. Use `refresh(repo=...)` to rebuild a target and nothing else:

```
refresh(repo="/work/dashboard")   # rebuilt (incrementally); default target untouched
```

`index(path)` still sets the **default** target — the one used when `repo` is omitted — and resets the ranking context **of that target only**: it no longer wipes the graph or the session state of the other targets. Calling a tool without `repo` behaves exactly as it always did.

## Languages

Python, JavaScript/JSX, TypeScript/TSX — via precompiled [tree-sitter](https://tree-sitter.github.io/tree-sitter/) grammars (no C toolchain required, including on Windows).

## Install

```bash
git clone https://github.com/noambinabout-boop/repo-map.git
cd repo-map
python -m venv .venv
# Windows:  .venv\Scripts\activate
# Unix:     source .venv/bin/activate
pip install -r requirements.txt   # or: uv pip install -r requirements.txt
```

### Wire it into Claude Code

Register the server (adjust the paths to your clone):

```bash
claude mcp add repo-map -- /path/to/repo-map/.venv/bin/python /path/to/repo-map/server.py
```

Then, from any session:

```
index("/path/to/the/project/you/want/to/explore")
where_is("MyClass")
outline("src/app.py")
get_symbol("src/app.py", "MyClass")
```

The server also runs standalone over stdio (`python server.py`) for any MCP-compatible client.

## How it works

- **Symbol graph.** tree-sitter parses each file into definitions and a call/reference graph.
- **Importance ranking.** A PageRank variant weighted *against* popularity (`1/fan-in`) so ubiquitous helpers don't drown out the code that actually structures the repo, merged with the import graph so shared components/constants aren't invisible.
- **Scope resolution.** `self`/`cls`/`this`, class inheritance (Python & JS/TS `extends`), named/namespace/default imports, light type inference (`x = Ctor(); x.foo()` → `Ctor.foo`) and **lexical scope** (a call to a name the calling file defines itself resolves to that local definition, not to same-named functions elsewhere) are resolved to the right target. Resolution stays *conservative*: when a target is ambiguous, it falls back to a broad edge rather than dropping one.
- **Path aliases.** `compilerOptions.paths` / `baseUrl` from `tsconfig.json` / `jsconfig.json` are honored (JSON-with-comments tolerated), so `@/lib/db` resolves like a relative import. This is the default import style on Next/Vite: on a 285-file Next app, 532 of 601 internal imports go through `@/`, and reading them **doubled the import graph** (3 135 → 6 745 edges). Fail-open — an unreadable or exotic tsconfig just means no aliases.
- **Incremental cache.** Parses are cached per file by mtime in `~/.repo-map/cache/` (override with `REPO_MAP_CACHE`). Nothing is written into the repos you target. First index of a 284-file TS project: ~17 s; subsequent: ~0.3 s.
- **Per-repo ignores.** Drop a `.repomapignore` (`.gitignore` syntax) at a repo root to keep generated/vendored dirs out of the graph.

## Limitations (honest)

- **PageRank is sharper on Python than on React/Expo.** The import-graph merge fixes "invisible shared components," but entry-point screens referenced only once by a route table can still rank low.
- **Edges mean "uses", not just "calls" (JS/TS).** Since 2026-08-19 the graph also reads JSX tags (`<PatientCard />`), identifiers passed as props (`onClick={handleDelete}`) and methods called on an imported object (`db.query(...)` links to `db`, the symbol that actually crosses the file boundary). Measured on a 208-file Next.js app: unreachable symbols dropped from 571 to 442, and screen components never reached by anyone from 113 to 7. Lowercase JSX tags (`<div>`) are skipped — that is JSX's own rule for native elements. Python is untouched.
- Name-based resolution outside the cases above may **add** a spurious edge. The single place where edges are deliberately **removed** is lexical scope: a call is no longer linked to a same-named definition in *another* file when the calling file defines that name itself. Measured on a 285-file TS app: 59 edges out of 2006 dropped, all of them cross-file homonyms (a `main()` per CLI script, a `load()` per React component), and **no caller left disconnected** from the symbol it calls. Methods are excluded from this rule — reaching them requires `self.`/`this.`, handled separately.
- Python `from . import x` (no module name) is not resolved.
- repo-map indexes **structure**, not literals — use `grep_code` for flags/config strings.

## Tests

```bash
./.venv/Scripts/python.exe tests/run_tests.py
```

A non-regression suite of fixtures freezes each scope-resolution feature and compares the exact set of graph edges. It also covers target isolation: two repos alive in one process, same-named files, default target, ranking context and LRU eviction.

## Prior art

The "repo map" idea was pioneered by [Aider](https://aider.chat). repo-map is an independent MCP implementation with its own ranking, conservative scope resolution, and per-file incremental cache.

## License

[MIT](LICENSE)