Skip to main content
Glama

repo-graph-mcp

Turn a codebase into a deterministic AST knowledge graph (no embeddings, no vector store, no LLM) and serve it over MCP, so coding agents ask the graph instead of grepping.

CI License: MIT Python Typed MCP

repo-graph-mcp

repo-graph-mcp parses Python (stdlib ast) and TypeScript/JavaScript (tree-sitter) into a graph of modules, classes, functions, methods, symbols and tests, linked by imports, defines, calls, inherits, tests and references edges. The graph lives in one SQLite file (.repo-graph/graph.db), is rebuilt incrementally by content hash, and is served to Claude Code, Cursor, Codex or OpenCode through the official MCP Python SDK. Every answer is compact JSON with path:line, so the agent jumps to the right lines instead of reading whole files. Runs 100% locally, no paid APIs required.

Why

An agent that needs "who calls parse_order?" or "what breaks if I change Store.open?" usually greps, opens every file the grep mentions, and reads them in full. That costs tokens, and grep cannot tell a call from a comment, a method from a same-named function, or follow self.repo.store(...) to Repo.store. A static graph can, and it answers in a few hundred tokens.

Question on this repository

Graph tokens

grep + read tokens

Savings

callers read_bounded

213

12,190

57.2x

impact Store.open

1,269

35,431

27.9x

path_between cli.main->build_graph

308

19,794

64.3x

source Graph.tests_for

559

6,480

11.6x

dependents repo_graph_mcp.paths

234

154

0.7x

All 10 benchmark questions

5,808

117,818

20.3x

Measured offline by benchmarks/token_savings.py (full table there; CI re-runs it and fails below 5x). It is not always a win: for "who imports this module", a grep for import ...paths is already small.

No embeddings means no model download, no drift between runs and nothing to re-embed: the same commit always gives the same graph.

Related MCP server: Symbra

Quickstart

pip install "repo-graph-mcp[typescript] @ git+https://github.com/MartinCastroAlvarez/repo-graph-mcp"
cd your-repo && repo-graph index        # -> .repo-graph/graph.db
repo-graph query callers parse_order    # ask from the shell
claude mcp add --scope project repo-graph -- repo-graph serve
repo-graph config cursor                # or codex / opencode

It will be pip install "repo-graph-mcp[typescript]" once the first release is on PyPI. Leave out [typescript] for a Python-only install (no compiled dependencies beyond the MCP SDK).

$ repo-graph query callees shop.service:Checkout.run
{"symbol":{"id":"shop.service:Checkout.run","kind":"method","loc":"shop/service.py:22","sig":"(self, items: list[int]) -> Order"},
 "callees":[{"id":"shop.models:Order","kind":"class","loc":"shop/models.py:13","at":23},
            {"id":"shop.models:Order.save","kind":"method","loc":"shop/models.py:28","at":24},
            {"id":"shop.models:audit","kind":"function","loc":"shop/models.py:33","at":25},
            {"id":"shop.service:Repo.store","kind":"method","loc":"shop/service.py:12","at":26}]}

order.save() resolved through order = Order(items), and self.repo.store() through the repo: Repo constructor parameter.

How it works

flowchart LR
    subgraph Repo["repository (read-only)"]
        PY[".py / .pyi"] & TS[".ts .tsx .js .jsx"]
    end
    W["walker<br/>.gitignore · size + count limits<br/>no symlink escapes"] --> H{"sha256<br/>changed?"}
    Repo --> W
    H -->|yes| P["parse<br/>ast · tree-sitter<br/>(never executed)"]
    H -->|no| C["cached facts"]
    P --> F[("files.facts<br/>JSON")]
    C --> F
    F --> R["resolver<br/>imports · re-exports · self/this ·<br/>super() · type hints · return types"]
    R --> DB[("graph.db<br/>nodes · edges · FTS5")]
    DB --> G["Graph queries"]
    G --> MCP["MCP server (stdio)<br/>13 tools"]
    G --> CLI["repo-graph query / export"]
    MCP --> A["Claude Code · Cursor ·<br/>Codex · OpenCode"]
  1. Walk. Source files under the root, sorted, honouring .gitignore (nested ones too), exclude globs, max_file_bytes and max_files. Symlinks are never followed; one that points outside the root is refused.

  2. Parse what changed. Each file is hashed; only new or changed files are parsed. A parser turns a file into facts (definitions with signatures and first docstring lines, imports, uses with their enclosing scope, and simple type hints) stored as JSON next to the hash.

  3. Resolve. Facts from every file are resolved into one graph: module names (src layouts, packages, relative imports), re-exports through __init__.py / index.ts, star imports, self/cls/this, super(), annotated parameters, x = Foo(), self.x = ..., return annotations (make().run()), and the class hierarchy. When none of that works for obj.method(), a name match is used only if exactly one method in the project has that name (and it is not a generic one like get); those edges carry "conf": "name".

  4. Tests. Test functions/it() blocks are linked to what they call; a test with no resolvable call falls back to its name (test_add_numbers_x → add_numbers), and a test module to the module it is named after.

  5. Store and answer. Nodes and edges go to SQLite (parameterised statements only) with an FTS5 index over names and docstrings. Queries are breadth-first walks with depth and size limits.

The id of a node is module:Local.path (shop.models:Order.total, src/util:Cache.put); every tool also accepts a dotted name, a bare name, or path:line. Ambiguous names return the candidates instead of guessing.

MCP tools

Tool

Answers

find_symbol(name, kind?)

where something is defined (with signature and doc line)

source(symbol, context?)

only that definition's lines, flagged stale if the file changed

callers(symbol, depth?) / callees(symbol, depth?)

the call graph, with the line of each call

dependencies(module) / dependents(module, depth?)

internal imports, external packages, importers

impact(symbol, depth?)

blast radius: transitive callers, references, subclasses, importers, the tests to run and the files touched

tests_for(symbol)

tests linked directly, and tests that reach it through calls (with the path)

path_between(source, target)

the shortest chain of calls/imports/references/inherits

module_summary(module)

outline of a file: classes with methods, functions, symbols, imports, dependents, tests

search(query, kind?)

FTS5 over names, snake_case/CamelCase words and docstrings

stats()

counts, parse errors

reindex()

incremental refresh after edits (the only tool that writes, and only to graph.db)

All tools except reindex are annotated read-only. repo-graph query <tool> runs the same code from the shell.

Clients

repo-graph config <client> prints these; --root pins the repository when the client does not start the server in it.

Client

Configuration

Claude Code

claude mcp add --scope project repo-graph -- repo-graph serve (writes .mcp.json)

Cursor

.cursor/mcp.json: {"mcpServers": {"repo-graph": {"command": "repo-graph", "args": ["serve"]}}}

Codex

codex mcp add repo-graph -- repo-graph serve, or [mcp_servers.repo-graph] in ~/.codex/config.toml

OpenCode

opencode.json: {"mcp": {"repo-graph": {"type": "local", "command": ["repo-graph", "serve"]}}}

serve refreshes the index before it starts (--no-index skips that). See examples/ for ready-to-copy files.

CLI

repo-graph index   [--root .] [--db PATH] [--full] [--json]
repo-graph query   [--root .] [--pretty] <tool> <args>     # find_symbol, callers, impact, ...
repo-graph serve   [--root .] [--no-index]                 # MCP over stdio
repo-graph stats   [--root .]
repo-graph export  [--root .] --format mermaid|json|dot [--level module|symbol|all] [--focus PREFIX] [-o FILE]
repo-graph config  claude|cursor|codex|opencode [--root PATH] [--exe repo-graph]

Exit codes: 0 ok, 1 the question had no answer (unknown or ambiguous symbol; JSON on stderr), 2 usage or configuration error.

repo-graph export --focus repo_graph_mcp/ on this repository (Mermaid is the default format):

flowchart LR
    n0(["repo_graph_mcp"])
    n1(["repo_graph_mcp.__main__"])
    n2(["repo_graph_mcp.cli"])
    n3(["repo_graph_mcp.clients"])
    n4(["repo_graph_mcp.config"])
    n5(["repo_graph_mcp.export"])
    n6(["repo_graph_mcp.facts"])
    n7(["repo_graph_mcp.graph"])
    n8(["repo_graph_mcp.indexer"])
    n9(["repo_graph_mcp.parsers"])
    n10(["repo_graph_mcp.parsers.python"])
    n11(["repo_graph_mcp.parsers.typescript"])
    n12(["repo_graph_mcp.paths"])
    n13(["repo_graph_mcp.resolver"])
    n14(["repo_graph_mcp.server"])
    n15(["repo_graph_mcp.store"])
    n16(["repo_graph_mcp.walker"])
    n1 --> n2
    n2 --> n0
    n2 --> n3
    n2 --> n4
    n2 --> n5
    n2 --> n7
    n2 --> n8
    n2 --> n12
    n2 --> n14
    n5 --> n7
    n7 --> n12
    n7 --> n15
    n8 --> n0
    n8 --> n4
    n8 --> n6
    n8 --> n9
    n8 --> n12
    n8 --> n13
    n8 --> n15
    n8 --> n16
    n9 --> n6
    n9 --> n10
    n9 --> n11
    n10 --> n6
    n11 --> n6
    n13 --> n6
    n14 --> n0
    n14 --> n4
    n14 --> n7
    n14 --> n8
    n15 --> n6
    n15 --> n13
    n16 --> n4
    n16 --> n12

Configuration

[tool.repo-graph] in pyproject.toml, or a .repo-graph.toml at the root. Unknown keys are errors.

Key

Default

Env override

Meaning

exclude

[]

gitignore-style globs to skip (on top of .gitignore)

languages

["python", "typescript"]

typescript also covers .tsx, .js, .jsx, .mjs, .cjs

max_file_bytes

1000000

REPO_GRAPH_MAX_FILE_BYTES

larger files are skipped

max_files

20000

REPO_GRAPH_MAX_FILES

the walk stops here (reported)

docs

true

store first docstring / JSDoc lines (set false to keep comments out of answers)

respect_gitignore

true

honour .gitignore files

db

.repo-graph/graph.db

REPO_GRAPH_DB

graph location (--db wins)

Always skipped: .git, .venv, venv, node_modules, __pycache__, tool caches and .repo-graph itself. *.d.ts files are ignored. The database directory gets its own .gitignore (*), so it never ends up in a commit.

Limits (by design)

Static and best-effort: dynamic dispatch, getattr, decorators that replace functions, monkey-patching, dependency injection containers and untyped callbacks are not followed. TypeScript paths aliases from tsconfig.json are treated as external packages. When a link is a guess it says so (conf: name); when a name is ambiguous the tool returns candidates.

Testing

python -m venv .venv && . .venv/bin/activate && pip install poetry && poetry install --all-extras
poetry run pytest                                  # 87 tests, coverage ~93%
poetry run ruff check repo_graph_mcp tests benchmarks && poetry run mypy repo_graph_mcp
poetry run python benchmarks/token_savings.py      # the table above

The tests index two fixture projects (Python, and TypeScript/TSX/JS), check every resolution rule, drive the MCP server in-process and over a real stdio subprocess, and enforce the security contract: a fixture module that would write a file if executed must never run, symlinks out of the root are refused, the query connection is read-only, SQL is never built from strings, hostile FTS5 input is inert. A CI job also runs without the typescript extra.

Part of the series

Series

Repositories

Agent tooling

agent-lint · repo-graph-mcp · s3-agent-memory · pytest-llm-evals · serverless-mcp-aws · agent-model-router

Django admin family

django-admin-react · django-admin-rest-api · django-admin-mcp-api · django-admin-graph · django-admin-skills

Apps

local-rag-ollama · ui-review-agent

Patterns catalog

agentic-architectures — every agent pattern, linking the LangGraph, ADK, CrewAI and LangChain repos

Contributing

See CONTRIBUTING.md, ARCHITECTURE.md, SECURITY.md and the agent contract in CLAUDE.md.

License

MIT © 2026 Martín Castro Álvarez. See LICENSE.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    Deep code indexing for AI agents. Search symbols, navigate call graphs, explore inheritance, track git history — all via MCP.
    141 PyPI
    57
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI coding agents to query a local, continuously updated symbol graph of a codebase, providing ranked search, caller/callee exploration, dependency paths, and git-diff impact analysis through MCP tools.
    1 npm
    Apache 2.0
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables people and AI agents to query local code repositories as graphs through stdio MCP tools for project listing, search, dependency tracing, impact analysis, bridge detection, node inspection, bounded code reading, and optional LSP definition/reference lookups.
    MIT
  • A
    license
    B
    quality
    C
    maintenance
    Indexes a Python/JS/TS repository into a local SQLite knowledge graph so agents can trace callers, callees, imports, subclasses and covering tests, measure impact radius, rank diff risk, and summarize architecture without grepping files. Exposes these queries as MCP tools alongside an equivalent CLI, with no daemon, embeddings, or network access.
    10
    MIT