A zero-dependency, local, interactive call-graph navigator for Python codebases. One script in, one self-contained HTML file out — plus an MCP server so AI agents can walk your call graph too.
python3 callscope.py /path/to/repo -o map.html
open map.html
No install, no server, no index database, no API keys. Python 3.9+ stdlib only.
git clone https://github.com/benwelker/callscope
cd callscope
# generate an interactive map of any Python codebase
python3 callscope.py ~/src/myproject -o myproject.html
open myproject.htmlOr install it as a command (optional):
pipx install git+https://github.com/benwelker/callscope
callscope ~/src/myproject -o myproject.htmlIn the map:
- Pick a function in the sidebar (or press
/and search). Its source opens in a pane with every call site highlighted. - Click a call → the callee opens in the next pane. The chain of panes is the call path you're exploring.
- ← callers on any pane walks the graph backwards.
- Link styling is honest about confidence: solid underline = statically resolved, dashed = best-effort match, dotted grey = defined outside the codebase (builtin/stdlib/third-party).
Understanding an unfamiliar codebase means chasing function calls across many files. The tool developers keep asking for — most recently as the top request in Hacker News' "Ask HN: What developer tool do you wish existed in 2026?" — is: open a function in a pane showing only that function with its calls highlighted, click a call, and keep walking.
Sourcetrail, the beloved tool closest to this, was discontinued in 2021 and never replaced. Sourcegraph is cloud-oriented code search; Understand and Source Insight are commercial; IDE "peek definition" is one hop at a time with no persistent trail. CallScope is the missing piece: local, instant, deterministic, and shareable — the output is one HTML file with zero external requests, so you can email it, commit it, or open it on a plane.
callscope ROOT [-o map.html] [--name LABEL] [--json PATH] [--no-html]
| Flag | What it does |
|---|---|
-o PATH |
Output HTML file (default callscope.html) |
--name |
Project label shown in the UI (default: directory name) |
--json |
Also write the raw call-graph index as JSON (for scripts and agents) |
--no-html |
Skip the HTML map (use with --json for data-only runs) |
The JSON index contains every function (id, module, file:line, docstring, source) with its resolved call edges and inverted caller edges — everything a script or agent needs to query the graph without re-parsing the codebase.
Coding agents have the same problem humans do — tracing call paths across files
burns time and context. callscope_mcp.py is a
Model Context Protocol server (stdio,
stdlib-only, no packages to install) that lets an agent query the graph
directly.
Register it with Claude Code:
claude mcp add callscope -- python3 /path/to/callscope/callscope_mcp.pyOr in any MCP client's config:
{
"mcpServers": {
"callscope": {
"command": "python3",
"args": ["/path/to/callscope/callscope_mcp.py"]
}
}
}Then ask your agent things like "use callscope to map this repo and show me
how cli() ends up calling Context.invoke". Tools exposed:
| Tool | What the agent gets |
|---|---|
analyze_project |
Scan a codebase, build + cache the graph; stats and module inventory |
search_functions |
Find function ids by name / Class.method / module substring |
get_function |
One function: source, calls (with resolution confidence), callers |
callers_of |
Reverse edges: everything that calls a function |
call_path |
Shortest call chain from A to B ("how does A end up invoking B?") |
generate_map |
Write the interactive HTML map for the human to open |
All tools take a path argument and auto-analyze on first use; pass
refresh: true to analyze_project after the code changes.
callscope.py does a two-pass static analysis with stdlib ast:
- Collect — walks every
.pyfile; indexes functions, methods, classes (with bases), per-module imports (including relative imports and aliases), and raw call sites with exact source positions.@overloadstubs are skipped. - Resolve — links each call site to a definition using, in order:
same-module definitions, imported names (module- and function-level),
self/clsdispatch with base-class walking, local-variable type inference fromvar = ClassName()assignments, module-alias attribute calls, class instantiation →__init__, and finally a globally-unique-name fallback flagged as approximate.
The resolved graph plus source snippets are embedded as JSON in an HTML template whose JS renders the panes, tokenizes Python for syntax highlighting, and overlays clickable call marks by exact line/column.
Scale check: pallets/click → 76 files, 1,679 functions, 2,292 resolved call edges, ~1.8 MB output, under two seconds.
python3 tests/test_resolution.py # each call-resolution rule, via --json
python3 tests/test_mcp.py # MCP handshake + all 6 tools over stdio
cd tests/e2e && npm install && npm test # drives the real UI headlessly (jsdom)Static analysis, not a type checker: duck-typed attribute calls on parameters,
decorators that rewrite functions, and dynamic dispatch (getattr) resolve
only via the flagged approximate fallback or not at all. Python-only for now —
the pane UI is language-agnostic, so other language front-ends only need to
emit the same JSON shape.
Roadmap: opt-in LLM-assisted resolution of the unresolved tail (--ai-resolve),
more language front-ends.
MIT
