ast-editor
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:
Jack Foxabbott's original post (LinkedIn)
GeometricAGI/blog (Benchmark code & data)
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 | 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 |
| ~20x fewer tokens |
One function's source | 2,000 LoC |
| ~50-100x fewer tokens |
Class API (10 methods, no bodies) | 500 LoC |
| ~10x fewer tokens |
Import block only | any size |
| ~20-50x fewer tokens |
Structural overview (names + line numbers) | any size |
| ~15-30x fewer tokens |
One function's signature | any size |
| ~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_bodyfor small additions instead of rewriting whole function bodiesInput tokens: Using
read_symbol/read_interface/read_importsto read only what's needed instead of entire files (~10-20x fewer input tokens per read)Discovery:
list_symbols/get_signatureinstead of reading whole filesZero 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 |
| ✅ | ✅ | ✅ function/class | Decorators preserved. Module-level |
JavaScript |
| ✅ | ✅ | — | |
TypeScript |
| ✅ | ✅ | — | Interfaces are treated as classes for |
C |
| ✅ | ✅ | — |
|
C++ |
| ✅ | ✅ | — | Supports |
Ruby |
| ✅ | ✅ | — | Classes, modules, instance methods, |
Go |
| ✅ | ✅ | — |
|
Java |
| ✅ | ✅ | — |
|
JSON |
| ✅ (keys, values, arrays) | — (no comment syntax) | — | |
YAML |
| ✅ (keys, values, sequences) | ✅ | — | Block and flow sequences supported. |
TOML |
| ✅ (keys, values, arrays, tables) | ✅ | — |
|
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_importskips exact duplicates automatically.
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 inattr_accessor— you provide the exact text you want (whether that'sattr_accessor,attr_reader,@instance_var = nilininitialize, orCLASS_CONST = 42).Go:
add_field("Cache", "\tversion int")inserts the literal string inside thestruct { ... }body. The tool does not infer types from bare names — you provide the full Go field declaration.Rationale: consistent with how
add_fieldworks 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 :foofrom the namefoo) — 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 thetype 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 a full function definition (signature + body + decorators). |
|
| Replace only the body of a function, preserving signature and decorators. |
|
| Replace only the signature, preserving body and decorators. |
|
| Insert content at the top of a function body. |
|
| Insert content at the bottom of a function body. |
|
| Append any top-level content (function, class, constant, type alias) to the end of the file. |
|
| Add a method at the end of a class body. |
|
| Add a field/attribute/member at the top of a class body. |
|
| Insert a sibling immediately before a named symbol. |
|
| Insert a sibling immediately after a named symbol. |
|
| Delete a function or class definition block (including decorators). |
Parameters & signatures
Tool | Parameters | Description |
|
| Add a parameter to a function signature ( |
|
| Remove a parameter by name. |
Imports & includes
Tool | Parameters | Description |
|
| Add an |
|
| Remove a matching import line. |
|
| Add one name to an existing |
|
| Remove one name from a multi-name Python from-import. |
Comments & docstrings
Tool | Parameters | Description |
|
| Insert a comment block immediately before a named symbol. Works for Python/Ruby/YAML/TOML ( |
|
| Remove the contiguous comment block above a symbol. Recognizes both line comments and C-style single-line or multi-line |
|
| Replace the leading comment block above a symbol (or insert one if none exists). |
|
| 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 the value of an existing config key. |
|
| Add a key-value pair to a dict/object/mapping/table. For Python, |
|
| Delete a key-value pair. For Python, target is |
|
| Append a literal value to a list/array/sequence. For Python, |
|
| Remove the first matching element from a list/array/sequence. |
Navigation & reading (read-only)
Tool | Parameters | Description |
|
| Formatted outline of all top-level functions, classes, and methods with line numbers. |
|
| Return the signature of a function as plain text. |
|
| Syntactic search for all occurrences of an identifier (no scope awareness). |
|
| Return the full source text of a single named symbol (function, class, method, config key). Typically 10-20x fewer tokens than reading the whole file. |
|
| Return all import/include statements in the file. |
|
| Return a stub view of a class: header, field declarations, and method signatures with bodies replaced by |
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_symbolsNeed just a function's signature? →
get_signatureNeed one specific function's full source? →
read_symbol(10-20x cheaper than reading the whole file)Need a class's public API (methods + fields, no bodies)? →
read_interfaceNeed to see a file's imports/dependencies? →
read_importsWhere is a symbol used? →
find_references
Adding new content
Intent | Tool |
New top-level function, class, constant, or type alias |
|
New method in an existing class |
|
New field/attribute/member in a class |
|
New content at a specific position relative to an existing symbol |
|
New lines at the top of an existing function body |
|
New lines at the bottom of an existing function body |
|
New parameter on an existing function |
|
New import or |
|
New name in an existing |
|
New comment above a symbol |
|
New Python docstring on a function/class |
|
New key in a dict/object/mapping/table (any lang) |
|
New item in a list/array/sequence (any lang) |
|
Modifying existing content
Intent | Tool |
Rewrite the full function (signature + body) |
|
Rewrite only the body, keep the signature |
|
Change only the signature, keep the body |
|
Change only the leading comment above a symbol |
|
Change only the Python docstring |
|
Change the value of an existing config key |
|
Removing content
Intent | Tool |
Remove a function, method, or class |
|
Remove a parameter from a function |
|
Remove an import or |
|
Remove one name from a multi-name Python from-import |
|
Remove a leading comment above a symbol |
|
Remove a key from a dict/config |
|
Remove an item from a list/array |
|
Anti-patterns to avoid
Don't use
replace_functionto add a few lines — useprepend_to_bodyorappend_to_bodyinstead. Rewriting the whole function is wasteful and error-prone.Don't use
replace_function_bodyto add a few lines either — same reasoning. Useprepend_to_body/append_to_body.Don't use
replace_signatureto add or remove one parameter — useadd_parameter/remove_parameter.Don't use
replace_valueto add a new key — useadd_key.replace_valueonly updates existing keys.Don't use
add_importto add a name to an existingfrom X import …— useadd_import_name.Don't guess at target names. Call
list_symbolsfirst. 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 | shWindows (PowerShell):
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"Homebrew (macOS):
brew install uvpip (any platform):
pip install uvVerify the installation:
uv --versionFor more options (Docker, Cargo, WinGet, etc.), see the official uv installation docs.
Installation
Note: Replace
/absolute/path/tobelow 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/mcp-servers/ast-editor run ast-editor-mcp
# Gemini CLI
gemini mcp add --transport stdio --scope user ast-editor -- uv --directory /absolute/path/to/mcp-servers/ast-editor run ast-editor-mcpMethod 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
uvfor"command", not just"uv". GUI-based MCP clients (Claude Desktop, Cursor, Antigravity) don't always inherit your shellPATH, 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/mcp-servers/ast-editor",
"run",
"ast-editor-mcp"
]
}
}
}Agent | Configuration File Path |
Claude Desktop |
|
Cursor |
|
Windsurf | Agent Panel → "..." → MCP Servers → View raw config |
Antigravity |
|
Gemini CLI |
|
Using Standard Python (Fallback)
If you prefer not to use uv, install manually and point to the .venv executable:
cd mcp-servers/ast-editor && python3 -m venv .venv && source .venv/bin/activate && pip install .{
"mcpServers": {
"ast-editor": {
"command": "/absolute/path/to/mcp-servers/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.
Claude (Code & Desktop)
Add the following block to CLAUDE.md, ~/.claude/CLAUDE.md, or Custom Instructions:
When editing
.py,.js,.ts,.c,.cpp,.rb,.go,.java,.json,.yaml, or.tomlfiles, do NOT use theedittool. Use theast-editorMCP server instead. It exposes 32 surgical tools for structural code and config edits.Picking a tool — always start here:
Unsure of the file layout? Call
list_symbolsfirst. Callget_signatureif you only need a signature. Callread_symbolto read one function/class without reading the whole file. Callread_interfaceto see a class's API (methods + fields, no bodies). Callread_importsto see dependencies. Callfind_referencesbefore renaming anything.Adding new content — choose the narrowest tool:
Top-level (function, class, constant, type alias):
add_top_levelIn a class:
add_method,add_fieldInside a function body:
prepend_to_body,append_to_bodyRelative to an existing symbol:
insert_before,insert_afterImports:
add_import(new line) oradd_import_name(add to existingfrom X import …)Parameters:
add_parameterComments/docstrings:
add_comment_before,replace_docstringAny dict (config OR Python literal):
add_keyAny list/array (config OR Python literal):
append_to_arrayModifying existing content:
Full function:
replace_functionBody only:
replace_function_bodySignature only:
replace_signatureConfig value:
replace_valueLeading comment:
replace_leading_commentPython docstring:
replace_docstringRemoving content:
Function/class/method:
delete_symbolParameter:
remove_parameterImport:
remove_import(whole line) orremove_import_name(one name)Leading comment:
remove_leading_commentDict key or list item:
delete_key,remove_from_arrayAnti-patterns to avoid:
Don't use
replace_functionorreplace_function_bodyto add a couple of lines — useprepend_to_body/append_to_body.Don't use
replace_signatureto add one parameter — useadd_parameter/remove_parameter.Don't use
replace_valueto add a new key — useadd_key.Don't use
add_importto add a name to an existing from-import — useadd_import_name.Don't guess target names — call
list_symbolsfirst. Names are case-sensitive.
Specific Tool Overrides
Agent | Target Instructions / File | Instruction |
(recommended universal option) |
| Drop the Claude instruction block above into |
Cursor |
| ...do NOT use |
Codex CLI (OpenAI) |
| ...do NOT use |
GitHub Copilot (VS Code / JetBrains) |
| ...do NOT use inline suggestions for structural edits. Use AST tools from the |
Windsurf |
| ...do NOT use Cascade's built-in write mode. Use AST tools instead. |
Antigravity |
| ...do NOT use |
Generic / Aider / Gemini CLI
Add to rules or system prompt:
When editing
.py,.js,.ts,.c,.cpp,.rb,.go, or.javafiles, do NOT use your default editing tools (diff, whole, etc.). Instead, use theast-editorMCP server, which exposes 32 surgical tools for adding/modifying/removing functions, classes, methods, fields, parameters, imports, comments, and docstrings. Start any edit session by callinglist_symbolsto discover exact target names. For reading, useread_symbolto read one function/class (~10-20x fewer input tokens than reading the whole file),read_interfacefor a class's API (signatures only, no bodies), orread_importsfor dependencies. For small additions to a function body (logging, validation, cleanup), useprepend_to_body/append_to_body— these are the single biggest output-token-saving tools in the suite, giving ~20x fewer output tokens than rewriting the whole body. For.json,.yaml, or.tomlfiles, usereplace_value/add_key/append_to_array/delete_keyinstead of freeform edits.
Latest Blog Posts
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/kambleakash0/agent-skills'
If you have feedback or need assistance with the MCP directory API, please join our Discord server