MCP server for deep structural analysis of Python source via the stdlib ast module
Project description
py-ast-mcp
An MCP (Model Context Protocol) server for deep structural analysis of Python source code.
It is the Python counterpart to ts-ast-mcp and mirrors its 20-tool surface as closely as Python semantics allow. Everything is built on the standard library ast module — no compilation, no type checker, no project configuration required. jedi is an optional extra used only for cross-file semantic resolution, and every tool degrades gracefully when it is not installed.
Unlike grep, this server understands actual structure: function signatures, class hierarchies, call relationships, cyclomatic complexity and unreferenced code.
Features
- Real AST, not regex — signatures with annotations and defaults, decorators, async flags, positional-only and keyword-only parameters, nested definitions.
- Python-aware classification — dataclasses, enums,
Protocol,TypedDict,NamedTuple, ABCs, exceptions andTypeAliasare recognised as distinct kinds. - Call graphs as Mermaid — file-scoped or package-scoped flowcharts, rooted at a function, with optional external call edges.
- Quality tooling — cyclomatic complexity with ranks, scored to match
radon/mccabe, code smells, and Python-specific hazards (mutable default arguments, bareexcept, late-binding loop closures, unawaited coroutines,assertused for validation). - Dead code across a directory — unreferenced private and module-level symbols, with a confidence split between private and public names.
- Protocol conformance — finds implementations both explicitly (base class, including indirect subclasses) and structurally (method-set match).
- Docstring parsing — Google and NumPy styles are split into summary / params / returns / raises.
- Structural diffing — signature-level, not text-level: what actually changed in the API.
- Model-friendly output — dense, scannable plain text with line numbers everywhere, not raw JSON dumps.
- Never crashes on bad input — syntax errors come back as a readable error with line and column, flagged
isErrorso a client can tell failure from analysis; the server stays up. - Parse caching — modules are cached by path + mtime + size, so repeated tool calls in one turn are cheap.
Installation
Requires Python 3.10+. Prefer the newest Python you have installed: the server can only parse syntax its own interpreter understands (see Known limitations).
git clone <this-repo> py-ast-mcp
cd py-ast-mcp
pip install -e .
# optional: cross-file semantic resolution
pip install -e ".[semantic]"
# development (pytest + jedi)
pip install -e ".[dev]"
This installs a py-ast-mcp console script that runs the stdio server. python -m py_ast_mcp works too.
Tools
All path arguments accept absolute paths or paths relative to the server's working directory.
Structural
| Tool | Parameters | Description |
|---|---|---|
analyze_file |
path |
High-level summary of every symbol: classes (with dataclass/enum/Protocol/TypedDict classification), functions, async functions, methods and module-level assignments. |
list_functions |
path |
All functions and methods with full signatures (annotations, defaults, return type, decorators, async flag) and line ranges. |
get_function_body |
path, name |
Full numbered source of a function or method. Supports Class.method and dotted nested paths; falls back to base classes in the same file. |
list_methods |
path, type |
All methods of a class: declared, properties, class/static methods, class attributes, and members inherited from base classes defined in the same file. |
get_type_definition |
path, name |
Extract a class / TypeAlias / Enum / Protocol / TypedDict / NamedTuple definition with its members and source. |
list_declarations |
path |
Module-level assignments with annotated or inferred types. |
list_exports |
path |
Public API: respects __all__ when present, otherwise non-underscore module-level names; flags re-exported imports and names in __all__ that are not defined. |
list_imports |
path |
All imports with bound name, module path, relative-import level and aliases, grouped into stdlib / third-party / relative. |
find_usages |
path, identifier, context (default 1) |
Every occurrence with surrounding source lines: reads, assignments, parameters, attribute access, imports, global/nonlocal. Adds project-wide references when jedi is installed. |
Call analysis
| Tool | Parameters | Description |
|---|---|---|
call_graph |
path, function, direction (TD/TB/LR/RL/BT, default TD), include_external (default false), scope (file/package, default file) |
Mermaid flowchart of call relationships. function roots the graph at one function; scope="package" walks every .py file in the containing directory. Also lists edges in text form and functions with no edges. |
get_callers |
path, function, scope (file/package, default file) |
Reverse call graph: direct callers with call sites, transitive callers, and the entry points that reach the function. |
Quality
| Tool | Parameters | Description |
|---|---|---|
code_complexity |
path, function |
Cyclomatic complexity per function with an A–F rank. Counts if/elif, for, while, for/try else, except, comprehensions and their if clauses, boolean operators, ternaries and match cases. with and assert are not counted (neither branches), and nested defs are scored separately rather than folded into the parent — so scores match radon cc --no-assert exactly. Pass function for a decision-point breakdown. |
code_smells |
path, function |
Long functions, deep nesting, god classes, too many parameters, mutable default arguments, bare except:, shadowed builtins, high complexity. Grouped by severity with a suggested fix. |
find_errors |
path, function |
Python-specific hazards: bare/broad except, except: pass, mutable default args, unawaited coroutine calls (best effort), assert used for runtime validation, late-binding closures over loop variables, ==/!= against None/True/False, methods that never use self. |
dead_code |
path, include_tests (default false) |
Unreferenced private and module-level symbols across a directory. If path is a file, its containing directory is scanned so cross-file references are seen. |
find_implementations |
path, interface |
Classes implementing a Protocol/ABC — explicit (direct or indirect base class) and structural (method-set match), plus near misses. The argument is interface, not protocol, so the same call works against ts-ast-mcp. |
Docs & multi-file
| Tool | Parameters | Description |
|---|---|---|
get_doc |
path, name |
Docstring extraction for a function, Class.method, class, module (name="module") or documented module constant. Google and NumPy styles are parsed into summary / description / params / returns / yields / raises. |
analyze_package |
path, include_tests (default false) |
Directory-level summary of every .py file: line counts, class/function counts, docstrings, per-file symbol lists, and any unparseable files. |
diff_ast |
old_path, new_path |
Structural diff: added / removed / modified functions, methods, classes, base classes, decorators, module variables, imports and __all__. Signature-level, with a "potentially breaking" summary. |
find_node_at_position |
path, line (1-based), column (0-based) |
The AST node at a cursor position, the full node chain, and the enclosing scope chain. Adds jedi-resolved definitions when available. |
Configuration
Claude Code
Add a .mcp.json at the root of your project (this file is picked up automatically):
{
"mcpServers": {
"py-ast": {
"command": "py-ast-mcp",
"args": []
}
}
}
If you installed into a virtualenv, point at it explicitly so the server does not depend on your shell's PATH:
{
"mcpServers": {
"py-ast": {
"command": "/absolute/path/to/venv/bin/python",
"args": ["-m", "py_ast_mcp"]
}
}
}
Or run it straight from a checkout without installing, using uv:
{
"mcpServers": {
"py-ast": {
"command": "uvx",
"args": ["--from", "/absolute/path/to/py-ast-mcp", "py-ast-mcp"]
}
}
}
You can also register it from the CLI:
claude mcp add py-ast -- py-ast-mcp
Claude Desktop
Edit claude_desktop_config.json:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json
{
"mcpServers": {
"py-ast": {
"command": "py-ast-mcp",
"args": []
}
}
}
Because Claude Desktop does not inherit your shell environment, an absolute path is usually safer:
{
"mcpServers": {
"py-ast": {
"command": "/absolute/path/to/venv/bin/py-ast-mcp",
"args": []
}
}
}
Restart Claude Desktop after editing the file.
Straight from GitHub
No checkout, no install - uv fetches and runs it:
{
"mcpServers": {
"py-ast": {
"command": "uvx",
"args": ["--from", "git+https://github.com/gclluch/py-ast-mcp", "py-ast-mcp"]
}
}
}
Add --with jedi to the args for cross-file semantic resolution.
Example output
call_graph on a small package:
# call_graph (package) samplepkg/core.py [16 functions, 11 edges]
## mermaid
```mermaid
flowchart TD
n2_core_Engine_store["core.Engine.store L57"]
n9_util_normalize["util.normalize L4"]
n2_core_Engine_store --> n9_util_normalize
edges
- Engine.store -> validate @L58
- Engine.store -> normalize @L59
- run_pipeline -> Engine.store @L70
`code_complexity` on the standard library's `argparse`:
complexity /usr/lib/python3.11/argparse.py [138 functions, module total 350]
average 3.7 max 46 functions over 10: 10
function lines cc rank len depth
ArgumentParser._parse_known_args L1930-2187 46 F 258 6 HelpFormatter._format_actions_usage L406-519 28 D 114 5
## Development
```bash
pip install -e ".[dev]"
# unit tests
pytest
# end-to-end: spawn the real server and drive it over stdio with a
# handwritten JSON-RPC client (initialize -> tools/list -> tools/call)
python scripts/stdio_smoke_test.py
Layout:
src/py_ast_mcp/
server.py MCP tool registration (FastMCP, stdio transport)
parse.py shared parse + cache by path/mtime/size, AST navigation
format.py shared output formatting
analyze.py analyze_file, analyze_package, find_node_at_position
functions.py list_functions, get_function_body, list_methods
types.py get_type_definition, list_declarations
imports.py list_imports, list_exports
usages.py find_usages
callgraph.py call_graph, get_callers
complexity.py code_complexity
smells.py code_smells
errors.py find_errors
deadcode.py dead_code
protocols.py find_implementations
doc.py get_doc
diff.py diff_ast
jedi_support.py optional cross-file resolution
Known limitations
These are heuristics over a syntax tree, not a type checker:
- The server parses with its own interpreter's grammar.
astcan only read syntax the running Python understands, so a server on 3.11 reportsPEP 695code (type X = ...,def f[T]()) as a syntax error even though the file is valid. Run the server on the newest Python you have, regardless of what the target project targets — parse errors say so when the interpreter may be the cause. - Call resolution is name-based.
obj.method()is matched by method name; when several classes in scope define the same name the first one wins.self.method()resolves within the enclosing class and then across classes in the file. - Cross-module call edges in
scope="package"are resolved throughimportstatements only. Dynamic dispatch, factories and callbacks are not followed. dead_codematches by name, not by scope.getattr, plugin registries, entry points and re-exports from outside the scan produce false positives; public symbols are reported separately as lower confidence. The larger risk is the other direction: any attribute access or string literal sharing a symbol's name marks it live, so the tool under-reports. Treat hits as candidates to confirm.jediresolution is scoped to the detected project root, found by walking up for.git/setup.py/requirements.txt. Files outside that root are invisible tofind_usages' cross-file section.unawaited-coroutineis best effort. It flags calls toasync deffunctions declared in the same file that are neither awaited nor wrapped in a recognisedasynciohelper.- Inherited members are only resolved for base classes defined in the same file; bases from other modules are listed as unresolved.
list_declarationstype inference is literal-shaped, not a real inference engine: it reports what a reader would infer from the right-hand side.
License
MIT
Project details
Release history Release notifications | RSS feed
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 py_ast_mcp-0.1.0.tar.gz.
File metadata
- Download URL: py_ast_mcp-0.1.0.tar.gz
- Upload date:
- Size: 63.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fd51ea1caa43c5e4747660f55f7fa9de79f3411ebf5ef970d5cd48a01b097d38
|
|
| MD5 |
2f8a6537975bb57959bdb8ff354b5d9d
|
|
| BLAKE2b-256 |
7e26f986e0ae068a7f9651e5b2d800cb8fc8bebcfafb1515516696570c9d6894
|
Provenance
The following attestation bundles were made for py_ast_mcp-0.1.0.tar.gz:
Publisher:
release.yml on gclluch/py-ast-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
py_ast_mcp-0.1.0.tar.gz -
Subject digest:
fd51ea1caa43c5e4747660f55f7fa9de79f3411ebf5ef970d5cd48a01b097d38 - Sigstore transparency entry: 2314866055
- Sigstore integration time:
-
Permalink:
gclluch/py-ast-mcp@7bcfb940bc228bbbc05d238a8d9cf0539aedc4d4 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/gclluch
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7bcfb940bc228bbbc05d238a8d9cf0539aedc4d4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file py_ast_mcp-0.1.0-py3-none-any.whl.
File metadata
- Download URL: py_ast_mcp-0.1.0-py3-none-any.whl
- Upload date:
- Size: 58.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
144f9430d5c6a7c6c5ddc7f1f4d0d447bc893aab3f99556629e0b1dd580f9cca
|
|
| MD5 |
f99e4a49d281ec241c9f5d505d43a15a
|
|
| BLAKE2b-256 |
876b2b765a5dfca32adb4ef9e604c123d043f7683394ab592074f91619186144
|
Provenance
The following attestation bundles were made for py_ast_mcp-0.1.0-py3-none-any.whl:
Publisher:
release.yml on gclluch/py-ast-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
py_ast_mcp-0.1.0-py3-none-any.whl -
Subject digest:
144f9430d5c6a7c6c5ddc7f1f4d0d447bc893aab3f99556629e0b1dd580f9cca - Sigstore transparency entry: 2314866066
- Sigstore integration time:
-
Permalink:
gclluch/py-ast-mcp@7bcfb940bc228bbbc05d238a8d9cf0539aedc4d4 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/gclluch
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7bcfb940bc228bbbc05d238a8d9cf0539aedc4d4 -
Trigger Event:
push
-
Statement type: