Code graph
The code graph is a SQLite database — .code-graph/graph.db — that holds your repository’s structural relationships. Definitions, calls, imports, inheritance, file → file edges. Your AI agent queries it through MCP tools instead of re-grepping for every question.
What’s in the graph
Two tables, as sqlite3 .code-graph/graph.db ".schema" prints them:
CREATE TABLE nodes (
id TEXT PRIMARY KEY, -- content hash, not an autoincrement integer
kind TEXT NOT NULL, -- file | function | method | class | interface | enum | table
name TEXT NOT NULL,
file TEXT NOT NULL, -- repo-relative path
start_line INTEGER,
end_line INTEGER
);
CREATE INDEX idx_node_file ON nodes(file);
CREATE TABLE edges (
src TEXT NOT NULL, -- always a nodes.id
dst TEXT NOT NULL, -- a nodes.id, or a raw name (see below)
kind TEXT NOT NULL -- imports | contains | calls | inherits
-- | implements | depends_on | tests_for
);
CREATE UNIQUE INDEX uq_edges ON edges(src, dst, kind);
CREATE INDEX idx_edge_dst ON edges(dst);
There is no language column — language is inferred from the file extension at
parse time and not stored.
dst is not always a node id. contains, tests_for and depends_on
resolve to a row in nodes. imports and inherits store the raw text the
parser saw — '../api/metricsClient', 'argparse', a base-class name — because
resolving a module specifier to a file needs resolution rules the parser does
not run. Join on dst for the first group; match on it for the second. The
examples below do one of each.
You can query all of this directly with sqlite3. The MCP server is a typed
convenience layer over the same file.
Languages
Tree-sitter parsers for: Python, TypeScript, JavaScript, Go, Rust, Java, C#, Ruby, and more. Languages without a tree-sitter package fall back to regex parsers automatically — coverage is degraded but the build doesn’t fail.
Full list: see .github/code-graph/requirements.txt.
MCP tools
The MCP server exposes the following queries to your agent:
| Tool | Purpose |
|---|---|
get_minimal_context(task) | Default first call. Resolves the symbols named in the task and returns files_to_read: the defining files, what they depend on, then one tier of what depends on them, ranked and capped at 6. Also graph size, risk of the uncommitted changes, and the tool to use next. Under ~150 tokens, so there is no per-file token map — pass files_to_read to get_review_context for that. An empty list always carries a files_reason. |
query_graph(pattern, node_name) | Named patterns: callers_of, callees_of, imports_of, importers_of, tests_for, file_summary. |
get_impact_radius(files) | Takes a list of repo-relative paths, not a symbol. Returns the files reachable from them. |
get_review_context(files, budget_tokens?) | For review tasks: the files plus their immediate neighbours, ranked and trimmed to a token budget. |
find_large_functions(min_lines=50) | Functions over N lines — debt-finder. |
detect_changes(base="HEAD") | Files whose SHA-1 changed since the given ref. |
update_graph() | Incremental re-parse of changed files. |
build_graph() | Full rebuild from scratch. |
graph_stats() | Node + edge counts, last-update time, file count by language. |
visualize_graph() | Renders an interactive HTML graph at .code-graph/graph.html. |
Visualize
uv run -p 3.12 --with-requirements .github/code-graph/requirements.txt \
.github/code-graph/server.py --visualize
Outputs .code-graph/graph.html — a standalone D3 force-directed view (no server needed). Open in any browser. Drag nodes, hover for symbol details, filter by language or kind. Useful for sanity-checking a fresh build, spotting orphan modules, and showing teammates what the agent actually sees.

Why SQLite
- Zero-dependency for queries. Anyone with
sqlite3installed can poke at the graph without booting the MCP server. - Cheap. A graph for a 50k-line codebase is typically a few MB.
- Stable across machines. A teammate can rebuild the graph from the same commit and get the same database — fixture-style reproducibility.
Direct SQL
If MCP isn’t available, the database is just a file:
# every definition inside one file, in source order
sqlite3 -header -column .code-graph/graph.db "
SELECT child.kind, child.name, child.start_line
FROM edges e
JOIN nodes parent ON parent.id = e.src
JOIN nodes child ON child.id = e.dst
WHERE parent.file = 'src/i18n/t.ts' AND e.kind = 'contains'
ORDER BY child.start_line;
"
kind name start_line
-------- ---------------------- ----------
function localizedPath 43
function detectLocaleFromHeader 49
Because an imports target is a raw string, the importers of a module are
matched rather than joined:
sqlite3 -header -column .code-graph/graph.db "
SELECT DISTINCT n.file
FROM edges e
JOIN nodes n ON n.id = e.src
WHERE e.kind = 'imports' AND e.dst LIKE '%metricsClient'
ORDER BY n.file;
"
This is the documented fallback when the MCP server isn’t registered. Agents in Coograph projects know to try MCP first, then fall back to sqlite3, then — only if both fail — to grep.
Auto-update
The git hooks in .github/code-graph/ (post-commit, post-merge, post-rewrite) call --update after every change. Only files whose content SHA-1 changed are re-parsed. Files that import from changed files are also re-parsed so cross-file edges stay accurate.
An update keeps the call edges earlier runs resolved. Before 1.6.0 it re-resolved them by name and dropped every one of them, so a graph that had lost its calls edges after a commit was that bug, not your code: pull the fix and rebuild once with --build.
Limits
- The graph captures structural relationships, not semantic ones. It knows that
place_ordercallsvalidate; it doesn’t know that adding caching will affect downstream billing. - Coverage depends on tree-sitter availability for your language. Fallback regex parsers work for symbol-finding but miss some edges (e.g. dynamic dispatch).
- The graph is rebuilt on a per-file basis. If you commit a thousand-file refactor, the incremental update is O(thousand-file).