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.

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:

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 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/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-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/mcp-servers/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:

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

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

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

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

Agent

Instruction file

Any agent that reads AGENTS.md (Codex CLI, Windsurf, Zed, Cursor secondary)

AGENTS.md at repo root

Cursor

.cursor/rules/*.mdc (current) — legacy: .cursorrules

GitHub Copilot

.github/copilot-instructions.md

Windsurf

.windsurfrules or AGENTS.md

Antigravity

_agents/rules/

Aider / Gemini CLI / generic

Rules file or system prompt

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

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

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

v1.x tool

v2.0.0 equivalent

add_comment_before(target, comment)

edit_leading_comment(target, op="add", comment=...)

replace_leading_comment(target, new_comment)

edit_leading_comment(target, op="replace", comment=...)

remove_leading_comment(target)

edit_leading_comment(target, op="remove")

read_symbol(target)

read_symbol(target) (or explicit depth="full")

read_interface(target)

read_symbol(target, depth="interface")

get_signature(target)

read_symbol(target, depth="signature")

prepend_to_body(target, content)

insert_in_body(target, content, at="top")

append_to_body(target, content)

insert_in_body(target, content, at="bottom")

insert_before(target, content)

insert_sibling(target, content, position="before")

insert_after(target, content)

insert_sibling(target, content, position="after")

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

Available Tools

28 tools
add_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_bodyA
Destructive

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

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

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

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_referencesA
Read-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_symbolsA
Read-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_importsA
Read-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_symbolA
Read-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_arrayA
Destructive

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

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

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

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

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

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

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

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

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

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

    A4.3/5.0

    Scored across 28 tools

    Disambiguation4/5

    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.

    Naming Consistency4/5

    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.

    Tool Count2/5

    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.

    Completeness4/5

    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

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers