AST_MCP
Structural code retrieval over MCP. Parses source with tree-sitter and serves
symbols instead of files, so an agent asking "what does parse_config do"
gets 40 lines rather than 2000.
Read-only. The server never writes to your source — the only file it writes is
its own index at .ast_mcp/index.db.
Install
uv tool install ast-mcp
Or run it without installing anything:
uvx ast-mcp --version
Requires Python 3.11 or newer.
Wire it into Claude Code
From the repository you want indexed:
ast-mcp init
That writes an ast-mcp stanza into <repo>/.mcp.json, adds .ast_mcp/ to
.gitignore, and builds the index. Claude Code reads .mcp.json at startup,
so the next session in that directory has the tools already connected — no
per-session step.
init merges. Every other server in .mcp.json is left exactly as it was,
and a file it cannot parse is reported rather than overwritten. Re-running it
is a no-op. Pass --dry-run to see the change first.
The stanza it writes carries no absolute paths, so it is safe to commit:
{
"mcpServers": {
"ast-mcp": { "command": "ast-mcp", "args": ["serve"] }
}
}
If ast-mcp is not permanently on PATH it records uvx ast-mcp serve
instead. Override either with --command "uv run ast-mcp serve".
CLI
| command | what it does |
|---|---|
ast-mcp init |
register in .mcp.json, ignore the index dir, build the index |
ast-mcp index [--rebuild] |
build or refresh the index; --rebuild discards it first |
ast-mcp status [--json] |
file/symbol counts, index size, freshness, registration |
ast-mcp languages [--group G] |
the language registry — 26 rows, their extensions and profiles |
ast-mcp serve |
the MCP server over stdio; what Claude Code launches |
Every command takes --root PATH. Root resolution is --root, else
AST_MCP_ROOT, else the working directory. Bare ast-mcp means ast-mcp serve.
Only init writes anything outside .ast_mcp/, and only .mcp.json and
.gitignore. The server itself never writes to your source.
Not every language gets the same treatment
A .py file has functions with signatures and docstrings. A
docker-compose.yml has none of that — it has a shape. A README.md has a
heading tree. Forcing all three through one symbol model produces garbage for
two of them, so there are four extraction profiles:
| profile | payload key | what you get | languages |
|---|---|---|---|
symbols |
symbols |
signature, docstring, nesting, imports | python, javascript, typescript, tsx, go, lua |
defs |
symbols |
same shape, weaker guarantees — docstrings often null |
ruby, perl, r, bash, zsh, css, scss, sql, graphql, proto, terraform, dockerfile |
schema |
schema |
key paths + inferred value types, not funcdefs | json, json5, yaml, toml, xml, csv |
outline |
outline |
heading / section tree | markdown, html |
26 languages across six groups: core, web, scripting, data, devops,
docs. Every response declares its profile and group before the
payload — read that field, don't assume symbols exists.
Kinds never lie about fidelity. A schema node's kind is a value type
(object, array, string); an outline node's kind is a document structure
(heading, code_block). Nothing outside the symbols/defs profiles ever
claims to be a function or a class.
Tools
| tool | use it for |
|---|---|
file_outline(path, max_depth, include_docstrings) |
the Read replacement — a file's shape, bodies elided |
get_symbol(name, path, mode) |
one definition. name is a symbol name, a key path (services.web.ports), or a heading slug |
search_symbols(query, kind, lang, group, path_glob, limit) |
find things by name across the repo |
get_docstrings(path | symbols) |
docs without bodies |
list_imports(path) |
dependency edges out of a file, plus exports where the language has them |
ast_query(path, query, captures) |
raw tree-sitter S-expression — the escape hatch |
Every tool takes max_tokens (default 4000). An over-budget response is
trimmed, flagged truncated: true, and tells you which argument narrows it.
Nothing is dropped silently.
get_symbol never guesses. A name matching several symbols returns
ambiguous: true with candidates; pass path or a qualified name to resolve.
When to use this vs CCE context_search
They answer different questions and both stay.
context_search(CCE) — fuzzy semantic retrieval over embedded chunks. Use for "how does auth work?", "where is rate limiting handled?", and anything where you know the concept but not the name.- AST_MCP — exact structural retrieval by name, kind and range. Use for
"show me
TokenStore.refresh", "what's in this config file?", "list everyCREATE TABLEin the repo", and anything where you know the name but not the location.
Rough rule: describing behaviour → context_search. Naming a thing →
AST_MCP.
What it costs
The saving grows with file size — the response envelope is fixed cost, so small files benefit least. Measured on this codebase:
| file | lines | full read | outline | saving |
|---|---|---|---|---|
ast_mcp/render.py |
94 | 789 tok | 352 tok | 2.2x |
ast_mcp/index.py |
428 | 3994 tok | 967 tok | 4.1x |
ast_mcp/tools.py |
457 | 3882 tok | 690 tok | 5.6x |
Below roughly 100 lines it is about break-even against Read. Above that it
pays, and get_symbol on a single definition pays regardless.
A 5000-row CSV or JSON array costs the same as a 3-row one: homogeneous
repeats collapse to one node carrying children_count.
Freshness
The SQLite index is a cache, never an oracle. Every path in a response was
stat-checked against its recorded (mtime_ns, size) during that same call,
and reparsed if it moved. There is no watcher daemon and no stale window.
Development
git clone https://github.com/matthew-brough/AST_MCP && cd AST_MCP
uv sync --all-groups
uv run --group dev pytest -q
The suite runs on 3.11 through 3.14. uv build produces the wheel; the
.scm query files ship inside it, and CI fails the build if any are missing.
SPEC.md is the contract: §V invariants, §I interfaces, §T tasks, §B the log
of what went wrong and what changed because of it.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ast_mcp-0.2.0.tar.gz.
File metadata
- Download URL: ast_mcp-0.2.0.tar.gz
- Upload date:
- Size: 98.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
87b8c310855f34cb0900ecba64c12956194989b14cde9aa808d9c0bd9f9cdf95
|
|
| MD5 |
aa35e01c90a2ac1c6ebd98ef5d788231
|
|
| BLAKE2b-256 |
c5910dfa54321b50b07e6261dac2560f761d8688a7db0b923d97d56269043f76
|
File details
Details for the file ast_mcp-0.2.0-py3-none-any.whl.
File metadata
- Download URL: ast_mcp-0.2.0-py3-none-any.whl
- Upload date:
- Size: 49.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9e66dc308ee9d5bfae3310ba7e29e25e0e774d3c09e83e4cf4f1d12f66ead626
|
|
| MD5 |
298145e8eba7d81875ff97204be34ce2
|
|
| BLAKE2b-256 |
2f1d6b585cd65f48e0e4ee88dd3a51dff89356b6c41298a87091059c1688f6b0
|