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)
Related MCP server: @lex-tools/codebase-context-dumper
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. For Go specifically, when a parenthesizedimport ( ... )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_symbolby default removes the contiguous leading comment block above the symbol (Godoc, Javadoc,#///comment runs) so docs don't become orphaned. Opt out withinclude_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 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. |
|
| Replace a byte-identical snippet inside a function body. Scoped to target's body; raises on multiple matches. |
|
| Delete a byte-identical snippet inside a function body. Scoped to target's body; raises on multiple matches. |
|
| Insert a snippet inside a function body. Pass exactly ONE of: |
|
| Insert top-level content. |
|
| Add a method at the end of a class body. |
|
| Add a field/attribute/member at the top of a class body. |
|
| Insert content as a sibling of a named symbol. |
|
| Delete a function or class definition block (including decorators). By default also consumes the contiguous leading comment block above the symbol (Godoc, Javadoc |
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 named-import statement: |
|
| 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 the contiguous leading-comment block above a named symbol. |
|
| 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. Targets: JSON/YAML/TOML dotted path; Python |
|
| 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. |
|
| Syntactic search for all occurrences of an identifier (no scope awareness). |
|
| Return source text of a single named symbol. |
|
| 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_symbolsNeed 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_importsWhere 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 |
|
New method in an existing class |
|
New field/attribute/member in a class |
|
New content before or after a top-level symbol |
|
New lines at the top of an existing function body |
|
New lines at the bottom of an existing function body |
|
New lines at a specific spot inside a function body (anchored to existing text) |
|
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 one statement/block inside a large body |
|
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 one statement/line inside a function body |
|
Remove a parameter from a function |
|
Remove an import or |
|
Remove one name from a multi-name named-import (Python or JS/TS) |
|
Remove a leading comment above a symbol |
|
Remove a key from a dict / config / JS-TS object literal |
|
Remove an item from a list/array |
|
Anti-patterns to avoid
Don't use
replace_functionorreplace_function_bodyto change a few lines — usereplace_in_body(scoped snippet match) orinsert_in_body(at="top" \| "bottom")for appending/prepending. Rewriting the whole function is wasteful and error-prone.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 …or named 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. 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/mcp-servers/ast-editor/AST-EDITOR.md ~/.claude/
echo '@AST-EDITOR.md' >> ~/.claude/CLAUDE.mdOr 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 |
|
Cursor |
|
GitHub Copilot |
|
Windsurf |
|
Antigravity |
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The old tools are hard-removed — calling them will fail with "unknown tool". Behavior is preserved 1:1 by the new calls.
Available Tools
28 toolsadd_fieldA
Add a field or member at the top of a class body: before the first
member, after a Python class docstring or a Java enum's constants
(adding the ; they need), and below the access label a C++ class
opens with. It is indented like the class's members, with a blank line
after it when a method follows. A function target is refused.
Use this when: adding a class attribute, field or member variable. Don't use this when: adding a method (add_method).
Example: class_target="LRUCache" content='version = "1.0"'
Inputs: content (required), file_path (required), class_target (required). Returns: result
add_importA
Add an import line, skipping an exact duplicate. It goes into the group
of imports it belongs with (same package, standard library or not,
relative or not), at its sorted place when the group is sorted, else
after the group's last import. A comment heading the group stays first
(a next-line directive stays on its import), and so do Python from __future__ imports. With no fitting group it goes after the last
import, and a first import after the preamble.
Use this when: importing something the file does not import yet. Don't use this when: adding a name to an existing import (add_import_name).
Example: import_text="from typing import Optional"
Inputs: file_path (required), import_text (required). Returns: result
add_import_nameA
Add a name to an existing from <module> import ... (Python) or
import { ... } from "<module>" (JS/TS) statement; a name already
there is left as is. It goes at its sorted place when the names are
sorted (plainly or as isort and ruff sort them), else at the end, in the
list's layout: names one per line give it a line of its own, an inline
list stays inline. Refused when the file has no such import.
Use this when: the module is already imported with named imports. Don't use this when: there is no such import yet, or for a default or namespace import (add_import with the full line).
Example (from typing import Any, cast becomes Any, Dict, cast):
module="typing"
name="Dict"
Inputs: name (required), module (required), file_path (required). Returns: result
add_keyA
Add a key-value pair to a JSON object, YAML mapping, TOML table, Python dict literal or JS/TS object literal. The entry follows the container's layout: one key per line stays so, an inline object stays inline, and a trailing comma is kept. A YAML key with no value gets a nested mapping. A key the container already has is refused, naming the target to give replace_value.
Use this when: the key does not exist yet. Don't use this when: the key exists (replace_value), or you add a list item (append_to_array).
Example (TS, const CONFIG = { server: {...} }): parent_target="CONFIG.server" key="port" value="8080"
Inputs: key (required), value (required), file_path (required), parent_target (required). Returns: result
add_methodA
Add a method at the end of a class body, after its last member, spaced like the class's methods. In Go it goes after the type declaration. In a C++ class whose last method is followed by an access label, it goes right after that method, so it gets the same access. A one-line Python or Ruby class is refused, and so is a function target (use insert_in_body).
Use this when: adding a method to an existing class. Don't use this when: adding a field (add_field) or a top-level function (add_top_level).
Example: class_target="LRUCache" content='def clear(self):\n self.items.clear()'
Inputs: content (required), file_path (required), class_target (required). Returns: result
add_parameterA
Add one parameter to a function's signature, keeping the body. "end"
puts it after the last parameter but before those that must come after
it (Python **kwargs, a JS/TS rest parameter, Go and Java variadics,
C/C++ ..., Ruby **opts and &blk); "start" before the first, but
after a TS this or Java receiver parameter. A list written one
parameter per line gets it on a line of its own, a C (void) takes it
in place of void, and a bare JS/TS arrow parameter gets parentheses.
A name the function already has is refused.
Use this when: adding a parameter without retyping the signature. Don't use this when: rewriting the signature (replace_signature), or changing the body too (replace_function).
Example (def f(a, **kw) becomes def f(a, b, **kw)):
target="f"
parameter="b"
Inputs: target (required), file_path (required), parameter (required), position. Returns: result
add_top_levelA
Add top-level code (a function, class, constant or other statement) to a source file. "bottom" appends it at the end, inside a header's include guard; "top" puts it after the preamble, before the first declaration. It is set apart by a blank line, two around a Python def or class, but one-line code added at the bottom under stacked one-line code is stacked too. In Go, or a file indented with tabs, its indentation becomes tabs.
Use this when: adding a top-level symbol or statement. Don't use this when: placing it next to a symbol (insert_sibling), in a class (add_method, add_field), in a body (insert_in_body) or in a config file (add_key).
Example: content="MAX_CONNECTIONS = 10" position="top"
Inputs: content (required), file_path (required), position. Returns: result
append_to_arrayA
Append a value to a JSON, YAML or TOML array or a Python module-level
list. The value follows the array's layout: an inline array stays
inline ([1, 2] becomes [1, 2, 3]), one element per line gets a new
line, and a trailing comma is kept. Comments are not elements: the
comma goes before them.
Use this when: adding a list item. Don't use this when: adding a key-value pair (add_key).
Example: target="project.dependencies" value='"new-package"'
Inputs: value (required), target (required), file_path (required). Returns: result
delete_in_bodyADestructive
Delete a snippet inside a function's body, matched as replace_in_body
matches one. A deletion that empties its lines removes them whole; a
comment after the snippet on its last line stays. One that would leave
a Python body without a statement is refused (replace the snippet with
pass).
Use this when: removing a statement, block or line in a body, or an entry of an object literal passed as an argument. Don't use this when: deleting a whole function or class (delete_symbol).
Example: target="RegisterRoutes" snippet='r.Mount("/kb", kbHandler)'
Inputs: target (required), snippet (required), file_path (required). Returns: result
delete_keyADestructive
Delete a key-value pair from a config file, or from a Python or JS/TS
module-level dict or object literal. The adjacent comma goes too where
the syntax has one, and deleting the only entry leaves {}; YAML and
TOML keys otherwise go with their lines.
Use this when: removing a whole entry. Don't use this when: removing a list item (remove_from_array), or an entry of an object literal passed as an argument (delete_in_body).
Example: target='CONFIG."timeout"'
Inputs: target (required), file_path (required). Returns: result
delete_symbolADestructive
Delete a whole function, method, class or other symbol, with its
decorators, a JS/TS export and a C++ template <...> header. The
blank lines that set it apart go too, so the file keeps its spacing. A
symbol that shares a line with other code is cut out of the line. One
name of a declaration of several (int a, b;, struct S {...} s;) is
refused: give replace_function the declaration without it. Refused in
JSON, YAML and TOML files (use delete_key).
Use this when: removing a symbol along with its doc comment. Don't use this when: removing a config key (delete_key), an import (remove_import) or lines inside a body (delete_in_body).
Example: target="LRUCache.old_method"
Inputs: target (required), file_path (required), include_leading_comments. Returns: result
edit_leading_commentADestructive
Add, replace or remove the comment block directly above a symbol, its
doc comment. The block sits above the symbol's decorators, annotations,
export and C++ template <...> header, and a new comment goes there,
re-indented to the symbol. A comment indented deeper than the symbol
ends the code above and is not part of the block. A symbol that other
code comes before on its line is refused. In YAML and TOML the target
is a key path or a TOML [table]; JSON has no comments and is refused.
Use this when: adding, rewriting or deleting a symbol's doc comment. Don't use this when: writing a Python docstring (replace_docstring) or a comment inside a body (insert_in_body, replace_in_body).
Example: target="LRUCache.get", op="add", comment="# Return the cached value."
Inputs: op (required), target (required), file_path (required), comment. Returns: result
find_referencesARead-only
List each occurrence of an identifier in a source file as line N: <source line>, in file order. Read-only and syntactic, with no scope
analysis, so unrelated identifiers of the same name show up too. C++
destructors and operators go by their list_symbols names (~A,
operator==).
Use this when: surveying a name's uses in a file before a rename. Don't use this when: you need references across files or by scope (use a language server).
Example: target="LRUCache"
Inputs: target (required), file_path (required). Returns: result
insert_in_bodyA
Insert new_snippet inside a function's body. Set exactly one of at,
after or before. at="top" goes after a Python docstring,
at="bottom" at body level after the last statement, and in an empty
body one level deeper than the function; at is refused for a body on
its signature's line (use replace_function_body).
An anchor is matched as replace_in_body matches a snippet and must be
unique. One that ends its line (after) or starts it (before) gets
new_snippet on lines of its own; after a line that opens a block (if x:, if (x) {), new lines go inside the block. A mid-line anchor gets
new_snippet byte for byte.
Use this when: adding lines to a body. Don't use this when: replacing the whole body (replace_function_body), changing existing lines (replace_in_body), or adding a top-level symbol (add_top_level).
Example (both written without indentation): target="handle" new_snippet='auth_check(request)' before='validate(request)'
Inputs: target (required), file_path (required), new_snippet (required), at, +2 more. Returns: result
insert_siblingA
Insert code next to a named symbol, at its level. "before" goes above
the symbol's leading comment, decorators and export; "after" below
the symbol (after the ; that ends a C/C++ class). The code is spaced
like the gap it goes into: one-line code stacked without blank lines
stays stacked, and next to a function or class it gets at least one
blank line, two around a top-level Python def or class. A comment
indented deeper than the target ends the code above, so "before" goes
below it. "before" is refused when other code comes before the target
on its line, "after" when code follows it. Refused in JSON, YAML and
TOML files (use add_key).
Use this when: placing code relative to a symbol, such as a helper above its caller. Don't use this when: appending at the end of the file (add_top_level), or inserting into a body (insert_in_body) or a class (add_method, add_field).
Example: target="LRUCache" content="CACHE_SIZE = 100" position="before"
Inputs: target (required), content (required), position (required), file_path (required). Returns: result
list_symbolsARead-only
List the symbols of a source file, one per line as <kind> <name> (line N), class members indented under their class (method A.B.m). Each
name is the target the other tools take: overloads with their
parameter types (A.m(int)), a C++ definition outside its class by
its qualified name (A::f), and a struct or TS type that shares its
name with another symbol after its keyword (struct struct stat
targets struct stat). Kinds include function, method, class, struct,
enum, interface, namespace, type, macro, field, variable and constant;
a Go var x = func(...) {...}, listed as a variable, also takes the
body, parameter and signature tools. A name with a prototype or
forward declaration is listed once, at its definition. Read-only.
Use this when: before editing a file, to get exact target names. Don't use this when: you already know the target (read_symbol reads one symbol).
Example: file_path="/abs/path/to/module.py"
Inputs: file_path (required). Returns: result
read_importsARead-only
Return the import statements of a source file (imports, includes, Ruby requires), without reading the rest. Read-only.
Use this when: checking a file's dependencies, as before add_import. Don't use this when: changing imports (add_import, remove_import).
Example: file_path="/abs/path/to/module.py"
Inputs: file_path (required). Returns: result
read_symbolARead-only
Return the source of one symbol (function, class, method, variable) or
the value of a config key, without reading the whole file. Read-only.
"full" is the text replace_function and delete_symbol take: decorators,
a JS/TS export, a C++ template <...> header and code that only
wraps the symbol (a Go type, a closure's key, a C typedef)
included, a C/C++ class ending with its ;. It starts at the symbol's
first byte, the later lines at their file indentation. "interface"
gives a class's header, fields and method signatures (bodies as
...), indented as in the class, or a function's signature;
"signature" the signature or class header alone, after decorators.
Both refuse a variable, constant, field, type alias or macro, saying
what it is. A name with a prototype or forward declaration reads as
its definition.
Use this when: reading one symbol; pick the narrowest depth. Don't use this when: you need the file's outline (list_symbols) or its imports (read_imports).
Example: target="LRUCache", depth="interface"
Inputs: target (required), file_path (required), depth. Returns: result
remove_from_arrayADestructive
Remove the first element matching value_match from a JSON, YAML or
TOML array or a Python module-level list. An element on a line of its
own goes with that line and its comment; other comments stay. The
trailing-comma style is kept, and removing the only element leaves
[].
Use this when: removing one list item. Don't use this when: removing a whole key (delete_key).
Example: target="project.dependencies" value_match='"old-package"'
Inputs: target (required), file_path (required), value_match (required). Returns: result
remove_importADestructive
Remove an import line, matched by its text with surrounding whitespace stripped.
Use this when: removing an unused import. Don't use this when: removing one name from a multi-name import (remove_import_name).
Example: import_text="import os"
Inputs: file_path (required), import_text (required). Returns: result
remove_import_nameADestructive
Remove one name from a from <module> import ... (Python) or import { ... } from "<module>" (JS/TS) statement. A name on a line of its own
goes with that line and its comment. Removing the only name removes the
statement, but is refused when a default import would be left with
empty braces (import D, { a } from "m"): use remove_import, then
add_import.
Use this when: dropping one name from a multi-name import. Don't use this when: removing the whole import line (remove_import).
Example: module="typing" name="List"
Inputs: name (required), module (required), file_path (required). Returns: result
remove_parameterADestructive
Remove a parameter by name from a function's signature, keeping the
body. A parameter on a line of its own goes with that line and its
comment. Removing the only one leaves (), or (void) in C. In Python
a * or / left invalid goes too, and a *args with keyword-only
parameters after it leaves a bare * (def f(a, *args, b) minus args
is def f(a, *, b)). One Go name of a, b int, or one name that a
destructuring parameter binds, goes alone. Refused: a name the function
lacks, and in C the last named parameter before ....
Use this when: dropping one parameter without retyping the signature. Don't use this when: rewriting the signature (replace_signature).
Example: target="LRUCache.get" parameter_name="default"
Inputs: target (required), file_path (required), parameter_name (required). Returns: result
replace_docstringADestructive
Replace or add the docstring of a Python function, method or class, or
of the module with target "". new_docstring must be one string literal
with its quotes; anything else (bare text, an f-string, bytes, an empty
value) is refused. A new docstring goes first in the body; the module's
goes below a #! line and encoding or header comments. A new docstring
for a body on its signature's line is refused (use
replace_function_body); so is any non-Python file.
Use this when: adding or updating a Python docstring.
Don't use this when: editing a # comment above the symbol
(edit_leading_comment).
Example: target="LRUCache.get" new_docstring='"""Return the cached value, or None."""'
Inputs: target (required), file_path (required), new_docstring (required). Returns: result
replace_functionADestructive
Replace a whole symbol (decorators, signature and body) with new content.
The replaced span is what read_symbol returns: decorators, a JS/TS
export, a C++ template <...> header and code that only wraps the
symbol are part of it, so keep them in content. read_symbol's text,
edited or not, goes in as it is, and may become several members;
content written from column 0 is re-indented to the symbol's level. A
symbol that shares a line with other code has only its own text
replaced. For one name of int a, b; the content must keep the others.
Refused in JSON, YAML and TOML files (use replace_value).
Use this when: rewriting a symbol top to bottom (name, parameters and body). Don't use this when: only the body changes (replace_function_body), only the signature (replace_signature), or a few lines (replace_in_body).
Example: target="LRUCache.get" content='def get(self, key, default=None):\n return self.items.get(key, default)'
Inputs: target (required), content (required), file_path (required). Returns: result
replace_function_bodyADestructive
Replace only the body of a function, keeping its signature and decorators. A body on its signature's line is rebuilt over lines of its own, and an empty body gets content one level deeper than the function. A Ruby method's rescue, else and ensure clauses stay and only the code before them is replaced, unless content has clauses of its own. Refused for a function without a block body: a Ruby endless method, or an arrow function whose body is an expression (use replace_function).
Use this when: changing the implementation but not the interface. Don't use this when: the parameters or return type change too (replace_function), or only a few lines change (replace_in_body).
Example: target="LRUCache.get" content='if key in self.items:\n return self.items[key]\nreturn None'
Inputs: target (required), content (required), file_path (required). Returns: result
replace_in_bodyADestructive
Replace a snippet inside a function's body; the match is scoped to that body, so text elsewhere in the file never matches. A snippet found nowhere is refused; one found in several places is refused with their line numbers: add context.
Use this when: changing one statement or block in a long body. Don't use this when: replacing the whole body (replace_function_body), or editing a sub-expression no unique snippet can locate (your normal edit tool).
Example (a block written without its indentation): target="handle" old_snippet="if retry:\n wait()" new_snippet="if retry:\n backoff()\n wait()"
Inputs: target (required), file_path (required), new_snippet (required), old_snippet (required). Returns: result
replace_signatureADestructive
Replace the signature of a function, or the header of a class, keeping
its body. Existing decorators, Java annotations and a C++ template <...> header stay unless new_signature has its own. Code that
read_symbol's signature starts with (a JS/TS const f =, a closure's
key, a TS declare, a Ruby private) is replaced when new_signature
starts with it, else kept. A method without a body keeps its ;.
Refused for a symbol with no signature, such as a variable.
Use this when: changing a function's parameters, return type or name. Don't use this when: the body changes too (replace_function), or one parameter is added or removed (add_parameter, remove_parameter).
Example: target="LRUCache.get" new_signature="def get(self, key, default=None):"
Inputs: target (required), file_path (required), new_signature (required). Returns: result
replace_valueADestructive
Replace the value of an existing config key, dict or object entry, or
of a variable, constant, field or macro in a source file (X = 1,
const X = 1;, int x = 1;, #define N 3). Only the value changes; a
declaration without one gets one (let x; becomes let x = 2;, YAML
a: becomes a: 2). Refused: a function, class or type (use
replace_function), a name sharing one value with others (a = b = 1)
and a declaration that cannot have a value (extern, TS declare).
Array elements are not addressable.
Use this when: updating a value that exists. Don't use this when: the key is new (add_key), or you edit an array (append_to_array, remove_from_array).
Example: target="project.version" content='"2.0.0"'
Inputs: target (required), content (required), file_path (required). Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 28 tools
Most tools target distinct operations, and the descriptions provide explicit “Use this when / Don’t use this when” guidance to separate close neighbors like replace_function, replace_function_body, and replace_in_body. The remaining ambiguity comes from the large number of fine-grained edit tools, where an agent must carefully choose between body, signature, parameter, sibling, and top-level variants.
Names are consistently snake_case and verb-first, following predictable patterns such as add_import, remove_import_name, replace_signature, and list_symbols. The minor inconsistency is the mixed use of delete and remove for deletion operations, though the names remain readable and mostly predictable.
With 28 tools, the surface is heavy and requires agents to select among many highly granular operations. The domain is complex, but several tools could plausibly be consolidated or grouped under operation-based interfaces to reduce selection burden.
The server covers a strong lifecycle for imports, symbols, bodies, signatures, parameters, comments, docstrings, and config keys/arrays. Notable gaps remain around higher-level refactors such as rename-across-file or moving symbols, but agents can work around these by combining find_references and copy/delete operations.
Maintenance
Related MCP Connectors
A comprehensive Model Context Protocol (MCP) server that enables AI assistants to interact with yo…
Shared memory MCP server for Claude Code, Codex, Gemini CLI and Cursor. Cited, editable.
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
A Model Context Protocol server for Wix AI tools
Related MCP Servers
- AlicenseAqualityCmaintenanceA Model Context Protocol (MCP) server that helps large language models index, search, and analyze code repositories with minimal setup22142,766 PyPI1,005MIT
- AlicenseAqualityDmaintenanceA Model Context Protocol (MCP) server designed to easily dump your codebase context into Large Language Models (LLMs).12 npm3Apache 2.0

CodeAlive MCPofficial
AlicenseNot gradedqualityCmaintenanceA Model Context Protocol server that enhances AI agents by providing deep semantic understanding of codebases, enabling more intelligent interactions through advanced code search and contextual awareness.91MIT- FlicenseAqualityDmaintenanceA Model Context Protocol (MCP) server for advanced code analysis and editing with semantic search capabilities, enabling AI assistants to perform complex code operations through a standardized interface.151-