codebase-cartographer
by sdg5-hub
README.md
# Codebase Cartographer
An MCP server that maps a local Python repository into an AST-backed symbol graph and
performs **verified multi-file refactors** on it.
Most coding agents refactor by grepping for a string and rewriting what they find. That
approach cannot tell a call to `compute()` apart from a local variable that happens to be
named `compute`, and it has no idea which twelve files break when you rename it. This server
gives the model a real index instead: scope-resolved references, an import graph, and a
two-phase edit protocol that refuses to write anything it cannot verify.
## What it does
**Maps.** Walks the repository (honouring `.gitignore`), parses every module, and builds a
symbol table of functions, classes, methods, and module-level variables, plus the import
graph between modules.
**Resolves.** Finds references using real scope analysis — the LEGB chain, `global` and
`nonlocal` declarations, comprehension scopes, walrus bindings, and the rule that class
bodies are invisible to nested functions. A local variable that shadows a module-level
symbol is *not* a reference to it, and the tool knows the difference.
**Refactors.** Renames, moves, and deletes symbols across every file that touches them —
rewriting `from x import y`, `import x` + `x.y` attribute access, `as` aliases, and `__all__`
entries. Nothing is written until you apply a plan you have seen the diff for.
## Safety model
Refactoring is two-phase, and the first phase never touches disk.
1. `plan_rename_symbol` / `plan_move_symbol` / `plan_delete_symbol` return a plan id, a
unified diff, and a list of warnings. Every touched file's content hash is recorded.
2. `apply_plan` re-checks those hashes (aborting if anything changed on disk since planning),
builds the new content for every file, **re-parses each one**, and refuses to write if any
file would end up unparseable. Originals are copied to `.cartographer-backups/<plan_id>/`.
A failure mid-write rolls every file back.
Further guardrails: paths outside the mapped root are rejected; renaming a *method* requires
an explicit `allow_heuristic=true` because attribute matching cannot be resolved without type
inference; deleting a symbol that still has references is refused unless forced.
## What it cannot see
Static analysis has a hard edge, and the tool is built to say where that edge is rather than
to pretend it isn't there.
`find_dynamic_references` reports string literals matching a symbol name — `getattr(mod, "compute")`,
plugin registries, dotted settings strings, entry-point tables — and detects imports of
underscore-prefixed native modules. Those warnings are attached to every refactor plan.
This matters concretely. Renaming `JSONDecodeError` in a copy of the stdlib `json` package
rewrites all 19 Python references correctly and *still* breaks the package, because the `_json`
C accelerator resolves that name at runtime through the C API. No pure-Python analysis can
follow that. The tool flags the native accelerator import and tells you to check by hand.
Also invisible: `from x import *` re-exports (reported as a warning), runtime monkey-patching,
and references from other languages or config files.
## Tools
| Tool | Purpose |
| --- | --- |
| `map_repository` | Scan a directory and build the index. Call this first, and again after applying. |
| `repository_overview` | Stats, packages, import cycles, most-depended-on modules. |
| `list_modules` | Indexed modules, filterable by dotted prefix. |
| `file_outline` | One file's imports and definitions. |
| `search_symbols` | Find definitions by substring, kind, or module prefix. |
| `get_symbol_source` | Full source of one definition, with docstring and decorators. |
| `find_references` | Every scope-resolved use of a symbol. |
| `find_dynamic_references` | String literals and native imports that static analysis can't follow. |
| `dependency_graph` | Internal import edges, whole-graph or one module's neighbourhood. |
| `impact_of_change` | Transitive dependents — the blast radius of editing a module. |
| `find_dead_code` | Module-level definitions with no discoverable references. |
| `plan_rename_symbol` | Plan a repository-wide rename. Writes nothing. |
| `plan_move_symbol` | Plan moving a definition to another module. Writes nothing. |
| `plan_delete_symbol` | Plan removing a definition and its `__all__` entry. Writes nothing. |
| `preview_plan` | Re-render a pending plan's diff. |
| `list_plans` / `discard_plan` | Inspect or drop pending plans. |
| `apply_plan` | Commit a plan, with hash checks, syntax validation, and backups. |
## Install
```bash
pip install -e .
```
Register it with an MCP client:
```json
{
"mcpServers": {
"codebase-cartographer": {
"command": "python",
"args": ["-m", "cartographer"],
"env": { "CARTOGRAPHER_ROOT": "/path/to/your/repo" }
}
}
}
```
`CARTOGRAPHER_ROOT` is only the default for `map_repository`; the tool takes an explicit
`root` argument too.
## A typical session
```
map_repository(root="~/work/service")
-> 412 files, 1 import cycle, 38k LOC
impact_of_change(module="service.auth.tokens")
-> 23 transitive dependents
find_references(qualname="service.auth.tokens.decode_jwt")
-> 31 references across 12 files
find_dynamic_references(name="decode_jwt")
-> 1 string literal in service/registry.py:44 <- read this before proceeding
plan_rename_symbol(qualname="service.auth.tokens.decode_jwt", new_name="decode_access_token")
-> plan 9f2a1c: 31 edits across 12 files, with diff
apply_plan(plan_id="9f2a1c")
-> written, backed up to .cartographer-backups/9f2a1c/
```
## Development
```bash
pip install -e ".[dev]"
pytest
```
65 tests cover scope resolution (shadowing, `global`, comprehensions, class-body visibility),
reference finding across every import style, rename/move/delete correctness, generated-import
style, and the apply-phase guarantees: stale-file detection, syntax validation, and rollback.
The suite's strongest check is that the tool refactors *its own source* — renaming a function
across the implementation and the tests, and moving one between modules — after which the full
suite still passes against the rewritten copy.
## Requirements
Python 3.10+ (uses `ast.alias` position attributes). Python source only.
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues