Skip to main content
Glama
py-ast-mcp
by gclluch

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 and TypeAlias are 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 @dataclass field defaults, bare except, unreachable except clauses, late-binding closures, is against a literal, unawaited coroutines, assert used 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 isError so 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

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 other than the irrefutable one (case _:, or a bare capture — it is the fall-through, not a branch). 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, mutable @dataclass field defaults, 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, except clauses made unreachable by an earlier one, mutable default args, mutable @dataclass field defaults (an import-time ValueError), unawaited coroutine calls (best effort), assert used for runtime validation, late-binding closures over a loop or comprehension variable, ==/!= against None/True/False, is against a literal (mirrors CPython's own SyntaxWarning), 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. ast can only read syntax the running Python understands, so a server on 3.11 reports PEP 695 code (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 through import statements only. Dynamic dispatch, factories and callbacks are not followed.

  • dead_code matches 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.

  • jedi resolution is scoped to the detected project root, found by walking up for .git / setup.py / requirements.txt. Files outside that root are invisible to find_usages' cross-file section.

  • unawaited-coroutine is best effort. It flags calls to async def functions declared in the same file that are neither awaited nor wrapped in a recognised asyncio helper.

  • Inherited members are only resolved for base classes defined in the same file; bases from other modules are listed as unresolved.

  • list_declarations type 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 tools
analyze_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

    A4/5.0

    Scored across 20 tools

    Disambiguation4/5

    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.

    Naming Consistency5/5

    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.

    Tool Count4/5

    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.

    Completeness5/5

    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

    ActivityInactive
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables deterministic static analysis of Python code, providing tools to inspect classes, functions, imports, dependencies, and more, without executing the code.
      1
      MIT
    • A
      license
      D
      quality
      D
      maintenance
      Provides deterministic Python codebase symbol exploration using AST parsing, with tools for definition lookup, signatures, references, call chains, and class hierarchy queries.
      6
      MIT
    • F
      license
      Not graded
      quality
      B
      maintenance
      Enables 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
      -
    • F
      license
      Not graded
      quality
      Not graded
      maintenance
      Enables 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.
      -