py-ast-mcp
Provides deep structural analysis of Python source code, including function/class extraction, signatures, call graphs, complexity metrics, code smells, dead code detection, protocol conformance, docstring parsing, and structural diffing.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@py-ast-mcp Show me the call graph for calculate_total in app.py"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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, mutable@dataclassfield defaults, bareexcept, unreachableexceptclauses, late-binding closures,isagainst a literal, 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.
Related MCP server: mcp-ast-explorer
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 |
|
| High-level summary of every symbol: classes (with dataclass/enum/Protocol/TypedDict classification), functions, async functions, methods and module-level assignments. |
|
| All functions and methods with full signatures (annotations, defaults, return type, decorators, async flag) and line ranges. |
|
| Full numbered source of a function or method. Supports |
|
| All methods of a class: declared, properties, class/static methods, class attributes, and members inherited from base classes defined in the same file. |
|
| Extract a class / |
|
| Module-level assignments with annotated or inferred types. |
|
| Public API: respects |
|
| All imports with bound name, module path, relative-import level and aliases, grouped into stdlib / third-party / relative. |
|
| Every occurrence with surrounding source lines: reads, assignments, parameters, attribute access, imports, |
Call analysis
Tool | Parameters | Description |
|
| Mermaid flowchart of call relationships. |
|
| Reverse call graph: direct callers with call sites, transitive callers, and the entry points that reach the function. |
Quality
Tool | Parameters | Description |
|
| Cyclomatic complexity per function with an A–F rank. Counts |
|
| Long functions, deep nesting, god classes, too many parameters, mutable default arguments, mutable |
|
| Python-specific hazards: bare/broad |
|
| Unreferenced private and module-level symbols across a directory. If |
|
| Classes implementing a Protocol/ABC — explicit (direct or indirect base class) and structural (method-set match), plus near misses. The argument is |
Docs & multi-file
Tool | Parameters | Description |
|
| Docstring extraction for a function, |
|
| Directory-level summary of every |
|
| Structural diff: added / removed / modified functions, methods, classes, base classes, decorators, module variables, imports and |
|
| 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-mcpClaude Desktop
Edit claude_desktop_config.json:
macOS:
~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:
%APPDATA%\Claude\claude_desktop_config.jsonLinux:
~/.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_normalizeedges
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.pyLayout:
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 resolutionKnown 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
Available Tools
20 toolsanalyze_fileA
High-level summary of every symbol in a Python file.
Lists classes (with dataclass/enum/Protocol/TypedDict classification), functions, async functions, methods and module-level assignments with line numbers. Start here when exploring an unfamiliar module.
Args: path: Path to a .py file.
Inputs: path (required). Returns: result
analyze_packageA
Directory-level summary of every .py file: sizes, class/function counts, docstrings and per-file symbol lists.
Args: path: Directory to analyse. include_tests: Include test files (default False).
Inputs: path (required), include_tests. Returns: result
call_graphA
Emit a Mermaid flowchart of the call relationships in a file or package.
Args: path: Path to a .py file. function: Optional root; only calls reachable from it are drawn. direction: Mermaid layout direction: TD, TB, LR, RL or BT. include_external: Include calls to names not defined in scope. scope: "file" (default) or "package" to walk every .py file in the containing directory.
Inputs: path (required), scope, function, direction, +1 more. Returns: result
code_complexityA
Cyclomatic complexity per function, with rank and decision-point breakdown.
Counts if/elif, for, while, except, comprehension fors and ifs, boolean
operators, ternaries, and match cases other than the irrefutable one.
Scores match radon cc --no-assert.
with and assert are deliberately not counted: neither branches. with
still adds nesting depth.
Args: path: Path to a .py file. function: Optional single function to analyse in detail.
Inputs: path (required), function. Returns: result
code_smellsA
Detect long functions, deep nesting, god classes, too many parameters,
mutable default arguments, mutable @dataclass field defaults, high
complexity, bare excepts and shadowed builtins.
Args: path: Path to a .py file. function: Optional single function to analyse.
Inputs: path (required), function. Returns: result
dead_codeA
Find unreferenced private and module-level symbols across a directory.
If path is a file, its containing directory is scanned so that cross-file
references are seen.
Args: path: A .py file or a directory. include_tests: Also scan test files (default False).
Inputs: path (required), include_tests. Returns: result
diff_astA
Structural diff between two Python files: added, removed and modified
functions, classes, methods, module variables, imports and __all__.
Signature-level, not text-level.
Args: old_path: Path to the original file. new_path: Path to the updated file.
Inputs: new_path (required), old_path (required). Returns: result
find_errorsA
Find Python-specific hazards: bare/broad except, except: pass, handlers
made unreachable by an earlier one, mutable default args, mutable
@dataclass field defaults (an import-time ValueError), unawaited
coroutines (best effort), assert used for runtime validation,
late-binding closures over a loop or comprehension variable, == against
None/True/False, is against a literal, and methods that never use self.
Args: path: Path to a .py file. function: Optional single function to analyse.
Inputs: path (required), function. Returns: result
find_implementationsA
Find classes implementing a Protocol or ABC, both explicitly (as a base class, including indirect subclasses) and structurally (method-set match).
Args:
path: A .py file or directory; the containing directory is scanned.
interface: Name of the Protocol/ABC/base class. Named interface
rather than protocol to match the identical tool in the
TypeScript server, so one vocabulary works across both.
Inputs: path (required), interface (required). Returns: result
find_node_at_positionA
Identify the AST node at a cursor position with its enclosing scope chain.
Args: path: Path to a .py file. line: 1-based line number. column: 0-based column offset.
Inputs: line (required), path (required), column (required). Returns: result
find_usagesA
Find every occurrence of an identifier with surrounding source lines.
Covers reads, assignments, parameters, attribute access, imports and
global/nonlocal declarations. Always reports a project-wide references
section: either the cross-file hits, or that there are none, or that jedi
is not installed and cross-file references were therefore not checked. An
empty result and an unchecked one are not the same answer.
Args: path: Path to a .py file. identifier: Name to search for. context: Lines of context to show around each hit (default 1).
Inputs: path (required), identifier (required), context. Returns: result
get_callersA
Reverse call graph: who calls this function, directly and transitively.
Args:
path: Path to a .py file.
function: Function name or Class.method.
scope: "file" (default) or "package".
Inputs: path (required), function (required), scope. Returns: result
get_docA
Extract a docstring and parse it into summary/params/returns/raises when it follows Google or NumPy style.
Args:
path: Path to a .py file.
name: Symbol name, Class.method, or "module" for the module docstring.
Inputs: name (required), path (required). Returns: result
get_function_bodyA
Extract the full source of a function or method, with line numbers.
Args:
path: Path to a .py file.
name: Function name, Class.method, or a dotted nested path.
Inputs: name (required), path (required). Returns: result
get_type_definitionA
Extract a class, TypeAlias, Enum, Protocol, TypedDict or NamedTuple definition with its members and source.
Args: path: Path to a .py file. name: Type name.
Inputs: name (required), path (required). Returns: result
list_declarationsA
List module-level assignments with annotated or inferred types.
Args: path: Path to a .py file.
Inputs: path (required). Returns: result
list_exportsA
List the public symbols of a module.
Respects __all__ when present, otherwise reports non-underscore
module-level names, and flags re-exported imports.
Args: path: Path to a .py file.
Inputs: path (required). Returns: result
list_functionsA
List all functions and methods with full signatures and line ranges.
Includes parameter annotations and defaults, return annotation, decorators, async flag, and nested function relationships.
Args: path: Path to a .py file.
Inputs: path (required). Returns: result
list_importsB
List all imports: bound name, module path, relative level and aliases, grouped into stdlib / third-party / relative.
Args: path: Path to a .py file.
Inputs: path (required). Returns: result
list_methodsA
List every method of a class: declared, inherited, properties, class and static methods, plus class-level attributes.
Inherited members are only resolved for base classes defined in the same file; bases from other modules are reported as unresolved.
Args: path: Path to a .py file. type: Class name.
Inputs: path (required), type (required). Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 20 tools
Most tools have clearly distinct purposes, but some overlap exists: `analyze_file` vs `list_functions` vs `list_declarations` all surface symbol information, and `code_smells` vs `find_errors` both detect code issues. Descriptions are detailed enough to disambiguate, but an agent might occasionally select the wrong one.
All tool names are snake_case and follow a consistent verb-first pattern (list_*, get_*, find_*, analyze_*). A few noun-phrase names like `code_smells`, `call_graph`, and `dead_code` are exceptions, but they are still readable and fit the overall style without introducing inconsistency.
With 20 tools, the server is on the heavier side of the ideal range but stays within reason for a Python AST analysis domain. Each tool covers a distinct aspect (symbols, calls, complexity, errors, docstrings, diff), so no tool feels redundant; however, the count is slightly above the sweet spot.
The tool surface is comprehensive for static Python analysis: it covers declarations, exports, imports, functions, methods, types, usages, call graphs, complexity, error detection, dead code, implementations, docstrings, package summaries, AST diffs, and node lookup. There are no obvious dead ends for typical analysis workflows.
Maintenance
Related MCP Connectors
AI-powered codebase analysis — call graphs, security, dead code, complexity. 150+ tools.
Code intelligence platform for AI agents. 20 tools for architecture, security & impact analysis.
Evidence-backed architecture-quality analysis for Python agent applications.
Security + bug + perf + refactor audit for Python. Returns 0-10 score + MD report.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables deterministic static analysis of Python code, providing tools to inspect classes, functions, imports, dependencies, and more, without executing the code.1MIT
- AlicenseDqualityDmaintenanceProvides deterministic Python codebase symbol exploration using AST parsing, with tools for definition lookup, signatures, references, call chains, and class hierarchy queries.6MIT
- FlicenseNot gradedqualityBmaintenanceEnables LLMs to analyze Python repositories structurally via AST and call graphs, providing code search, symbol lookup, change-impact analysis, workflow tracing, and explanation without embeddings.1-
- FlicenseNot gradedqualityNot gradedmaintenanceEnables coding agents to run deterministic, zero-dependency AST static analysis over source code, building call graphs and dependency metrics to find orphaned functions, unreachable methods, breaking API surface diffs, and token-bloat clusters, then return a structured refactoring plan. It plugs into MCP clients like Claude Desktop, Cursor, and Windsurf via a single Python entrypoint.-