MCP Hub
Back to servers

ast-editor

AST-targeted code editing MCP server with 28 surgical tools for structural edits across 11 languages. Built on tree-sitter, replaces brittle search/replace with byte-correct edits keyed by symbol names.

glama
Updated
May 6, 2026

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.

ast-editor MCP server

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 sizeFile sizevs whole-file rewritevs unified diffvs search/replace
1-line tweak100 LoC3–5x~1.5x~1.5x
Function body rewrite500 LoC8–12x2–3x2–3x
Function body rewrite4,000 LoC15–20x3–5x3–5x
Add 2 lines to a functionany size~20x (via prepend_to_body / append_to_body)5–10x3–5x

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

Read taskFile sizeAST reader toolvs full file read
One function's source500 LoCread_symbol~20x fewer tokens
One function's source2,000 LoCread_symbol~50-100x fewer tokens
Class API (10 methods, no bodies)500 LoCread_interface~10x fewer tokens
Import block onlyany sizeread_imports~20-50x fewer tokens
Structural overview (names + line numbers)any sizelist_symbols~15-30x fewer tokens
One function's signatureany sizeget_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

LanguageExtensionsStructural editsCommentsDocstringsNotes
Python.py#✅ function/classDecorators 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)

ToolParametersDescription
replace_functionfile_path, target, contentReplace a full function definition (signature + body + decorators).
replace_function_bodyfile_path, target, contentReplace only the body of a function, preserving signature and decorators.
replace_signaturefile_path, target, new_signatureReplace only the signature, preserving body and decorators.
replace_in_bodyfile_path, target, old_snippet, new_snippetReplace a byte-identical snippet inside a function body. Scoped to target's body; raises on multiple matches.
delete_in_bodyfile_path, target, snippetDelete a byte-identical snippet inside a function body. Scoped to target's body; raises on multiple matches.
insert_in_bodyfile_path, target, new_snippet, at | after | beforeInsert a snippet inside a function body. Pass exactly ONE of: at="top" (prepend), at="bottom" (append), after=<snippet> (anchored), before=<snippet> (anchored).
add_top_levelfile_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_methodfile_path, class_target, contentAdd a method at the end of a class body.
add_fieldfile_path, class_target, contentAdd a field/attribute/member at the top of a class body.
insert_siblingfile_path, target, content, positionInsert content as a sibling of a named symbol. position="before" or "after".
delete_symbolfile_path, target, include_leading_comments=TrueDelete 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

ToolParametersDescription
add_parameterfile_path, target, parameter, positionAdd a parameter to a function signature (position: "start" or "end").
remove_parameterfile_path, target, parameter_nameRemove a parameter by name.

Imports & includes

ToolParametersDescription
add_importfile_path, import_textAdd 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_importfile_path, import_textRemove a matching import line.
add_import_namefile_path, module, nameAdd 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_namefile_path, module, nameRemove 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

ToolParametersDescription
edit_leading_commentfile_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_docstringfile_path, target, new_docstringReplace or insert a Python function/class docstring. Python-only.

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

ToolParametersDescription
replace_valuefile_path, target, contentReplace the value of an existing config key. target is the dotted key path.
add_keyfile_path, parent_target, key, valueAdd 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_keyfile_path, targetDelete 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_arrayfile_path, target, valueAppend a literal value to a list/array/sequence. For Python, target is the list variable name; for config, a dotted path.
remove_from_arrayfile_path, target, value_matchRemove the first matching element from a list/array/sequence.

Navigation & reading (read-only)

ToolParametersDescription
list_symbolsfile_pathFormatted outline of all top-level functions, classes, and methods with line numbers.
find_referencesfile_path, targetSyntactic search for all occurrences of an identifier (no scope awareness).
read_symbolfile_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_importsfile_pathReturn 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

IntentTool
New top-level function, class, constant, or type aliasadd_top_level (use position="top" to prepend after preamble)
New method in an existing classadd_method
New field/attribute/member in a classadd_field
New content before or after a top-level symbolinsert_sibling(position="before" | "after")
New lines at the top of an existing function bodyinsert_in_body(at="top")
New lines at the bottom of an existing function bodyinsert_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 functionadd_parameter
New import or #includeadd_import
New name in an existing from X import … or import { a, b } from "mod"add_import_name
New comment above a symboledit_leading_comment(op="add")
New Python docstring on a function/classreplace_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

IntentTool
Rewrite the full function (signature + body)replace_function
Rewrite only the body, keep the signaturereplace_function_body
Change one statement/block inside a large bodyreplace_in_body (scoped snippet match)
Change only the signature, keep the bodyreplace_signature
Change only the leading comment above a symboledit_leading_comment(op="replace")
Change only the Python docstringreplace_docstring
Change the value of an existing config keyreplace_value

Removing content

IntentTool
Remove a function, method, or classdelete_symbol (consumes leading doc comment by default)
Remove one statement/line inside a function bodydelete_in_body
Remove a parameter from a functionremove_parameter
Remove an import or #includeremove_import
Remove one name from a multi-name named-import (Python or JS/TS)remove_import_name
Remove a leading comment above a symboledit_leading_comment(op="remove")
Remove a key from a dict / config / JS-TS object literaldelete_key
Remove an item from a list/arrayremove_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"
      ]
    }
  }
}
AgentConfiguration 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)
WindsurfAgent 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.

AgentInstruction 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 / genericRules 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 toolv2.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.

Reviews

No reviews yet

Sign in to write a review