Skip to main content
Glama

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 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

(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)

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.

Install Server
A
license - permissive license
A
quality
A
maintenance

Maintenance

Maintainers
<1hResponse time
Release cycle
1Releases (12mo)

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/ast-editor'

If you have feedback or need assistance with the MCP directory API, please join our Discord server