kambleakash0

AST Code Editor MCP Server

Community kambleakash0
Updated

AST Code Editor MCP Server

A robust, language-agnostic Model Context Protocol (MCP) server that provides AI coding agents with the ability to edit files surgically via Abstract Syntax Trees (AST) instead of relying on token-heavy, brittle search-and-replace or diff operations.

Why AST Edits?

Every non-AST edit format — search/replace, unified diff, whole-file rewrite — requires the model to copy text perfectly from a file it saw once. One whitespace mismatch on a 4,000-line file and the edit fails. AST edits sidestep the problem entirely: the model names the target (e.g., LRUCache.get) and provides the new code; the parser figures out where it lives.

Geometric AGI benchmarked every major format across 4 models and 29 edit tasks. AST edits were the only format to hit 100% correctness on 3 of 4 models, with 18x fewer output tokens than whole-file rewrite, and zero format failures. Full methodology and results: AST Edits: The Code Editing Format Nobody Uses.

Credits

This MCP server was inspired by research from Jack Foxabbott and the team at Geometric AGI. Their full findings, benchmark suite, and data are available here:

Estimated Token Savings

Per-edit output token savings versus other common edit formats:

Edit size File size vs whole-file rewrite vs unified diff vs search/replace
1-line tweak 100 LoC 3–5x ~1.5x ~1.5x
Function body rewrite 500 LoC 8–12x 2–3x 2–3x
Function body rewrite 4,000 LoC 15–20x 3–5x 3–5x
Add 2 lines to a function any size ~20x (via prepend_to_body / append_to_body) 5–10x 3–5x

Per-read input token savings versus reading the entire file:

Read task File size AST reader tool vs full file read
One function's source 500 LoC read_symbol ~20x fewer tokens
One function's source 2,000 LoC read_symbol ~50-100x fewer tokens
Class API (10 methods, no bodies) 500 LoC read_interface ~10x fewer tokens
Import block only any size read_imports ~20-50x fewer tokens
Structural overview (names + line numbers) any size list_symbols ~15-30x fewer tokens
One function's signature any size get_signature ~50-200x fewer tokens

For daily agent users, a realistic 40-60% reduction in total tokens per session is achievable, on average (combining output savings from surgical edits with input savings from targeted reads).

The savings come from four compounding effects:

  • Output tokens: Using prepend_to_body / append_to_body for small additions instead of rewriting whole function bodies
  • Input tokens: Using read_symbol / read_interface / read_imports to read only what's needed instead of entire files (~10-20x fewer input tokens per read)
  • Discovery: list_symbols / get_signature instead of reading whole files
  • Zero format failures: AST edits never fail on whitespace drift, eliminating retry loops that plague other formats.

Supported Languages & Capabilities

Language Extensions Structural edits Comments Docstrings Notes
Python .py # ✅ function/class Decorators preserved. Module-level dict/list literals editable via add_key / append_to_array.
JavaScript .js, .jsx, .mjs, .cjs // + /* ... */
TypeScript .ts, .tsx // + /* ... */ Interfaces are treated as classes for add_method / add_field.
C .c, .h // + /* ... */ (single + multi-line) .h defaults to C — use .hpp/.hxx/.hh for C++ headers.
C++ .cpp, .cc, .cxx, .hpp, .hxx, .hh // + /* ... */ (single + multi-line) Supports class, struct, union, enum, namespace; Class::method qualified names resolve correctly.
Ruby .rb # Classes, modules, instance methods, singleton_method (class methods via def self.foo). require/require_relative/load/autoload recognized as imports.
Go .go // + /* ... */ struct, interface, functions, methods. Methods are addressed by receiver: Cache.Get resolves to the top-level func (c *Cache) Get(...). Grouped import (...) blocks supported.
Java .java // + /* ... */ + Javadoc /** ... */ class, interface, enum, record; methods, constructors, fields. Annotations (@Override, @Deprecated) travel with their method on edits (wrapped in the modifiers node). Enum methods (nested in enum_body_declarations) discovered via BFS in list_symbols.
JSON .json ✅ (keys, values, arrays) — (no comment syntax)
YAML .yml, .yaml ✅ (keys, values, sequences) # Block and flow sequences supported.
TOML .toml ✅ (keys, values, arrays, tables) # [table] headers addressable by name for comment tools.

Cross-cutting features:

  • Decorated functions (Python @decorator): decorators are preserved on body/signature edits and included on delete.
  • Byte-correct slicing: multi-byte characters (emoji, , ) handled safely in source text.
  • Idempotent imports: add_import skips exact duplicates automatically. For Go specifically, when a parenthesized import ( ... ) block already exists, new specs are inserted inside the block rather than as a bare top-level line (which would be a syntax error for spec-only input like "path/filepath").
  • Doc-comment-aware deletion: delete_symbol by default removes the contiguous leading comment block above the symbol (Godoc, Javadoc, #/// comment runs) so docs don't become orphaned. Opt out with include_leading_comments=False.

Language-specific design decisions

A few tools have language-specific semantics where multiple reasonable interpretations exist. The chosen behavior is documented here for transparency:

add_field (Ruby and Go) — option (a): literal text passthrough

  • Ruby: add_field("LRUCache", " attr_accessor :capacity") inserts the literal string at the top of the class body. The tool does not auto-wrap bare names in attr_accessor — you provide the exact text you want (whether that's attr_accessor, attr_reader, @instance_var = nil in initialize, or CLASS_CONST = 42).
  • Go: add_field("Cache", "\tversion int") inserts the literal string inside the struct { ... } body. The tool does not infer types from bare names — you provide the full Go field declaration.
  • Rationale: consistent with how add_field works for other languages (Python, JS/TS, C++) where the caller provides the full source text. The alternative option (b) — auto-wrapping (e.g. attr_accessor :foo from the name foo) — would be more magical but harder to use for edge cases (typed fields, readonly fields, field with default value, etc.).

add_method (Go) — option (a): top-level sibling insertion

  • add_method("Cache", "func (c *Cache) Has(key string) bool { ... }") locates the type Cache struct { ... } declaration and inserts the new method immediately after it, at the top level (not inside the struct's braces).
  • Rationale: Go methods are lexically top-level, not nested inside their receiver type — this matches how Go code is actually written. The alternative option (b) — refusing because "Go methods aren't inside structs" — would be pedantically correct but force callers to use insert_after("Cache", content) instead, which loses the semantic signal that this is a method addition.

Tools Exposed

All tools require file_path to be an absolute path to an existing file.

Code editing — structural (Python, JS, TS, C, C++, Ruby, Go, Java)

Tool Parameters Description
replace_function file_path, target, content Replace a full function definition (signature + body + decorators).
replace_function_body file_path, target, content Replace only the body of a function, preserving signature and decorators.
replace_signature file_path, target, new_signature Replace only the signature, preserving body and decorators.
replace_in_body file_path, target, old_snippet, new_snippet Replace a byte-identical snippet inside a function body. Scoped to target's body; raises on multiple matches.
delete_in_body file_path, target, snippet Delete a byte-identical snippet inside a function body. Scoped to target's body; raises on multiple matches.
insert_in_body file_path, target, new_snippet, at | after | before Insert a snippet inside a function body. Pass exactly ONE of: at="top" (prepend), at="bottom" (append), after=<snippet> (anchored), before=<snippet> (anchored).
add_top_level file_path, content, position="bottom" Insert top-level content. position="bottom" appends at end of file (default); position="top" inserts after preamble (package/imports/includes/leading comments, plus Python module docstring) and before the first real declaration.
add_method file_path, class_target, content Add a method at the end of a class body.
add_field file_path, class_target, content Add a field/attribute/member at the top of a class body.
insert_sibling file_path, target, content, position Insert content as a sibling of a named symbol. position="before" or "after".
delete_symbol file_path, target, include_leading_comments=True Delete a function or class definition block (including decorators). By default also consumes the contiguous leading comment block above the symbol (Godoc, Javadoc /** ... */, # or // comments); pass include_leading_comments=False to keep it.

Parameters & signatures

Tool Parameters Description
add_parameter file_path, target, parameter, position Add a parameter to a function signature (position: "start" or "end").
remove_parameter file_path, target, parameter_name Remove a parameter by name.

Imports & includes

Tool Parameters Description
add_import file_path, import_text Add an import/from/#include line. Skips duplicates. For Go, if a parenthesized import ( ... ) block already exists, the spec is inserted inside that block (accepts either import "foo" or just "foo" / alias "foo" as input).
remove_import file_path, import_text Remove a matching import line.
add_import_name file_path, module, name Add one name to an existing named-import statement: from <module> import a, b (Python) or import { a, b } from "<module>" (JS/TS). Idempotent.
remove_import_name file_path, module, name Remove one name from a multi-name named-import statement (Python and JS/TS). If the last named import is removed and no default/namespace binding remains, the whole line is removed.

Comments & docstrings

Tool Parameters Description
edit_leading_comment file_path, target, op, comment="" Edit the contiguous leading-comment block above a named symbol. op="add" inserts; op="replace" replaces (or inserts if none); op="remove" deletes. Works for # / // / /* ... */ / Javadoc /** ... */.
replace_docstring file_path, target, new_docstring Replace or insert a Python function/class docstring. Python-only.

Dict/list editing (JSON, YAML, TOML, AND Python module-level dict/list literals)

Tool Parameters Description
replace_value file_path, target, content Replace the value of an existing config key. target is the dotted key path.
add_key file_path, parent_target, key, value Add a key-value pair to a dict/object/mapping/table. For Python, parent_target is the dict variable name; for config, a dotted path (use "" for root).
delete_key file_path, target Delete a key-value pair. Targets: JSON/YAML/TOML dotted path; Python DictName.keyExpr; JS/TS VarName.keyName on const/let/var or export const object literals (handles regular pairs, { key } shorthand, and quoted "complex-key"). For JSON and JS/TS, adjacent comma is also removed.
append_to_array file_path, target, value Append a literal value to a list/array/sequence. For Python, target is the list variable name; for config, a dotted path.
remove_from_array file_path, target, value_match Remove the first matching element from a list/array/sequence.

Navigation & reading (read-only)

Tool Parameters Description
list_symbols file_path Formatted outline of all top-level functions, classes, and methods with line numbers.
find_references file_path, target Syntactic search for all occurrences of an identifier (no scope awareness).
read_symbol file_path, target, depth="full" Return source text of a single named symbol. depth controls how much: "full" returns the entire source (typically 10-20x fewer tokens than the whole file); "interface" returns a class stub (header + fields + method sigs with ...) or a function's signature; "signature" returns signature-only.
read_imports file_path Return all import/include statements in the file.

Target format: Use the exact function name (e.g., get) or dotted Class.method path (e.g., LRUCache.get). Decorated Python functions are fully supported — decorators are preserved when replacing bodies or signatures, and included when deleting or replacing the full function.

Tip: Call list_symbols first to discover exact target names before editing. This avoids guessing and makes subsequent edits much more reliable.

Which tool should I use?

A decision guide grouped by intent. Start at the top and pick the narrowest match.

Discovering what's in a file (do this first)

  • Don't know what symbols exist?list_symbols
  • Need one specific function's full source?read_symbol (depth="full", the default — 10-20x cheaper than reading the whole file)
  • Need a class's public API (methods + fields, no bodies)?read_symbol(target, depth="interface")
  • Need just a function's signature?read_symbol(target, depth="signature")
  • Need to see a file's imports/dependencies?read_imports
  • Where is a symbol used?find_references

Dotted targets descend into closures: Go stdioCmd.RunE (func_literal in struct field), TS app.handler (arrow function in object literal).

Adding new content

Intent Tool
New top-level function, class, constant, or type alias add_top_level (use position="top" to prepend after preamble)
New method in an existing class add_method
New field/attribute/member in a class add_field
New content before or after a top-level symbol insert_sibling(position="before" | "after")
New lines at the top of an existing function body insert_in_body(at="top")
New lines at the bottom of an existing function body insert_in_body(at="bottom")
New lines at a specific spot inside a function body (anchored to existing text) insert_in_body(after=…) or insert_in_body(before=…)
New parameter on an existing function add_parameter
New import or #include add_import
New name in an existing from X import … or import { a, b } from "mod" add_import_name
New comment above a symbol edit_leading_comment(op="add")
New Python docstring on a function/class replace_docstring
New key in a dict/object/mapping/table (any lang) add_key
New item in a list/array/sequence (any lang) append_to_array

Modifying existing content

Intent Tool
Rewrite the full function (signature + body) replace_function
Rewrite only the body, keep the signature replace_function_body
Change one statement/block inside a large body replace_in_body (scoped snippet match)
Change only the signature, keep the body replace_signature
Change only the leading comment above a symbol edit_leading_comment(op="replace")
Change only the Python docstring replace_docstring
Change the value of an existing config key replace_value

Removing content

Intent Tool
Remove a function, method, or class delete_symbol (consumes leading doc comment by default)
Remove one statement/line inside a function body delete_in_body
Remove a parameter from a function remove_parameter
Remove an import or #include remove_import
Remove one name from a multi-name named-import (Python or JS/TS) remove_import_name
Remove a leading comment above a symbol edit_leading_comment(op="remove")
Remove a key from a dict / config / JS-TS object literal delete_key
Remove an item from a list/array remove_from_array

Anti-patterns to avoid

  • Don't use replace_function or replace_function_body to change a few lines — use replace_in_body (scoped snippet match) or insert_in_body(at="top" \| "bottom") for appending/prepending. Rewriting the whole function is wasteful and error-prone.
  • Don't use replace_signature to add or remove one parameter — use add_parameter/remove_parameter.
  • Don't use replace_value to add a new key — use add_key. replace_value only updates existing keys.
  • Don't use add_import to add a name to an existing from X import … or named import — use add_import_name.
  • Don't guess at target names. Call list_symbols first. Names are case-sensitive and must match exactly.

Logging & Debugging

The server logs all tool invocations and errors to stderr (safe for stdio transport — does not interfere with JSON-RPC). Logs include timestamps and severity levels.

To inspect logs when running under Claude Desktop, check ~/Library/Logs/Claude/mcp*.log (macOS) or %APPDATA%\Claude\logs\mcp*.log (Windows).

For interactive testing, use the MCP Inspector.

Prerequisites

This MCP server uses uv to manage its Python environment and dependencies automatically. Install uv if you don't have it already:

macOS / Linux:

curl -LsSf https://astral.sh/uv/install.sh | sh

Windows (PowerShell):

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

Homebrew (macOS):

brew install uv

pip (any platform):

pip install uv

Verify the installation:

uv --version

For more options (Docker, Cargo, WinGet, etc.), see the official uv installation docs.

Installation

Note: Replace /absolute/path/to below with the actual path to this repository on your machine.

Method 1: CLI Configuration (Claude Code, Codex, Gemini)

If your agent supports adding servers via CLI, run the following:

Claude Code / Codex CLI / Gemini CLI:

--scope user installs the server globally so it's available in every project on your machine. Drop it if you only want the server active in the current project.

# Claude Code / Codex
[claude|codex] mcp add ast-editor --scope user -- uv --directory /absolute/path/to/ast-editor run ast-editor-mcp

# Gemini CLI
gemini mcp add --transport stdio --scope user ast-editor -- uv --directory /absolute/path/to/ast-editor run ast-editor-mcp

Method 2: JSON Configuration

For tools that use a mcp_config.json or settings.json file, add the following block to the appropriate file path.

Important: Use the absolute path to uv for "command", not just "uv". GUI-based MCP clients (Claude Desktop, Cursor, Antigravity) don't always inherit your shell PATH, so a bare "uv" will fail with a "command not found" error. Get your absolute path with:

which uv
# e.g. /Users/you/.local/bin/uv  or  /opt/homebrew/bin/uv
{
  "mcpServers": {
    "ast-editor": {
      "command": "/absolute/path/to/uv",
      "args": [
        "--directory",
        "/absolute/path/to/ast-editor",
        "run",
        "ast-editor-mcp"
      ]
    }
  }
}
Agent Configuration File Path
Claude Desktop ~/Library/Application Support/Claude/claude_desktop_config.json (macOS); %APPDATA%\Claude\claude_desktop_config.json (Windows)
Cursor .cursor/mcp.json (Project) or ~/.cursor/mcp.json (Global)
Windsurf Agent Panel → "..." → MCP Servers → View raw config
Antigravity ~/.gemini/antigravity/mcp_config.json (or via Agent Panel)
Gemini CLI ~/.gemini/settings.json (Global) or .gemini/settings.json (Project)

Using Standard Python (Fallback)

If you prefer not to use uv, install manually and point to the .venv executable in ast-editor directory:

python3 -m venv .venv && source .venv/bin/activate && pip install .
{
  "mcpServers": {
    "ast-editor": {
      "command": "/absolute/path/to/ast-editor/.venv/bin/python",
      "args": ["-m", "ast_editor.server"]
    }
  }
}

Agent Configuration (Important)

Coding agents are heavily biased toward their default tools. You must explicitly instruct them to use AST tools. The agent prompt lives in AST-EDITOR.md — a standalone file you wire into your agent's system instructions.

Claude Code / Claude Desktop (via @-include)

Claude Code supports @filename includes in CLAUDE.md. Copy the prompt file into your global config directory and add one include line:

cp /absolute/path/to/ast-editor/AST-EDITOR.md ~/.claude/
echo '@AST-EDITOR.md' >> ~/.claude/CLAUDE.md

Or for a single project, place it next to the project's CLAUDE.md and add @AST-EDITOR.md there.

Other agents (Cursor, Codex CLI, Windsurf, Antigravity, Aider, Gemini CLI, etc.)

Copy the contents of AST-EDITOR.md into your agent's instruction file (AGENTS.md, .cursor/rules/*.mdc, .windsurfrules, .github/copilot-instructions.md, system prompt, etc.). Most non-Claude agents don't support @-include — paste the prompt body directly.

Agent Instruction file
Any agent that reads AGENTS.md (Codex CLI, Windsurf, Zed, Cursor secondary) AGENTS.md at repo root
Cursor .cursor/rules/*.mdc (current) — legacy: .cursorrules
GitHub Copilot .github/copilot-instructions.md
Windsurf .windsurfrules or AGENTS.md
Antigravity _agents/rules/
Aider / Gemini CLI / generic Rules file or system prompt

Migrating from v1.x / pre-AST-EDITOR.md instructions

If your CLAUDE.md (or other rules file) contains an inline quoted "When editing ... use ast-editor" block from an older README version, delete that block and replace it with the @AST-EDITOR.md include (or paste the current file's contents). The old block will reference tool names removed in v2.0.0 consolidation (prepend_to_body, append_to_body, insert_before, insert_after, add_comment_before, replace_leading_comment, remove_leading_comment, read_interface, get_signature) — keeping it will cause agents to call tools that no longer exist.

v2.0.0 consolidated 10 closely-related tools into 4 parametrized tools. Mapping:

v1.x tool v2.0.0 equivalent
add_comment_before(target, comment) edit_leading_comment(target, op="add", comment=...)
replace_leading_comment(target, new_comment) edit_leading_comment(target, op="replace", comment=...)
remove_leading_comment(target) edit_leading_comment(target, op="remove")
read_symbol(target) read_symbol(target) (or explicit depth="full")
read_interface(target) read_symbol(target, depth="interface")
get_signature(target) read_symbol(target, depth="signature")
prepend_to_body(target, content) insert_in_body(target, content, at="top")
append_to_body(target, content) insert_in_body(target, content, at="bottom")
insert_before(target, content) insert_sibling(target, content, position="before")
insert_after(target, content) insert_sibling(target, content, position="after")

The old tools are hard-removed — calling them will fail with "unknown tool". Behavior is preserved 1:1 by the new calls.

MCP Server · Populars

MCP Server · New