Surgical AST-aware editor for Go code with MCP tools for patching, refactoring, and code generation
go-surgeon demonstrates strong schema completeness and detailed parameter descriptions across 11 tools. All tools have explicit input schemas with type definitions and descriptions. Tool names follow verb_noun conventions (patch, find_definition, rename_symbol, scaffold, etc.). However, descriptions vary in clarity and actionability. The patch tool description is exceptionally detailed (300+ chars) but some tools like insert_call lack sufficient context on when to use them vs alternatives. Error handling guidance is minimal, most tools lack recovery hints. Output schemas are not formally documented in the visible code. No tool annotations (readOnlyHint/destructiveHint) despite clear risk levels (WRITE, DESTRUCTIVE, READ_ONLY marked in metadata).
Add a new file, function, or struct. object='file' creates a new file; 'func'/'struct' append to an existing file; 'auto' infers from content (func ... → func, type ... struct → struct, else file). When adding several related items, use execute_plan instead.
Remove a function, method, struct, or file. object='file' deletes the file from disk (no identifier needed); object='struct' also removes every method on the struct across the package. For interfaces, use the interface tool with action=delete instead (handles mock cleanup). preview=true returns a unified diff without writing.
Locate the declaration of a Go symbol (function, method, type, var, const, or struct field). Key parameter: 'name' (not 'query') — the exact symbol name. Resolution is type-aware via go/packages — works across packages, including when the same name exists in multiple packages. Returns file:line:column of the defining identifier. Pair name with receiver/package/file+line when ambiguous. Works on dependencies via module='github.com/org/repo' — paths are returned relative to the module root.
Find every reference to a Go symbol across the module, using go/packages type information. Key parameter: 'name' (not 'query') — the exact symbol name. Returns file:line:column for each use, deduplicated and sorted. Set include_definition=true to also get the declaration site in one call. Use this before a rename to preview impact. Works on dependencies via module='github.com/org/repo' — paths are returned relative to the module root.
Missing tool annotations (readOnlyHint, destructiveHint, idempotentHint) despite clear risk levels. Tools marked WRITE/DESTRUCTIVE/READ_ONLY in metadata should declare these in schema.
Output schemas not formally documented. Tools return structured results but schema definitions are not visible in code, LLMs cannot predict response structure.
Error handling lacks recovery guidance. No tool description explains what to do on failure (retry, ask user, try alternative tool). Errors will be raw Go errors.
Generic descriptions on create/delete/insert_call. 'Add a new file, function, or struct' and 'Remove a function, method, struct, or file' lack context on when to use vs patch/update/scaffold.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 62 | 2025-06-18+ | v2 |
Insert one statement into a function body. Idempotent: skipped if already present. position: 'before-return' (default), 'end-of-body', or 'after:<marker>'.
Manage interfaces and their mocks atomically. The action field selects the sub-operation: - action=add: create a new interface. Requires file and content (raw Go interface body or full declaration). Optional mock_file + mock_name also generate a function-field mock with a compile-time var assertion. Prefer this over create for interfaces. Example: {"action": "add", "file": "repo.go", "identifier": "Repository", "content": "type Repository interface { FindByID(id string) (*Entity, error) }", "mock_file": "mock_repo.go", "mock_name": "MockRepository"} - action=update: replace an interface wholesale and regenerate its mock. Requires file and identifier. Pass content to rewrite the interface body; pass doc to set/replace the doc comment, or strip_doc=true to remove it. mock_file + mock_name keep the mock in sync. For single-method changes, prefer patch target=interface. Example: {"action": "update", "file": "repo.go", "identifier": "Repository", "content": "type Repository interface { FindByID(id string) (*Entity, error); Delete(id string) error }", "mock_file": "mock_repo.go", "mock_name": "MockRepository"} - action=delete: remove an interface. Requires file and identifier. Pass delete_mock=true with mock_file + mock_name to also remove the mock struct, its methods, and the var assertion (mock file is kept even if empty). Example: {"action": "delete", "file": "repo.go", "identifier": "Repository", "delete_mock": true, "mock_file": "mock_repo.go", "mock_name": "MockRepository"} preview=true returns a unified diff without writing for any action.
Surgical AST-aware editor for one declaration kind at a time. SHAPE — pick exactly one per call: single-target (common) sets file + identifier + patches at the top level (also include_nested for function, mock_file/mock_name for interface, scope for file); bulk sets items: [{file, identifier, patches, ...}]. Mixing is rejected. target: function | struct | interface | file | decl. Each target accepts its own op set — run `go-surgeon discovery patch` (or `patch.<target>`) for the ops, examples, and limitations. BATCH: function/struct items are atomic; interface/file/decl items run sequentially (failures leave earlier items written). For multi-line replacements or whole-declaration rewrites, prefer update — patch op=replace is fragile across line boundaries. preview=true returns diff without writing.
Rename a Go symbol and every reference to it across the module. Type-aware: walks go/packages type information so only identifiers that resolve to the same declaration are rewritten (no false positives on same-named but unrelated identifiers). Rejects renames that would change export status or collide with an existing name in the same scope. Pass allow_export_change=true to opt in to a case-flip rename (foo → Foo or Foo → foo); the result will include a warning and the text output is prefixed with an export-changed notice. Set preview=true to list every site without writing.
Generate code derived from an existing symbol. kind selects what to scaffold: - interface_from_type: scaffold an interface from a struct's exported methods. Required: file, identifier (struct), target (new interface name). Optional: out (interface output file), mock_file + mock_name (also generate a mock). - impl_from_interface: generate method stubs on a receiver to satisfy an interface (already-implemented methods are skipped, stubs marked '// TODO(go-surgeon): implement'). Required: file, source (fully-qualified interface, e.g. io.ReadCloser), target (receiver, e.g. *MyStruct). - mock_from_interface: generate a function-field mock for an interface you don't own (stdlib, third-party). For your own interfaces, prefer 'interface' action=add with mock_file. Required: file, source (fully-qualified interface), target (mock struct name, e.g. MockReader). preview=true returns a unified diff without writing.
Scaffold a table-driven _test.go for a function or method. Handles boilerplate (t.Run loop, tt struct, receiver setup). identifier: 'FuncName' or 'Type.Method'. preview=true returns a unified diff without writing.
Replace a whole function, method, struct, or file. content is the complete new declaration. For small changes inside a function body, prefer patch target=function. object defaults to 'auto', which infers from content (func/type/file). Doc comments are kept unless doc or strip_doc=true. preview=true returns a diff.
No pagination or result limits documented. Tools like find_references could return hundreds of results; no limit or cursor guidance visible.