Skip to main content
Glama
README.md
# 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](https://geometricagi.github.io/2026/04/02/ast-edits.html).

### Credits

This MCP server was inspired by research from [Jack Foxabbott](https://www.linkedin.com/in/foxabbott/) and the team at [Geometric AGI](https://geometricagi.github.io/). Their full findings, benchmark suite, and data are available here:

- [AST Edits: The Code Editing Format Nobody Uses](https://geometricagi.github.io/2026/04/02/ast-edits.html) (Blog)
- [Jack Foxabbott's original post](https://www.linkedin.com/posts/foxabbott_i-didnt-know-until-recently-that-all-the-share-7445506956783480832-dcXr/) (LinkedIn)
- [GeometricAGI/blog](https://github.com/GeometricAGI/blog) (Benchmark code & data)

## Estimated Token Savings

Per-edit output token savings versus other common edit formats:

| Edit size | File size | vs whole-file rewrite | vs unified diff | vs search/replace |
| :--- | :--- | :--- | :--- | :--- |
| 1-line tweak | 100 LoC | 3–5x | ~1.5x | ~1.5x |
| Function body rewrite | 500 LoC | 8–12x | 2–3x | 2–3x |
| Function body rewrite | 4,000 LoC | **15–20x** | 3–5x | 3–5x |
| Add 2 lines to a function | any size | **~20x** (via `insert_in_body(at="top")` / `insert_in_body(at="bottom")`) | 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_symbol(depth="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 | `read_symbol(depth="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 `insert_in_body` (`at="top"` / `at="bottom"`, or anchored with `after=` / `before=`) for small additions instead of rewriting whole function bodies
- **Input tokens:** Using `read_symbol` (`depth="full"` / `"interface"`) / `read_imports` to read only what's needed instead of entire files (~10-20x fewer input tokens per read)
- **Discovery:** `list_symbols` / `read_symbol(depth="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. Nested classes (decorated ones too) are listed with their members (`class A.B`, `method A.B.m`). Module-level `dict`/`list` literals editable via `add_key` / `replace_value` / `delete_key` / `append_to_array` (nested dicts addressed by key path, e.g. `CONFIG."db"`); comments in a list are not elements, so `append_to_array` puts the comma before them. Module-level variables (`X = 1`, `Y: int = 2`) are listed as `variable` and class attributes as `field` (`A.x`), at their first assignment; `replace_value` replaces their value. `@overload` stubs directly before their function are one symbol with it, listed once at the first stub. A name defined again (a redefinition, a `@property` setter) is listed again, and the name targets the first. |
| **JavaScript** | `.js`, `.jsx`, `.mjs`, `.cjs` | ✅ | ✅ `//` + `/* ... */` | — | Exported declarations (`export function`, `export class`, `export default function c`) are listed and targeted by their own names, an anonymous `export default function () {}` / `export default class {}` as `default`; the `export` is part of the symbol, so `read_symbol` shows it and `replace_function` content should keep it. Generator functions (`function* g()`) and each name of a `const`/`let`/`var` bound to an arrow function or function expression (`const f = (x) => {...}`, exported or not) are listed and targeted as functions: `read_symbol`, `replace_function`, `delete_symbol`, the comment tools, `insert_sibling` and the parameter tools work on them, and the body tools on those with a `{ }` body (one whose body is an expression, `(x) => x * 2`, is refused: rewrite it with `replace_function`). Their signature is `const f = (x) =>`, and `replace_signature` replaces from the `const` when the new signature starts with it, else only `(x) =>`; a lone unparenthesized parameter gets parentheses from the parameter tools (`x => ...` becomes `(x, y) => ...`, or `() => ...` when it is removed). Module-level `const`/`let`/`var` object literals (exported or not) editable via `add_key` / `replace_value` / `delete_key`; nested objects addressed by dotted path (`CONFIG.server`). Other data is listed too: each name of a `const` as `constant` and of a `let`/`var` as `variable`, and class fields as `field` (`A.x`, `A.#y`, static, decorated and `declare` ones), which the whole-symbol tools and `replace_value` (its value alone) take; a method or function wins over a class field or `var` of its name (`static run = false;` beside `run() {}`), and a setter beside its getter is not listed (the name targets the getter). Computed and quoted names are targets as listed (`E.[Symbol.iterator]`, `E.'a.b'`). A comment after a symbol on its last line (`function f() {} // f`) is no part of it, as in the other languages. CommonJS `require` statements count as imports for `add_import`. |
| **TypeScript** | `.ts`, `.tsx` | ✅ | ✅ `//` + `/* ... */` | — | Exports, generators and functions bound to names as for JavaScript. Abstract classes (`abstract class A`, exported or not) are classes for every tool, and their abstract methods are listed (an overloaded one once) and targeted (`A.m`) like the others, `replace_signature` included (the final `;` stays); enums, type aliases and interfaces are listed as `enum`, `type` and `interface` and can be read, replaced, deleted, commented and used as `insert_sibling` targets. Interfaces are treated as classes for `add_method` / `add_field`, which refuse an enum or type alias (rewrite those with `replace_function`); a type alias has no `depth="signature"`. A type alias or interface that shares its name with a value in one scope, as in zod's `export const User = z.object(...)` with `export type User = z.infer<typeof User>` (or an interface and a function), is targeted with its keyword, `type User` or `interface Props` (listed as `type type User`; with a dotted path the keyword goes first, `type N.User`), and the plain name targets the value. Overload signatures directly before their implementation (through `export` and `declare`, comments and the implementation's decorators between them; a run of signatures alone too) are one symbol with it, listed once at the first signature: `read_symbol`, `replace_function` and `delete_symbol` take them all, `insert_sibling(position="before")` and `edit_leading_comment` go above the first, and the signature depths, `replace_signature`, the parameter tools and the body tools work on the implementation. Namespaces (`namespace N {}`, `module N {}`) are listed as `namespace` with their classes and functions (`N.C`, `N.f`); one opened again, or merged with the class, function or enum of its name, is one container: the name targets the class, function or enum, its members and the namespace's are found and listed under it, and none of this is ambiguous. `declare` declarations are listed like plain ones (`read_symbol` shows the `declare`), and an abstract method reads with its `;`. A namespace's interface ends with its `}`, without a comment after it. `.tsx` files are parsed with the TSX grammar, so JSX is valid there. Object literals editable as for JavaScript, including `{ ... } as const` and `satisfies T`. |
| **C** | `.c`, `.h` | ✅ | ✅ `//` + `/* ... */` (single + multi-line) | — | `.h` defaults to C — use `.hpp`/`.hxx`/`.hh` for C++ headers (adding C++-only syntax to a `.h` file that parses as C is refused by the save-time syntax check). A struct, union or enum ends with the `;` after its `}`: `read_symbol` shows it, `replace_function` content should end with `};`, and `delete_symbol` and `insert_sibling(position="after")` take it, also when it is on a line of its own. Listed as `struct`, `union`, `enum`, `function`, `macro` (`#define`, function-like ones over several lines too), `type` (typedefs; an anonymous `typedef struct {...} P;` lists its fields as `P.x` and takes `add_field`), `field`, `variable` and `constant` (`const` but not a pointer), including what `#if`/`#ifdef` blocks (include guards too) and `extern "C"` hold; a struct defined in a declaration or typedef is shown whole by `read_symbol` (`struct S {...} s;`, `typedef struct N {...} N;`). A definition wins over its forward declaration (`struct S;`), prototype or `extern` declaration in either order, and is what list and target; a prototype alone is a bodiless `function` (`replace_signature` and the parameter tools work on it). A definition outside `#if`/`#ifdef` blocks wins over one inside (`#if 0`), and of alternative definitions in `#ifdef`/`#else` branches the first is listed and targeted. A struct, union or enum that shares its name with a function, variable or typedef is targeted with its keyword (`struct stat`, listed as `struct struct stat`), and the plain name targets the other symbol (`typedef struct N {...} N;` stays one symbol, and a typedef of a struct declared apart, `typedef struct Node Node;`, is `Node`); `add_method` and `add_field` look only at containers, so `stat` is the struct there. Without overloading, `f(int)` only checks the parameter types. `replace_value` replaces the value of a global (`int g = 1;`, `int a[] = {1}`) or a macro's text (`#define N 3`), and gives one to a declaration without one (a C struct's field cannot have one and is refused); for a name that a macro shares with a function, the name is the macro and the function is `name(types)`. A type that is only used (`struct S *p;`) and variables inside function bodies are not targets. |
| **C++** | `.cpp`, `.cc`, `.cxx`, `.hpp`, `.hxx`, `.hh` | ✅ | ✅ `//` + `/* ... */` (single + multi-line) | — | Supports `class`, `struct`, `union`, `enum`, `namespace`; `Class::method` qualified names resolve correctly. Classes end with their `;` as in C. A template's `template <...>` header is part of its symbol, as decorators are: `read_symbol` shows it, `replace_function` content should include it, `delete_symbol` removes it, a leading comment and `insert_sibling(position="before")` go above it, and `replace_signature` keeps it unless the new signature starts with `template`. Template functions, classes and member templates are listed at the line of their header, an explicit specialization (`template <> int id<int>(int)`) under the name it specializes, and a definition outside its class (`int A::f()`, `template <typename T> T Box<T>::get()`) as a function under its qualified name (`A::f`, `Box::get`, `A::A`; the last name alone, or a shorter tail, still finds it when nothing else in the scope has that name). Overloads take their parameter types, written as declared without names, defaults or attributes, spacing free (`A.m(int)`, `A.m(const std::string &, int)`, `A::A(int)`, `A.operator()(int)`), and method qualifiers where only those differ (`A.get(int) const`); spellings of one type match (`unsigned` and `unsigned int`, `char const *` and `const char *`, `int[]` and `int *`, a `const` on the parameter itself, `struct S *` and `S *`), so a prototype and a definition that spell it differently are one function. The bare name of an overloaded function, and a name several qualified names end with, are refused with the targets to use. A class or namespace declared with its scope (`struct A::B {...}`, `namespace a::b {}`) is `A::B`, and `class X` targets a class that shares its name with another symbol. Everything listed for C (`constexpr` variables as `constant` too), plus `class`, `namespace` (members as `ns.f`; a namespace opened again in its scope is listed once with the members of all its blocks, and a target finds a definition in any of them), nested classes (`class A.B`), `using` aliases as `type`, static members, methods declared in their class, and structured bindings (`auto [x, y] = p;`). Destructors and operators are named as written without spaces, `~A`, `operator==`, `operator()`, `operator bool`, and targeted as `A.~A` or `A.operator==` (any spacing matches); `find_references` finds them. |
| **Ruby** | `.rb` | ✅ | ✅ `#` | — | Classes, modules, instance methods, `singleton_method` (class methods via `def self.foo`). `require`/`require_relative`/`load`/`autoload` recognized as imports. A statement that opens a heredoc (`X = <<~EOS`) ends with the heredoc's body, for `read_symbol` and the whole-symbol edits. An endless method (`def f = x`) has no body for the body tools. Classes and modules nested in modules are listed with their members, `private def m` and `class << self` methods too (`read_symbol` shows the `private`). Embedded documents (`=begin` ... `=end` at column 0) stay at column 0 when content is re-indented, and an edit that would leave a `=begin` indented is refused; one directly above a method is its leading comment. A Sorbet signature directly above a method (`sig { ... }`, `sig do ... end`) is part of it, as a decorator is, and the signature depths start at its `def`. Constants are listed as `constant` and locals as `variable`. A class reopened in one scope is listed for each block, and the name targets the first; a dotted target finds a member in any of them. Interfaces show every method as its signature plus ` ...`, empty and endless ones too. |
| **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. Type aliases (`type T = int`) are listed as `type`, and every name of a `var`/`const` as `variable`/`constant`. `read_symbol` shows the `type`, `var` or `const` of a declaration of one spec (a spec in a `type (...)` group alone) and a closure's key (`RunE: func() error {...}`). `remove_parameter` removes one name of `a, b int`, keeping the others' comments. A `var x = func(...) {...}` is a function for the parameter, body and signature tools (listed as `variable`; one of several values, `var p, q = func() {}, func() {}`, has its own signature). `replace_value` on a constant without a value that the constants below it repeat is refused, as a value would change theirs. |
| **Java** | `.java` | ✅ | ✅ `//` + `/* ... */` + Javadoc `/** ... */` | — | `class`, `interface`, `enum`, `record`; methods, constructors, fields. Annotations (`@Override`, `@Deprecated`) travel with their method on edits (wrapped in the `modifiers` node); `replace_signature` keeps them unless the new signature includes its own, and also works on interface and abstract methods, which have no body. Enum methods (nested in `enum_body_declarations`) discovered via BFS in `list_symbols`. Fields and interface constants are listed as `field A.x` and targetable; a field that shares its name with a method is the name's target, and the method is `A.x()`. Overloaded methods and constructors take their parameter types (`A.m(int)`, `A.m(String...)`, `A.join(List<String>, String)`, `A.A()`), without modifiers and annotations; the bare name is refused with the targets to use. Nested classes, enums, interfaces and records are listed with their members, and a nested class whose name a field takes is left out, its members still listed (`A.B.m`). |
| **JSON** | `.json` | ✅ (keys, values, arrays) | — (no comment syntax) | — | Numbers with a `+` in the exponent (`6.02E+23`), which the grammar cannot parse on its own, are read as whole values, so `replace_value`, `delete_key` and `add_key` beside them work as for any other number. |
| **YAML** | `.yml`, `.yaml` | ✅ (keys, values, sequences) | ✅ `#` | — | Block and flow sequences supported. Keys inside flow mappings are addressable by path (`a: {x: 1}` is `a.x`, nested ones too), so `read_symbol`, `replace_value` and `delete_key` work on them and they stay inline. `add_key` also adds to flow mappings (`{a: 1}`, `{}`) and to a key with no value (`parent:`). A key with no value can be read (as an empty string), commented, and given a value with `replace_value` (`a: 3`; content of several lines, or a block sequence or mapping, goes below the key one level deeper); an explicit key with no value (`? a`) gets its value on a `: 3` line below it, in the column of the `?`. In a multi-document file a path is looked up in each document in turn (`add_key` with `parent_target=""` adds to the first document with content). Quoted keys are matched by the name they spell, so `'it''s'` is target `it's` and `"caf\u00e9"` is `café`. Removing the only item of a block sequence leaves `key: []`. |
| **TOML** | `.toml` | ✅ (keys, values, arrays, tables) | ✅ `#` | — | `[table]` headers (dotted and quoted ones too, `[tool.uv]`, `[a."b.c"]`) addressable by their names for comment tools and `add_key`. Keys inside inline tables (`t = { a = 1 }` is `t.a`, nested ones too) and dotted keys (`x.y = 1` is `x.y`) are addressable by path. `add_key` also adds to inline tables (`t = { a = 1 }`, `t = {}`). |

**Cross-cutting features:**

- **Decorated functions and classes** (Python and JS/TS `@decorator`, Java annotations): decorators are part of the symbol. They are preserved on body edits; `read_symbol`, `replace_function` and `delete_symbol` include them; a leading comment belongs above the first of them (`edit_leading_comment` puts it there and finds an existing one there), and `insert_sibling(position="before")` goes above them and that comment, so the comment stays on the symbol it documents. This holds for TypeScript methods too, whose decorators the grammar places beside the method in the class body, for decorated Python classes, for a C++ template's `template <...>` header and for a Ruby Sorbet `sig`. `replace_signature` keeps them as written unless the new signature includes its own, which then replace them.
- **Layout-aware `add_method` / `add_field`**: the new member is indented like the class's existing members (falling back to the file's indent unit, then the language default: 4 spaces for Python and Java, a tab for Go, 2 spaces otherwise), keeping the content's relative indentation; in a class indented with tabs that indentation is written in tabs too (one per indentation step of the content), so a snippet written with spaces fits a tab-indented file. The lines that start inside a multi-line string literal of the content (a triple-quoted string, template literal, heredoc, raw string, C string continued with `\`) or a Ruby embedded document (`=begin` ... `=end`, at column 0) keep their text, here and in every tool that re-indents content (`replace_function`, `replace_function_body`, `insert_in_body(at=...)`, `insert_sibling`, `edit_leading_comment`); a Python docstring, a Ruby `<<~` heredoc and a Java text block, whose indentation the language strips, move with the code instead, every line by the same amount. A method follows the last member after one blank line, or none when the class's methods have no blank lines between them (a C++ member template counts as a method; TS interface members stay one per line) or the last member is a C++ access label (`public:`), and one-line content after stacked one-line members (fields) is stacked with them. In a C++ class whose last method is followed by an access label (`public:`, `protected:`, `private:`), the new method goes right after that method, spaced as methods are, so it gets the same access; a comment after that method on its line stays with it, a method inside an `#if`/`#ifdef` block (either branch) counts as the last method (the new one goes after the `#endif`), and a label moved off a method's line is indented like the class's other labels, or at the class's indentation. A C++ class with no label after its last method, or with no methods, gets the new method after its last member. Content in `read_symbol`'s shape copied from another class (its first line without indentation, the later lines at the indentation they had there) is indented as a whole, a Python def without decorators only when it was read at the indentation of this class's members. A field goes before the first member (after a Python class docstring, and below the access label a C++ class body opens with, so it gets that section's access), with a blank line after it when that member is a method (a C++ member template counts), and a comment after the `{` on its line (`class K { // NOLINT`) stays there. In a Java enum both go after the constants, and a `;` is added after them when they have none. No blank line is added before the closing brace, and a braced body on one line (`class K {}`, `class K { a() {} }`, Go `struct{ A int }`) is opened up over several lines, its existing members on a line of their own. A Python or Ruby class written on one line (`class K: pass`) is refused. Both tools look only at containers, so a name a struct or interface shares with a function (`struct stat` and `int stat(...)`) is the struct, a typedef of a struct declared apart (`typedef struct Node Node;`) is that struct, and a function target is refused (use `insert_in_body`).
- **Layout-aware `add_key`**: in JSON objects, Python dicts, JS/TS object literals, YAML flow mappings and TOML inline tables the new entry follows the container's layout (one key per line at the existing indentation, inline objects stay inline, a trailing comma is kept, and a comment on the last entry's line stays there); an empty JS/TS or YAML `{}` becomes `{ key: value }` (TOML: `{ key = value }`). A YAML key with no value gets a nested block mapping one indentation level deeper (inside a flow mapping, an inline `parent: { key: value }`). YAML and TOML keys are quoted only when they need it: `my key` and `a.b` stay plain in YAML, while `a:b`, `yes`, `123` or a leading `-`/`#`/`&` get quotes (in the quote style most of the file uses); in TOML anything but a bare key (letters, digits, `_`, `-`) is quoted, and dots separate the parts of a dotted key, so `b.c` adds `c` under `b` and a name with a dot in it must be passed quoted (`'"example.com"'`). A JSON or YAML key passed already in quotes is used as given. A key the container already has is refused with a pointer to `replace_value`, for every format, naming a target that works (quoted where the key has a dot, inside flow mappings and inline tables too).
- **Layout-aware `append_to_array`**: JSON arrays, TOML arrays, YAML flow sequences and Python lists keep their layout: an inline array stays inline (`[1, 2]` becomes `[1, 2, 3]`, `[]` becomes `[3]`), an array with one element per line gets a new line at the elements' indentation (also one aligned under its `[` whose `]` closes the last line), a trailing comma is kept (the new element gets one too), and comments are not elements: one on the last element's line stays there, after the comma (`2,  # two`), and a value that ends with a comment gets its comma before it (`3,  # new`). A Python list whose `[` and `]` are on lines of their own with nothing between them gets the value on a line of its own, with a comma. A YAML block sequence gets a new `- value` item.
- **Config paths from the top level**: a dotted config target is followed from the top of the file, one key per segment: `a.b` is key `b` under the top-level key `a`, never an `a.b` nested deeper (such as `x.a.b`), and in TOML a plain `version` is a top-level key, not one inside a `[table]` (use `project.version`). One path syntax covers JSON, YAML and TOML and every config tool (`read_symbol`, `replace_value`, `add_key`'s `parent_target`, `delete_key`, `append_to_array`, `remove_from_array` and `edit_leading_comment`): a segment in double quotes is one key taken literally, dots included, its escapes resolved as in a JSON string (`"editor.fontSize"`, `a."b.c".d`), and in TOML single quotes work too (`a.'b.c'`), as in a TOML dotted key, whose syntax TOML paths share. Keys compare by the names they spell, so `"a.b"` finds `a.b: 1`, `'a.b': 1` and `"a.b": 1` in YAML, and `[x.'y.z']` and `[ x . "y.z" ]` are both TOML table `x."y.z"`. Keys inside YAML flow mappings and TOML inline tables are part of the path. Elements of arrays are not addressable by path (`a.0` is a key named `0`).
- **Byte-correct slicing**: multi-byte characters (emoji, `═`, `→`) handled safely in source text.
- **Import placement**: `add_import` skips exact duplicates. A new import goes into the group it belongs with, a group being a run of imports, includes or requires with no blank line or other code between them (comments may be; a conditional of includes counts as one include, a CommonJS `require` as an import). The group is picked by language. Python: a group holding imports of the same top-level package, else the standard-library group for a standard-library module and the group of other modules for others, relative imports with relative imports. C/C++: `<...>` with `<...>`, `"..."` with `"..."`, never a group of only the file's own header (`"foo.h"` in `foo.cpp`). Go: standard-library paths (no dot in the first path element) with standard-library paths, others with others, never with cgo's `import "C"`. JS/TS: relative paths (`./`, `../`) with relative paths, packages with packages, a `require` with requires. Java: static with static. Ruby: `require_relative` with `require_relative`. When several groups fit, the one with an import sharing the longest leading path with the new one wins (package parts, directories: `from app.y import z` goes with `from app import x`, `#include <sys/types.h>` with `<sys/stat.h>`), else the first; when none fits, the import goes after the file's last import. In a group that is already sorted it goes at its sorted place, else after the group's last import. Python sorts as isort and ruff do: module names without case, numbers in numeric order, `import x` lines before `from x import` lines in a group that orders them that way, else both together. The other languages sort by the path or module text (C/C++ includes as written, with their brackets or quotes). The import goes above the next import and the comment lines right above it, but below a comment above the group's first import, which heads the group; a comment that applies to the next line stays on its import even when the new import sorts first (`@ts-ignore`, `@ts-expect-error`, eslint or tslint `disable-next-line`, pylint `disable-next`, `biome-ignore`, `deno-lint-ignore`, `prettier-ignore`, `NOLINTNEXTLINE`, Flow suppressions, istanbul, c8 or v8 `ignore next`, Go `//nolint` and `//lint:ignore`, `noinspection`). The blank lines between groups stay as they are. Python `from __future__` imports stay first: a new one goes after the existing ones, or before the file's first import, and other imports go after them; a `from __future__` import that its group would place after code goes right after the module docstring, before any other statement, with a blank line on each side. Go: inside an `import ( ... )` block the same rules apply to the block's blank-line groups of specs, an alias sorting by its path (`"io"` goes between `"fmt"` and `"os"`); a block written on one line (`import ("fmt"; "os")`, `import ()`) takes the spec inline at its sorted place; without a block, a spec given without its keyword (`"fmt"`, `f "path/filepath"`) is written as `import "fmt"`; and nothing goes between `import "C"` and its cgo preamble comment (the new import goes after `import "C"`). A file's first import goes after its preamble (a package clause, `#!` and encoding or magic comments, the module docstring, `'use strict'` and other directives, TS `///` lines, `#pragma once`, an include guard's `#define`, and a header comment set off by a blank line), with a blank line above it and one below it (two before a Python def or class).
- **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`. The block ends at a blank line and at a comment line indented deeper than the symbol, which belongs to the code above it (`    # end of f` under f's last line, a commented-out YAML list item), for this and every tool that finds a leading comment: `insert_sibling(position="before")` goes below such a comment, and `edit_leading_comment` neither removes nor replaces it. Such comment lines right below a symbol's last line belong to it: `delete_symbol` removes them with it and `insert_sibling(position="after")` goes below them (`read_symbol` and `replace_function` leave them out). A Ruby `=begin` ... `=end` document directly above a symbol is its leading comment. Only real comments count, for this and every other tool that finds a leading comment: a line that starts with `#` or `//` inside a multi-line string, template literal or YAML block scalar is text. A `#!` line and the encoding or magic comments after it (`# -*- coding: utf-8 -*-`, `# frozen_string_literal: true`) at the top of a Python or Ruby file are never taken for a symbol's comment, by this or any other tool.
- **Layout-aware deletion**: the blank lines that separated a deleted symbol go with it, so the rest keeps the file's spacing: deleting the first symbol of a file or class body leaves no blank lines at the top or after the `{`, deleting the last one leaves none before the closing brace (and the next top-level symbol keeps its own separation, such as Python's two blank lines), and between the two symbols that remain only the wider of the two gaps stays. Only empty lines and lines of spaces and tabs are blank: a form feed line (a Python page break) next to the symbol stays. A symbol that shares a line with other code (`class A { a() {} b() {} }`, `x = 1; y = 2`) is cut out of that line instead, with the `;` or `,` that ends it (the last entry of a one-line literal takes the one before it: `{ a: 1, b: 2 }` minus `b` is `{ a: 1 }`) and a comment after it at the end of the line; the spaces that were before it stay between the code on both sides. `replace_function` likewise replaces only such a symbol's own text, and refuses content that ends with a `#` or `//` comment when code follows the symbol on its line.
- **Syntax-checked saves**: before anything is written, the edited file is re-parsed with its tree-sitter grammar. An edit that would introduce a syntax error into a file that parsed cleanly is refused with the error's line and column (e.g. `Edit refused: the result would have a syntax error at line 11, column 2 (missing '}')`), and the file is left unchanged. Files that already had syntax errors are not guarded, so you can keep working on them. For `.py` files Python's own parser (that of the interpreter running the server) decides: it catches the indentation errors tree-sitter lets through and accepts newer syntax the grammar does not know (PEP 696 type parameter defaults). For `.json` files the `json` module overrules the grammar wherever it would reject valid JSON, and comments in JSONC files stay allowed; exponents written with a `+` (`1e+2`), which tree-sitter-json cannot parse, are masked before parsing, so files that use them are checked like any other. Other languages have no second parser, so valid code using syntax the grammar does not support (very new language features, macro-heavy C/C++) can be refused: if you are sure the code is valid, make that edit with your normal edit tool.
- **Line endings and byte order marks preserved**: CRLF files stay CRLF and LF files stay LF, whichever tool makes the edit, and blank lines at the end of a file are kept as they were (deleting the last symbol does not leave extra ones behind); a file whose last line has no line break keeps it that way. A UTF-8 byte order mark stays at the start of the file, whatever the edit adds or removes at the top; it never shows up in tool output, and content that starts the file with one leaves a single mark. Files that are not UTF-8 (UTF-16, UTF-32, Latin-1), also UTF-16 or UTF-32 without a byte order mark and files with NUL bytes, are refused with a message saying so, and left unchanged. Write content, snippets and anchors with `\n` line breaks: inserted text is converted to the file's style, and a `\n` in a snippet or anchor matches a CRLF line break. Content is split into lines on `\n` (or CRLF) only, so a form feed, vertical tab, U+0085, U+2028 or U+2029 inside a string literal or comment stays where it was written. Only spaces and tabs count as indentation, so when content is re-indented a form feed at the start of a line (a Python page break, `\fdef g():`) or a U+2028 starting a line inside a string stays, and a form feed before the target's own `def` is not copied onto the new lines. In a file that mixes CRLF and LF, every line the edit does not rewrite keeps its own ending (the lines next to the edit too, though they read like lines it removed or added); a line it writes takes the ending of the lines it replaces when they agree, else of the lines on both sides when they agree, else the file's most common one.
- **Body snippets and anchors** (`replace_in_body`, `delete_in_body`, `insert_in_body(after=…, before=…)`): a snippet matches its exact text (`\n` matching CRLF) where it neither starts nor ends inside a token of code, an identifier, number, keyword or operator: `x = 1` is not found in `max = 10`, nor `= 1` in `a == 1`, `el` in `$el` or `foo` in Ruby `foo?`; inside a comment or string (not in an interpolation's code) it may start or end between any two words, and it may end at a Python backslash continuation. Without an exact match, its lines match whole lines of the body with one indentation added to or taken from each of them, so a block copied at column 0 or deeper than in the file is found: blank lines match blank lines, spaces and tabs at line ends do not count, tabs and spaces differ, and a snippet that starts or ends in the middle of a line must match exactly. The text of a multi-line string is never shifted: a line that starts inside one is matched only as written in the file (Python docstrings, Ruby `<<~` heredocs and Java text blocks, which move with the code, aside). An exact match wins, except that when every exact match is inside a string or comment, shifted matches in other places count too. The match must be unique: two or more places are refused with their line numbers (`Snippet matches 2 places in body of 'f' (lines 2, 4)`), and text found only inside longer tokens is refused saying so. A deletion that leaves its lines without code takes the whole lines, indentation and line break included; a comment after the deleted code on its last line stays, at the line's indentation; a Python body is never left without a statement (replace the snippet with `pass`). New text goes in indented as the snippet matched: moved with a shifted match's lines; after an exact one-line snippet that left out its line's indentation (or starts mid-line), its later lines given that indentation unless they are that deep already; after an exact snippet of several lines, or when already at the file's indentation there, as written; in Python, when one reading of the indentation does not parse and the other does, the other is taken. `insert_in_body` puts new text on lines of its own when the anchor ends its line (`after`) or starts it (`before`), indented like the anchor (new text at column 0 beside an anchor written with its indentation goes at the anchor's, and text in `read_symbol`'s shape goes in as written), and inside the block that an `after` anchor's line opens (`if (x) {`, `if x:`); in the middle of a line, or where the new line would start in the text of a string or heredoc (not a docstring, `<<~` heredoc or Java text block, whose lines are indented like code), it is placed byte for byte. Lines of new text that go into a string of the file keep their text.
- **Tab-indented code**: every tool that writes source content over several lines (`replace_function`, `replace_function_body`, `replace_in_body`, `insert_in_body`, `insert_sibling`, `add_top_level`, `add_method`, `add_field`) writes the content's indentation steps in tabs where the file indents with tabs there: the place's indentation has a tab, or, at column 0, most of the indented lines of the code it replaces or goes beside start with one (the text of its strings and lines aligned under a bracket aside), or, with no such lines, the file is Go or mostly tab-indented. Each step of the content's own indentation becomes one tab; the text of multi-line strings, Ruby `=begin` documents, and the spaces that align a block comment's lines or a line continued inside a `(` or `[` stay as written. Config files are never converted.

### 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), indented with tabs and with a blank line on each side.
- **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_sibling("Cache", content, position="after")` 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, and a JS/TS `export` or C++ `template <...>` header): exactly what `read_symbol` returns, so its text can be edited and given back as is, and given back unchanged leaves the file byte for byte as it was; that shape may hold several members (a method replaced by two, a field by two fields, a method and a decorated one), each starting at the symbol's indentation once the one before has ended. Content written relative (first line at column 0, the rest indented from it) or at its file indentation is re-indented as before. When no line sits at the symbol's own indentation to tell these shapes apart, content whose later lines are all deeper than the symbol's indentation is taken as `read_symbol`'s text and keeps them where they are (a 4-space method body for a 2-space class stays 4 spaces in); a decorator or annotation at the symbol's indentation counts only before the first line of code; at column 0 the shapes are one and content goes in as written. A comment before the symbol on its first line (`/** @type {X} */ let x;`) is no part of it and stays, and the content's later lines are indented as that line, or as the line where a block comment ending there (` */ void f() {`) starts. A comment after the symbol on its last line stays unless the content ends with one, and so does a `;` or `,` after it unless the content ends with it (a JS/TS class field's goes when the content is a method). A name declared together with others (`int a = 1, b = 2;`, `struct S {...} s;`) has that declaration replaced, and content that would drop the others is refused. A symbol that shares a line with other code has only its own text replaced (see Layout-aware deletion above). Refused in JSON, YAML and TOML files (use `replace_value`). |
| `replace_function_body` | `file_path`, `target`, `content` | Replace only the body of a function, preserving signature and decorators. A body written on one line with its signature or braces (`function f() { return 1; }`, `def f(): return 1`) is rebuilt over lines of its own, one level deeper than the function, as is content for an empty body (`{` and `}` on lines of their own, and an empty Ruby method, `def f` over `end` or `def h; end`, or one holding only comments, which the content replaces); a comment that ends a one-line Python body stays at the end of the def's line (`def b():  # old`). A Ruby endless method (`def f = x`) is refused. In a Ruby method with `rescue`, `else` or `ensure` clauses only the code before the first clause is replaced and the clauses stay: `def f\nrescue => e\n  log(e)\nend` with content `1` gives `def f\n  1\nrescue => e\n  log(e)\nend`. With no code before the clause the content goes one level deeper than the def, as in an empty method; a clause on the same line as code moves to a line of its own; content with clauses of its own replaces the whole body, clauses included. When a one-line Ruby body is rebuilt, clauses that share a line with their code or the `end` are rebuilt too (`def f; x; rescue; y; end` becomes `def f\n  1\nrescue\n  y\nend`); clauses already on lines of their own keep their text. |
| `replace_signature` | `file_path`, `target`, `new_signature` | Replace only the signature, preserving the body. `new_signature` is everything from the start of the definition up to its body (`def get(self, key):`, JS `get(key, d)` without the `{`). Existing decorators (Python, JS/TS), Java annotations and a C++ `template <...>` header are kept unless `new_signature` includes its own, which replace them. Indentation on the signature's first line is ignored, so `"def get(self):"` and `"    def get(self):"` both work for a method; the lines after it may be indented relative to it (`"@staticmethod\ndef get(key):"`) or written as `read_symbol(depth="signature")` returns them. For a JS/TS `const f = (a) => {...}`, `"const f = (a, b) =>"` replaces from the `const` on and `"(a, b) =>"` only the function's own part; likewise a closure's key (`handler: (a) =>`), a TS `declare`, a C/C++ `typedef` or `extern "C"`, a Go `type` or `var`, a Ruby `private` and a Sorbet `sig` are replaced by a new signature that starts with its own, and kept otherwise (an `export` stays; a new closure key renames it). A TS overload group and a Python `@overload` group have the implementation's signature replaced, and a Ruby endless method keeps its `= expression`. A method declared without a body (a TS abstract method, a Java interface or abstract method, a C++ `A() = default;`, a Go function without one, a C/C++ prototype or a method declared in its class, `int f(int);`) is all signature but its final `;`, which stays whether or not `new_signature` ends with one. A Go struct or interface type and an empty Ruby class, module or method take the header `read_symbol(depth="signature")` gives them (`type A struct`, `class A < B`); a variable or constant has no signature and is refused. |
| `replace_in_body` | `file_path`, `target`, `old_snippet`, `new_snippet` | Replace a snippet inside a function body, scoped to the target's body: its exact text at token boundaries, else its whole lines with their indentation shifted, matching exactly one place (see Body snippets and anchors above). `new_snippet` is indented as the snippet matched; `''` deletes. |
| `delete_in_body` | `file_path`, `target`, `snippet` | Delete a snippet inside a function body, matched as `replace_in_body` matches one. Lines left without code go whole, with their indentation and line break; a comment after the snippet stays; a Python body keeps a statement. |
| `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). `at` inserts at the body's own indentation level even when the body ends in a nested block; a Python docstring stays first; a body written on one line with its signature or braces is refused (rewrite it with `replace_function_body`); an empty body (also an empty Ruby method) takes the content one level deeper than the function. An anchor is matched as a `replace_in_body` snippet; one that ends (`after`) or starts (`before`) its line gets the new text on lines of its own, indented like it and inside the block its line opens (see Body snippets and anchors above), else byte for byte, as does an anchor in a body on its signature's line. A line break at the start of `new_snippet` after an anchor that has none, and spaces after its last line break, are taken for the anchor line's own, and a final line break is added. In an empty body there is no code to anchor to (an anchor on its brace inserts byte for byte); use `at="top"` or `at="bottom"` there. `after` an anchor that ends with the body's closing brace is refused. |
| `add_top_level` | `file_path`, `content`, `position="bottom"` | Insert top-level content. `position="bottom"` appends at end of file (default), in a header inside its include guard, before the `#endif`; `position="top"` inserts after the preamble (as for a first import: package, `#!` and magic comments, directives, imports and includes, a header comment, the Python module docstring, a header's include guard) and before the first real declaration, without splitting a comment from the code it documents. The content is separated from its neighbours by one blank line, two in a Python module when either is a def or class (PEP 8); at the top, more blank lines the file had there stay below it; at the bottom, one-line content after one-line code that is stacked with the code above it is stacked too, but not after imports, includes or a package clause (unless it is an import itself). `read_symbol`'s text of a nested symbol (recognised by its closing line or a decorator) is moved to column 0 as a whole. |
| `add_method` | `file_path`, `class_target`, `content` | Add a method at the end of a class body (in a C++ class, right after the last method when an access label follows it), indented like the class's members and separated as its methods are (see Layout-aware `add_method` / `add_field` above). |
| `add_field` | `file_path`, `class_target`, `content` | Add a field/attribute/member at the top of a class body (after a Python class docstring or a Java enum's constants, below the access label a C++ class body opens with), indented like the class's members. |
| `insert_sibling` | `file_path`, `target`, `content`, `position` | Insert content as a sibling of a named symbol. `position="before"` or `"after"`; `"before"` goes above the symbol's leading comment block, decorators, a JS/TS `export` and a C++ `template <...>` header. The new code goes into the gap between the target and the symbol on that side (with that symbol's comment, decorators and `export`) and is spaced like it: next to a function, class or other code over several lines, by that gap or the usual one when wider (one blank line, two at the top level of a Python module around a def or class, PEP 8); between one-line code (fields, constants, prototypes, macros) stacked without blank lines, stacked too (`int f(void);` `int h(void);` `int g(void);`), also after the last stacked field or before the first, and below stacked fields before a method; the end of a body or file, a C++ access label and a comment above the new code get no added blank lines. `"before"` keeps a `#!` line and encoding or magic comments first. `"after"` goes below comment lines right under the target that are indented deeper than it (they end its code), and below a block comment after the target on its last line that goes on over more lines. `read_symbol`'s text is indented as a whole, also when it holds several members (see `replace_function`). Content is indented in tabs next to a tab-indented target, as for `add_method`. `"before"` is refused when other code comes before the target on its first line, `"after"` when other code follows it on its last line or the line starts with the code the target is part of (`class A: x = 1`), and empty content is refused, as is the tool in JSON, YAML and TOML files. |
| `delete_symbol` | `file_path`, `target`, `include_leading_comments=True` | Delete a function or class definition block (including decorators, a JS/TS `export` and a C++ `template <...>` header) and the blank lines that separated it (see Layout-aware deletion above). By default also consumes the contiguous leading comment block above the symbol and its decorators (Godoc, Javadoc `/** ... */`, `#` or `//` comments); pass `include_leading_comments=False` to keep it. A Ruby `=begin` document and a Sorbet `sig` above a method go too, and so do comment lines right below the symbol indented deeper than it. A comment before the symbol on its first line (`/** @type {X} */ let x;`) counts as a leading comment: by default it goes with the symbol, also when it is a block comment that began on an earlier line and when code follows the symbol on its line (`/* c */ let x; let y;` minus `x` is `let y;`); with `include_leading_comments=False` it stays, on a line of its own (`/** @type {X} */` above the next line). A name declared together with others (`int a, *b;`, Java `int x, y;`, C++ `auto [x, y] = p;`, `struct S {...} s;`, Go `var a, b = 1, 2`, JS `const a = 1, b = 2;`) is refused, naming them, rather than deleted with them: give `replace_function` the declaration without that name instead. Refused in JSON, YAML and TOML files (use `delete_key`). |

### Parameters & signatures

| Tool | Parameters | Description |
| :--- | :--- | :--- |
| `add_parameter` | `file_path`, `target`, `parameter`, `position="end"` | Add a parameter to a function signature (`position`: `"start"` or `"end"`; any other value is refused). A list written one parameter per line (aligned under the first one too) gets the new one on a line of its own at the parameters' indentation, with a trailing comma when the last parameter has one; comments are not parameters and stay on their lines. A list with two parameters on a line gets it inline (`a, b, c`). An empty list over two lines (`def f(\n):`) gets the parameter on a line of its own, one level deeper (with a trailing comma in Go, and in Python, JS and TS as the file's other lists have it). A name the function already has is refused (`Parameter 'a' already exists in 'f'`), names in destructuring patterns and Go groups included; several `_` are fine in Go, Java and Ruby. A C or C++ `(void)` list gets the parameter in place of `void` (`f(void)` becomes `f(int c)`; `void *p` is a parameter). `"end"` goes before the parameters that must stay last: a Python `**kwargs` (`f(a, **kw)` plus `b` is `f(a, b, **kw)`; after `*args` or a bare `*` the new parameter is keyword-only), a JS/TS rest parameter (`function f(a, ...rest)` plus `b` is `function f(a, b, ...rest)`), a Go variadic one (`func f(a int, xs ...int)` plus `b string` is `func f(a int, b string, xs ...int)`), Java varargs (`String... xs`), the `...` of C and C++, a C++ parameter pack (`Ts... xs`) and a C/C++ macro's GNU named variadic parameter (`#define M(a, args...)` plus `b` is `#define M(a, b, args...)`). On the old C++ form `int a...` outside a template, the new parameter goes before the `...` (`int g(int a, int b...)`); `int...` and template functions keep it before the variadic parameter. A C++ `int f(...)` gets `int f(int b, ...)` at either position. In Ruby the new parameter goes before the kinds that must come after it, in Ruby's order: required, optional, `*splat`, required, keyword, `**opts`/`**nil`, `&blk`, `...`; so `b` goes before keyword parameters, `b = 1` before the splat, `*r` before keywords and `k: 1` before `**opts` (`def f(a, **opts, &blk)` plus `b` is `def f(a, b, **opts, &blk)`). In a list written one per line the new parameter gets its own line above them. `"start"` goes after a TS `this` parameter or a Java receiver parameter, which must stay first (`function f(this: Window, a)` plus `b` is `function f(this: Window, b, a)`); in a list written one per line, above the first parameter and the comment lines right before it. |
| `remove_parameter` | `file_path`, `target`, `parameter_name` | Remove a parameter by name. One on a line of its own goes with that line and the comment at its end; comments on other lines stay (one after the parameter before it too), a last parameter with no comma after it takes the comma before it (except in Go before a line break, which needs it), and removing the only parameter leaves `()`. A Go name of several that share a type goes alone (`func f(a, b int)` minus `a` is `func f(b int)`), the other names keeping their comments; a type-only Go parameter, the receiver and named results are not parameters; a comment after the comma before a last Go name stays. A name bound by a JS/TS or Ruby destructuring parameter goes alone (`{a: x, b}` minus `x` is `{b}`; in an array or Ruby pattern a hole or `_` keeps the positions, `[a, b]` minus `a` is `[, b]`), and the parameter goes when it was its only name; a key (`a` of `{a: x}`) is not a name. In a C file (`.c`, `.h`) removing the last parameter leaves `(void)`, not `()` (unspecified parameters before C23), and removing the last named parameter before a `...` is refused, since `f(...)` is invalid C before C23; C++ keeps `()` and allows `f(...)` (`int f(int a, ...)` minus `a` is `int f(...)`). In Python a bare `*` goes when only `**kwargs` would follow it (`f(a, *, b)` minus `b` is `f(a)`, `f(a, *, b, **kw)` minus `b` is `f(a, **kw)`), and a `/` goes when the removed parameter was the only one before it (`f(a, /, b)` minus `a` is `f(b)`). Removing `*args` leaves a bare `*` when keyword-only parameters follow it (`f(a, *args, b)` minus `args` is `f(a, *, b)`, and `f(a, *args, b=1, **kw)` gives `f(a, *, b=1, **kw)`); with nothing or only `**kwargs` after it, `*args` just goes (`f(a, *args, **kw)` gives `f(a, **kw)`). |

### Imports & includes

| Tool | Parameters | Description |
| :--- | :--- | :--- |
| `add_import` | `file_path`, `import_text` | Add an `import`/`from`/`#include`/`require` line. Skips duplicates. Goes into the group of imports it belongs with, at its sorted place when the group is sorted, else after the group's last import, and after the file's last import when no group fits; a first import goes after the preamble with a blank line around it (see Import placement above). For Go, `import "foo"` and a spec alone (`"foo"`, `alias "foo"`) both work: the spec goes inside an existing `import ( ... )` block, or is written as `import "foo"` without one. |
| `remove_import` | `file_path`, `import_text` | Remove a matching import line, compared after stripping surrounding whitespace: pass the exact 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, `import type {...}` and `import D, {...}` too). Idempotent. When the names are sorted the new one goes at its sorted place, else at the end. Sorted means plain, case-sensitive order (a single name always is), or else isort's and ruff's order by type: CONSTANT_CASE, then CamelCase, then the rest, each without case (`from typing import TYPE_CHECKING, Any, cast` plus `Dict` gives `TYPE_CHECKING, Any, Dict, cast`); names that fit both use plain order (`Any, Dict` plus `TYPE_CHECKING` gives `Any, Dict, TYPE_CHECKING`). A name sorts by the name it imports (`b as bb` as `b`, a TS `type B` as `B`). The list keeps its layout: names written one per line in parentheses or braces get the new one on a line of its own at their indentation (aligned under the `(` or `{` when they are), above the next name and the comment lines right above it, or at the end after the last name's line and its comment, with the trailing-comma style kept (`from x import (alpha, gamma)` one per line plus `beta` gives `alpha`, `beta`, `gamma`, one per line); a comment heading the list stays first, and a next-line directive (see Import placement above) stays on the name below it. Inline lists, and a Python list without parentheses, stay inline, the new name going before the next one (`from x import a, c` plus `b` gives `a, b, c`; `import { a, c } from "m"` plus `b` gives `{ a, b, c }`). For a default or namespace import (`import Foo from ...`, `import * as ns from ...`) use `add_import` with the full line. |
| `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; removing the last name beside a default binding (`import D, { a } from "m"`) is refused, as it would leave empty braces: use `remove_import`, then `add_import` the default binding. In parentheses or braces, a name on a line of its own goes with that line and the comment at its end, comments on other lines stay, and a last name without a comma takes the comma before it. |

### Comments & docstrings

| Tool | Parameters | Description |
| :--- | :--- | :--- |
| `edit_leading_comment` | `file_path`, `target`, `op`, `comment=""` | Edit the contiguous leading-comment block above a named symbol, which is above its decorators/annotations, a JS/TS `export` and a C++ `template <...>` header; a symbol that other code comes before on its line has none of its own and is refused. `op="add"` inserts (refused if the symbol already has a leading comment; the error quotes it, so a following `op="replace"` can repeat lines worth keeping, such as `// eslint-disable-next-line` or `//go:noinline` directives); `op="replace"` replaces (or inserts if none); `op="remove"` deletes. `comment` includes its markers, is re-indented to the symbol, and is required for `add` and `replace` (ignored for `remove`). Works for `#` / `//` / `/* ... */` / Javadoc `/** ... */` and a Ruby `=begin` document (alone or with `#` lines beside it, kept at column 0); a Ruby Sorbet `sig` and a TS function's overload signatures are part of the symbol, so the comment goes above them; a comment line indented deeper than the symbol ends the code above, so `op="add"` puts the new comment between the two; a `/* ... */` that follows code on its line (`int x = 1; /* n */`) is not taken as a leading comment. In YAML and TOML the target is a key path (a YAML key with no value too) or a TOML `[table]` by its header's names (`tool.uv`); a key that does not start its line (inside a flow mapping or inline table) is refused, since the comment line above would belong to another key. |
| `replace_docstring` | `file_path`, `target`, `new_docstring` | Replace or insert a Python function/class docstring, or the module's for `target=""`. Python-only. `new_docstring` must be one Python string literal with its quotes, for every target: bare text, an f-string, a bytes literal or an empty new_docstring is refused (the error says to write it with its quotes, as in `'"""Summary."""'`) and the file is left unchanged. Inserting one into a body on its signature's line (`def f(): return 1`) is refused: rewrite the body with `replace_function_body`. A new module docstring goes before the first statement (a `from __future__` import stays after it), right below a `#!` line and an encoding comment, and below a header comment set off by a blank line, which keeps its blank lines; a comment right above the first statement stays with it, below the docstring. One blank line follows the docstring (two before a def or class), and in a file with no statement it goes last. An f-string or bytes literal at the top of the module is no docstring and stays. |

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

Values (`content`, `value`) are literal source text in the file's own syntax (`'"2.0.0"'`, `42`): quote strings where the format requires it.

| Tool | Parameters | Description |
| :--- | :--- | :--- |
| `replace_value` | `file_path`, `target`, `content` | Replace the value of an existing key. Targets as for `delete_key`: JSON/YAML/TOML dotted path from the top level (`"editor.fontSize"` for a key with dots; keys inside YAML flow mappings and TOML inline tables too, which stay inline); Python `DictName.keyExpr`; JS/TS `VarName.key` (a shorthand `{ key }` becomes `key: <content>`). A YAML key with no value (`a:`) gets one (`a: 3`, or a nested block below the key for content of several lines or a block sequence or mapping; a first line that is only `\|`, `>-`, an `&anchor` or a `!tag` stays after the colon). In source files it also replaces the value of a variable, constant or field (Python `X = 1` and class attributes, JS/TS `const X = 1;`, each name of `let a = 1, b = 2` alone, and class fields `A.x`, Go `var`/`const`, C/C++ globals, C++ fields and static members, macros, Java fields, Ruby constants and locals, a heredoc's body too): only the value (`int x(1);` and `int y{1};` keep their delimiters), and a declaration without one gets one (`let x;` becomes `let x = 2;`, `X: int` becomes `X: int = 2`, `int g;` becomes `int g = 2;`). Refused: a function, class or type (rewrite it with `replace_function`), a name that shares one value with others (`a = b = 1`, Go `var c, d = f()`), and one that cannot have a value of its own (`extern`, TS `declare`, an abstract field, Go `var e, g int`; for C struct fields and Go constants see the language table). |
| `add_key` | `file_path`, `parent_target`, `key`, `value` | Add a key-value pair to a dict/object/mapping/table. For config, `parent_target` is a dotted path from the top level (use `""` for root; a TOML `[table]` by its header's names, `a."b.c"` for `[a.'b.c']`); YAML flow mappings (nested ones too) and keys with no value and TOML inline tables work too. For JSON and YAML `key` is one name, dots included. For Python it is the dict variable name; for JS/TS the name of a module-level `const`/`let`/`var` (or `export const`) object literal, `as const` / `satisfies` included. Both may continue into nested literals (`CONFIG."db"`, `CONFIG.server`). JSON keys are quoted for you, YAML and TOML keys only when they need it (in TOML, dots make a dotted key), JS/TS keys only when they are not valid identifiers, Python keys are key expressions (`'"timeout"'`). The entry follows the container's layout (one per line, inline, trailing comma), and a key that already exists is refused (use `replace_value`). |
| `delete_key` | `file_path`, `target` | Delete a key-value pair. Targets: JSON/YAML/TOML dotted path from the top level (`"editor.fontSize"` for a key with dots; keys inside YAML flow mappings and TOML inline tables too, cut like JSON entries with their comma); Python `DictName.keyExpr`; JS/TS `VarName.keyName` on `const`/`let`/`var` or `export const` object literals (handles regular pairs, `{ key }` shorthand, methods, and quoted `"complex-key"`). Python and JS/TS paths may continue into nested literals (`CONFIG.server.port`). For JSON, Python and JS/TS, the adjacent comma is also removed (an entry on its own line goes with that whole line, including its same-line comment), and deleting the only entry leaves `{}`; other YAML and TOML keys go with their lines. For an entry of an inline object literal passed as a function argument (`foo({ x })`), use `delete_in_body` on the enclosing function. |
| `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 from the top level. The array keeps its layout: inline stays inline (`[1, 2, 3]`), one element per line gets a new line at the elements' indentation, and a trailing comma is kept; comments are not elements, and the new element's comma goes before a comment (see Layout-aware `append_to_array` above). |
| `remove_from_array` | `file_path`, `target`, `value_match` | Remove the first matching element from a list/array/sequence. `value_match` is compared with each element's source text after stripping whitespace. Comments are not elements: one on its own line goes with that line and the comment at its end, comments on other lines stay (one above it, or after the element before it, too), the trailing-comma style is kept (a last element with no comma after it takes the comma before it), and removing the only element leaves `[]` (a YAML block sequence becomes `key: []`, before a comment on the key's line). |

### Navigation & reading (read-only)

| Tool | Parameters | Description |
| :--- | :--- | :--- |
| `list_symbols` | `file_path` | Formatted outline of all top-level functions, classes, methods and data with line numbers, one `<kind> <name> (line N)` per symbol, the name being its target (a symbol's line is that of its first decorator, `export`, Ruby Sorbet `sig` or first overload signature). JS/TS exported declarations are listed under their own names, an anonymous default export as `default`; generators and functions bound to a `const`/`let`/`var` as `function`, other `const` names as `constant`, `let`/`var` names as `variable` and class fields as `field`, TS abstract classes as `class` (with their abstract methods), and TS enums, type aliases, interfaces and namespaces as `enum`, `type`, `interface` and `namespace`. Nested classes are listed under their containers with their members (`class A.B`, `method A.B.m`), as are Python module variables and class attributes, Ruby constants and locals, C/C++ fields, macros, typedefs, globals, destructors and operators, Java fields and Go type aliases and variables (see the language table). Of a name a scope declares more than once, the target is listed, once. In C, C++, Java, JS and TS every entry is listed under the target that finds it at the listed line: overloads with their parameter types (`method A.m(int) (line 2)`), a C++ or Java method beside a field of its name as `A.x()` (in JS/TS the method is `A.x` and the field is not listed), a C++ definition outside its class by its qualified name (`function A::f`), and a tag or type that shares its name with another symbol after its keyword, so the line reads `struct struct stat (line 1)` or `type type User (line 2)`; an entry no target reaches is left out. In Python and Ruby a name defined again is listed again, and the name targets the first. Names that a pattern binds (`a, b = 1, 2`, `const {a} = o`) are not listed, and a declaration of several names (`int a, *b;`) lists each. Go lists types with their methods, then the names of `var` and `const` declarations, then functions. |
| `find_references` | `file_path`, `target` | Syntactic search for all occurrences of an identifier (no scope awareness, so same-named unrelated identifiers show up too), matched per identifier rather than as a dotted path, returned as `line N: <source line>` in file order, by line and column. |
| `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), from its first byte, so the later lines keep their file indentation: exactly what `replace_function` and `delete_symbol` take (with a Go `type`/`var`/`const`, a closure's key, a TS `declare`, the C typedef or declaration around a struct, a Ruby `private`, a TS abstract method's `;`), ready to be edited and given back to `replace_function` as is; `"interface"` returns a class stub (header + fields + method sigs with `...`, each member at its indentation relative to the class header and ending as written, with its `;` or `,`; a C/C++ class closes with `};`) or a function's signature; `"signature"` returns signature-only (a method with no body, such as a TS abstract or Java interface method or a C++ `A() = default;`, is its whole declaration, and a Go struct its header, `type A struct`). Both start where the full text starts, except that a function's or class's signature leaves out its decorators and a JS/TS `export` (`handler: () =>`, `RunE: func() error`, `declare function f(a: number): void`, `extern "C" int f(int a)`, `private def m(x)`, `typedef struct N`, or `typedef struct` without a tag), and a closure's interface is its signature. A TS overload group and a Python `@overload` group give the implementation's signature, a Ruby endless method its part before the `=`. For a Go type, `"interface"` is its declaration as `"full"` reads it plus its methods' signatures (`type N int` has no signature, but its interface lists its methods). Both refuse a variable, constant, field, type alias, macro or C/C++ forward declaration, saying what it is (`'X' is a constant, which has no signature ...; read it with depth='full'`). A declaration of several names (`int a, *b;`, Java `int x, y;`) reads whole for each of them, and a comment after the symbol on its last line is no part of it. For a config key path, `"full"` (the only depth config files take) returns the key's value (an empty string for a YAML key with no value). |
| `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`). In C, C++ and Java an overload takes its parameter types (`A.m(int, String)`, `A.m()`, C `f(void)` being `f()`), a C++ definition outside its class its qualified name (`A::f`), and a name that a field shares with methods targets the field (the method is `A.x()`). A C/C++ tag or TS type alias or interface that shares its name with another symbol takes its keyword (`struct stat`, `union u`, `class C`, `type User`, `interface Props`), in front of a dotted path (`type N.User`). JS/TS exported declarations are targeted by their own names (`export function add` is `add`); an anonymous `export default function/class` is `default` (and its methods `default.run`), and `const f = () => {...}` is `f`. Decorated 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")` (`"before"` goes above the symbol's leading comment) |
| 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 key (config, Python dict, JS/TS object) | `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** (a route mount, a middleware call, a key of an inline object-literal argument) | `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, and `add_key` refuses a key that already exists.
- **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.

### Escape hatches

- **Deleting several related symbols?** Call `delete_symbol` once per target. There is no `delete_group` tool: the extra calls cost less than a grouped primitive would.
- **Editing a sub-expression inside a method chain** (dropping `.references(...)` from `integer("x").notNull().references(...)`)? Use `replace_in_body` with the literal snippet when it is unique; for arbitrary expression surgery use your normal edit tool, since ast-editor does no pattern matching on expression trees.
- **Rewriting a whole file whose structure changes substantially?** Use your normal write tool.
- **Valid code refused by the syntax check** (very new language features, macro-heavy C/C++)? Make that edit with your normal edit tool.

## Prerequisites

This MCP server uses [`uv`](https://docs.astral.sh/uv/) to manage its Python environment and dependencies automatically. Install `uv` if you don't have it already:

**macOS / Linux:**

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```

**Windows (PowerShell):**

```powershell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```

**Homebrew (macOS):**

```bash
brew install uv
```

**pip (any platform):**

```bash
pip install uv
```

Verify the installation:

```bash
uv --version
```

> For more options (Docker, Cargo, WinGet, etc.), see the [official uv installation docs](https://docs.astral.sh/uv/getting-started/installation/).

**uv 0.9.17 or newer is required.** This server's `pyproject.toml` sets a 7-day dependency cooldown (`[tool.uv] exclude-newer = "7 days"`). Older uv versions can't parse that relative duration. They print a warning and ignore the setting. A plain `uv sync` or `uv run`, including the `uv --directory … run` launch commands below, then discards the committed `uv.lock`, re-resolves to the newest compatible versions (even ones published in the last 7 days), and rewrites the file. Upgrade with `uv self update`, `brew upgrade uv`, or `pip install -U uv`, depending on how you installed it. Upgrading doesn't undo an earlier rewrite: restore the original `uv.lock` with `git checkout HEAD -- uv.lock` in this folder, or copy it again from the archive you downloaded.

The server depends on `mcp`, `tree-sitter`, and a tree-sitter grammar package for each supported language. `uv` handles all of these automatically on first run.

## Installation

> **Note:** Replace `/absolute/path/to/ast-editor` below with the absolute path to the `ast-editor` folder on your machine (the folder that contains its `pyproject.toml`).

### Method 1: CLI Configuration (Claude Code, Codex, Gemini)

If your agent supports adding servers via CLI, run the following:

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

```bash
# 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.

> **Important:** Use the **absolute path** to `uv` for `"command"`, not just `"uv"`. GUI-based MCP clients (Claude Desktop, Cursor) don't always inherit your shell `PATH`, so a bare `"uv"` will fail with a "command not found" error. Get your absolute path with:
>
> ```bash
> which uv
> # e.g. /Users/you/.local/bin/uv  or  /opt/homebrew/bin/uv
> ```

```json
{
  "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/config/mcp_config.json` — see [Antigravity IDE](#antigravity-ide) (uses `uvx --from`) |
| **Gemini CLI** | `~/.gemini/settings.json` (Global) or `.gemini/settings.json` (Project) |

### Antigravity IDE

The Antigravity IDE uses its own MCP config file and a `uvx`-based command — **not** the `uv --directory … run` form shown above. Add the server to `~/.gemini/config/mcp_config.json`:

> **Important:** Use the **absolute path** to `uvx` for `"command"`, not a bare `"uvx"` — Antigravity doesn't inherit your shell `PATH`. Find it with `which uvx` (e.g. `/opt/homebrew/bin/uvx`).

> **Note:** `uvx` ignores both this server's `uv.lock` and the `[tool.uv]` settings in its `pyproject.toml`. It re-resolves dependencies on each launch instead of using the locked versions, and the 7-day cooldown is passed explicitly as `--exclude-newer "7 days"` in `args`, so you get the newest compatible releases that are at least 7 days old. This requires uv 0.9.17 or newer: older versions reject the value (`invalid value '7 days' for '--exclude-newer <EXCLUDE_NEWER>'`) and the server fails to start.

```json
{
  "mcpServers": {
    "ast-editor": {
      "command": "/absolute/path/to/uvx",
      "args": [
        "--exclude-newer",
        "7 days",
        "--from",
        "/absolute/path/to/ast-editor",
        "ast-editor-mcp"
      ]
    }
  }
}
```

### Using Standard Python (Fallback)

If you prefer not to use `uv`, run the following from the `ast-editor` folder (the one that contains its `pyproject.toml`), then point to the `.venv` executable it creates. `python3` must be Python 3.10 or newer.

```bash
python3 -m venv .venv && source .venv/bin/activate && pip install .
```

This path doesn't use uv, so the `[tool.uv]` 7-day dependency cooldown doesn't apply. With pip 26.1 or newer, add `--uploaded-prior-to P7D` to the `pip install` command to apply the same cooldown.

```json
{
  "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-editor` when editing code and config files. The agent prompt lives in [`AST-EDITOR.md`](./AST-EDITOR.md) — a standalone file you wire into your agent's system instructions. It is kept short, since it is loaded in every session: it covers how to pick and call each tool, and this README is the full reference. The server also sends a short, condensed version of this guidance (start with `list_symbols`, pick the narrowest tool, what to do when an edit is refused for a syntax error) as MCP server `instructions`, which some clients, Claude Code among them, pass to the model automatically alongside any prompt file. Wiring in the full prompt file is still recommended. After updating the server, copy the new `AST-EDITOR.md` over the one you installed (for example `~/.claude/AST-EDITOR.md`), since an installed copy does not update itself.

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

```bash
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`](./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 |

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

- **macOS:** `~/Library/Logs/Claude/mcp*.log`
- **Windows:** `%APPDATA%\Claude\logs\mcp*.log`

For interactive testing, use the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector), which provides a UI for exercising each tool with custom parameters and inspecting responses.

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 issues