fixup! fixup! fixup! Add Docker usage instructions to README

This commit is contained in:
2026-03-12 10:42:39 +00:00
parent 34d4401280
commit db803c6950
8 changed files with 38 additions and 268 deletions
+9 -18
View File
@@ -8,7 +8,7 @@ A Go-based MCP (Model Context Protocol) server for Claude Code providing intelli
- **AST-Aware File Reading**: Read files with symbol extraction using Tree-sitter - **AST-Aware File Reading**: Read files with symbol extraction using Tree-sitter
- **Code Pattern Matching**: Query code using patterns with capture placeholders - **Code Pattern Matching**: Query code using patterns with capture placeholders
- **LSP Integration**: Go-to-definition, find references, and symbol info via language servers - **LSP Integration**: Go-to-definition, find references, and symbol info via language servers
- **Safe Editing**: AST-aware file editing with syntax validation and preview - **Safe Editing**: AST-aware file editing with syntax validation (edit_apply)
- **Multi-Language Support**: Go, TypeScript, JavaScript, Python, C, C++, HTML, Vue, React - **Multi-Language Support**: Go, TypeScript, JavaScript, Python, C, C++, HTML, Vue, React
- **Token Efficient**: Optimized for minimal token usage with symbols-only mode and output limiting - **Token Efficient**: Optimized for minimal token usage with symbols-only mode and output limiting
@@ -140,13 +140,13 @@ For optimal performance, keep the most frequently used tools loaded at startup a
"filepuff": { "filepuff": {
"command": "mcp-filepuff", "command": "mcp-filepuff",
"args": ["-workspace", "."], "args": ["-workspace", "."],
"alwaysAllow": ["file_read", "file_search", "edit_apply", "edit_preview"] "alwaysAllow": ["file_read", "file_search", "edit_apply"]
} }
} }
} }
``` ```
This keeps `file_read`, `file_search`, `edit_apply`, and `edit_preview` immediately available while deferring less frequently used tools (`ping`, `ast_query`, `symbol_at`, `find_definition`, `find_references`). This keeps `file_read`, `file_search`, and `edit_apply` immediately available while deferring less frequently used tools (`ping`, `ast_query`, `symbol_at`, `find_definition`, `find_references`).
#### System Prompt Guidance #### System Prompt Guidance
@@ -158,7 +158,7 @@ You have access to filepuff MCP tools providing:
- Fast regex search powered by ripgrep (file_search) - Fast regex search powered by ripgrep (file_search)
- Structural code pattern matching across 9+ languages (ast_query) - Structural code pattern matching across 9+ languages (ast_query)
- LSP-powered go-to-definition, find-references, and symbol info (find_definition, find_references, symbol_at) - LSP-powered go-to-definition, find-references, and symbol info (find_definition, find_references, symbol_at)
- AST-aware file editing with syntax validation and preview (edit_preview, edit_apply) - AST-aware file editing with syntax validation (edit_apply)
``` ```
## Usage ## Usage
@@ -241,7 +241,7 @@ When performing file operations, prefer filepuff MCP tools over built-in equival
| Read files | `mcp__filepuff__file_read` | Read | | Read files | `mcp__filepuff__file_read` | Read |
| Search content | `mcp__filepuff__file_search` | Grep | | Search content | `mcp__filepuff__file_search` | Grep |
| AST pattern search | `mcp__filepuff__ast_query` | Grep/Glob | | AST pattern search | `mcp__filepuff__ast_query` | Grep/Glob |
| Edit files | `mcp__filepuff__edit_preview` + `mcp__filepuff__edit_apply` | Edit | | Edit files | `mcp__filepuff__edit_apply` | Edit |
| Find definitions | `mcp__filepuff__find_definition` | Grep | | Find definitions | `mcp__filepuff__find_definition` | Grep |
| Find references | `mcp__filepuff__find_references` | Grep | | Find references | `mcp__filepuff__find_references` | Grep |
| Symbol info | `mcp__filepuff__symbol_at` | - | | Symbol info | `mcp__filepuff__symbol_at` | - |
@@ -399,8 +399,8 @@ Find all references to the symbol at a specific position.
--- ---
### `edit_preview` ### `edit_apply`
Preview an edit without applying it. Uses AST-aware editing for code files (Go, TypeScript, JavaScript, Python, C, C++), and text-based editing for other files (Markdown, JSON, YAML, config files, etc.). Apply an edit to a file. Uses AST-aware editing for code files with syntax validation, and text-based editing for other files.
**Parameters**: **Parameters**:
- `file` (required): Path to the file to edit - `file` (required): Path to the file to edit
@@ -420,13 +420,6 @@ Preview an edit without applying it. Uses AST-aware editing for code files (Go,
- `selector_text`: Exact text to match (must be unique or use selector_index) - `selector_text`: Exact text to match (must be unique or use selector_index)
- `selector_pattern`: Regex pattern to match - `selector_pattern`: Regex pattern to match
---
### `edit_apply`
Apply an edit to a file. Uses AST-aware editing for code files with syntax validation, and text-based editing for other files.
**Parameters**: Same as `edit_preview`
**Example (AST mode - Go file)**: **Example (AST mode - Go file)**:
```json ```json
{ {
@@ -544,8 +537,7 @@ make clean
│ MCP Server │ │ MCP Server │
├─────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────┤
│ Tools: file_search, file_read, ast_query, symbol_at, │ │ Tools: file_search, file_read, ast_query, symbol_at, │
│ find_definition, find_references, │ │ find_definition, find_references, edit_apply, ping
│ edit_preview, edit_apply, ping │
├─────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────┤
│ Core Engines │ │ Core Engines │
├───────────┬─────────────┬────────────┬─────────────────┤ ├───────────┬─────────────┬────────────┬─────────────────┤
@@ -746,7 +738,7 @@ flowchart TB
C -->|Read| E[file_read] C -->|Read| E[file_read]
C -->|Query| F[ast_query] C -->|Query| F[ast_query]
C -->|LSP| G[symbol_at<br/>find_definition<br/>find_references] C -->|LSP| G[symbol_at<br/>find_definition<br/>find_references]
C -->|Edit| H[edit_preview<br/>edit_apply] C -->|Edit| H[edit_apply]
end end
subgraph "Core Engines" subgraph "Core Engines"
@@ -819,7 +811,6 @@ The edit engine validates syntax before and after edits.
**Solution**: **Solution**:
- Ensure `new_content` is syntactically valid for the target language - Ensure `new_content` is syntactically valid for the target language
- Use `edit_preview` first to see the proposed changes
- Check that the selector matches exactly one node - Check that the selector matches exactly one node
#### Timeout Errors #### Timeout Errors
-28
View File
@@ -175,34 +175,6 @@ func AllTools() []Tool {
"Results grouped by file", "Results grouped by file",
}, },
}, },
{
Name: "edit_preview",
Description: "Preview an edit without applying it. Uses AST-aware editing for code files (Go, TypeScript, JavaScript, Python, C, C++), and text-based editing for other files (Markdown, JSON, YAML, config files, etc.).",
Category: "Edit Operations",
ReadOnly: true,
Parameters: []ToolParameter{
{Name: "file", Type: "string", Required: true, Description: "Path to the file to edit"},
{Name: "operation", Type: "string", Required: true, Description: "Edit operation: replace, insert_before, insert_after, delete"},
{Name: "new_content", Type: "string", Required: false, Description: "New content (required for replace/insert operations)"},
{Name: "selector_kind", Type: "string", Required: false, Description: "AST node type to match (e.g., function_declaration, class_declaration). For code files only."},
{Name: "selector_name", Type: "string", Required: false, Description: "Name of the symbol to match. For code files only."},
{Name: "selector_line", Type: "number", Required: false, Description: "Line number (1-indexed). For AST mode: narrows search. For text mode: start of line range."},
{Name: "selector_index", Type: "number", Required: false, Description: "Index of the match to use if multiple matches found (default: 0)"},
{Name: "selector_line_end", Type: "number", Required: false, Description: "End line number for range selection (text mode). Used with selector_line."},
{Name: "selector_text", Type: "string", Required: false, Description: "Exact text to match (text mode). Must be unique or use selector_index."},
{Name: "selector_pattern", Type: "string", Required: false, Description: "Regex pattern to match (text mode). Must be unique or use selector_index."},
},
Examples: []string{
`{"file": "server.go", "operation": "replace", "selector_kind": "function_declaration", "selector_name": "Hello", "new_content": "func Hello() {\\n\\tprintln(\\"New Hello\\")\\n}"}`,
`{"file": "README.md", "operation": "replace", "selector_text": "## Installation", "new_content": "## Getting Started"}`,
`{"file": "package.json", "operation": "replace", "selector_pattern": "\\"version\\":\\\\s*\\"[^\\"]+\\"", "new_content": "\\"version\\": \\"2.0.0\\""}`,
},
Notes: []string{
"Returns a diff showing proposed changes",
"Does not modify the file",
"Use to validate changes before applying",
},
},
{ {
Name: "edit_apply", Name: "edit_apply",
Description: "Apply an edit to a file. Uses AST-aware editing for code files (Go, TypeScript, JavaScript, Python, C, C++) with syntax validation, and text-based editing for other files (Markdown, JSON, YAML, config files, etc.).", Description: "Apply an edit to a file. Uses AST-aware editing for code files (Go, TypeScript, JavaScript, Python, C, C++) with syntax validation, and text-based editing for other files (Markdown, JSON, YAML, config files, etc.).",
+4 -61
View File
@@ -1,6 +1,6 @@
# MCP Filepuff API Reference # MCP Filepuff API Reference
> Auto-generated on 2026-01-28 > Auto-generated on 2026-03-12
This document provides detailed API documentation for all MCP tools available in filepuff. This document provides detailed API documentation for all MCP tools available in filepuff.
@@ -24,7 +24,6 @@ This document provides detailed API documentation for all MCP tools available in
- [`find_references`](#find_references) - [`find_references`](#find_references)
### Edit Operations ### Edit Operations
- [`edit_preview`](#edit_preview)
- [`edit_apply`](#edit_apply) - [`edit_apply`](#edit_apply)
--- ---
@@ -47,10 +46,9 @@ Health check - returns pong to verify the server is running
{"tool": "ping"} {"tool": "ping"}
``` ```
**Returns:** `"pong"` text string.
**Notes:** **Notes:**
- Returns: "pong"
- Use to verify server connectivity - Use to verify server connectivity
--- ---
@@ -85,8 +83,6 @@ Search for text patterns in files using ripgrep. Supports regex patterns, file t
{"pattern": "TODO", "ignore_case": true, "context_lines": 3} {"pattern": "TODO", "ignore_case": true, "context_lines": 3}
``` ```
**Returns:** Results grouped by file with match context. Format: `"Found N matches in M files:"` followed by file sections, each with matching lines prefixed by `"L{line}│"` and context lines prefixed by `" │"`.
**Notes:** **Notes:**
- Requires ripgrep (rg) to be installed - Requires ripgrep (rg) to be installed
@@ -131,8 +127,6 @@ Read a file's contents with optional line range and AST symbol summary
{"path": "large_file.go", "max_lines": 100} {"path": "large_file.go", "max_lines": 100}
``` ```
**Returns:** File content with numbered lines (format: `" 12│ line text"`). When `include_ast=true`: prepends symbol summary (`"**file.go** (N lines, go)\nSymbols:\n func Name L12\n struct Config L45"`). When `symbols_only=true`: returns only the symbol summary (~95% fewer tokens). When `max_lines` is set: truncates output with `"[... N more lines omitted]"` notice.
**Notes:** **Notes:**
- symbols_only mode reduces token usage by ~90-98% - symbols_only mode reduces token usage by ~90-98%
@@ -175,8 +169,6 @@ Search for AST patterns in code files. Use code patterns with $VAR placeholders
{"pattern": "function $NAME($PROPS) { $$$BODY }", "language": "javascript", "name_matches": "^[A-Z]"} {"pattern": "function $NAME($PROPS) { $$$BODY }", "language": "javascript", "name_matches": "^[A-Z]"}
``` ```
**Returns:** `"Found N match(es):"` followed by entries in format `"**file:line** (node_type)"` with code blocks and captured variables (`$NAME=value`). Returns `"No matches found."` when no results.
**Notes:** **Notes:**
- $NAME captures identifiers - $NAME captures identifiers
@@ -208,8 +200,6 @@ Get information about the symbol at a specific position in a file. Returns type,
{"file": "server.go", "line": 25, "column": 10} {"file": "server.go", "line": 25, "column": 10}
``` ```
**Returns:** `"**Symbol Information**"` followed by hover/type information from LSP, or `"**Symbol Information** (AST fallback)"` with node type and text when LSP unavailable. Returns `"No symbol information available at this position."` when nothing is found.
**Notes:** **Notes:**
- Requires LSP server for full type information - Requires LSP server for full type information
@@ -237,11 +227,10 @@ Find the definition of the symbol at a specific position. Uses LSP to locate whe
{"file": "server.go", "line": 42, "column": 15} {"file": "server.go", "line": 42, "column": 15}
``` ```
**Returns:** `"Found N definition(s):"` with entries showing `"**file:line:column**"` and a 3-line code preview with the target line marked by `">"`. Returns `"No definition found."` when the symbol has no definition.
**Notes:** **Notes:**
- Requires language server for the file type - Requires language server for the file type
- Returns file path, line, and column of definition
- Shows code preview at definition location - Shows code preview at definition location
--- ---
@@ -271,8 +260,6 @@ Find all references to the symbol at a specific position. Uses LSP to locate all
{"file": "server.go", "line": 42, "column": 15, "include_declaration": false} {"file": "server.go", "line": 42, "column": 15, "include_declaration": false}
``` ```
**Returns:** `"Found N reference(s):"` grouped by file, each showing `"**file** (count)"` with locations as `"L{line}:{column}"`. Returns `"No references found."` when no usages exist.
**Notes:** **Notes:**
- Requires language server for the file type - Requires language server for the file type
@@ -282,49 +269,6 @@ Find all references to the symbol at a specific position. Uses LSP to locate all
### Edit Operations ### Edit Operations
#### `edit_preview`
Preview an edit without applying it. Uses AST-aware editing for code files (Go, TypeScript, JavaScript, Python, C, C++), and text-based editing for other files (Markdown, JSON, YAML, config files, etc.).
🔒 **Read-only**: This tool does not modify files.
**Parameters:**
| Name | Type | Required | Description |
|------|------|----------|-------------|
| `file` | `string` | **Yes** | Path to the file to edit |
| `operation` | `string` | **Yes** | Edit operation: replace, insert_before, insert_after, delete |
| `new_content` | `string` | No | New content (required for replace/insert operations) |
| `selector_kind` | `string` | No | AST node type to match (e.g., function_declaration, class_declaration). For code files only. |
| `selector_name` | `string` | No | Name of the symbol to match. For code files only. |
| `selector_line` | `number` | No | Line number (1-indexed). For AST mode: narrows search. For text mode: start of line range. |
| `selector_index` | `number` | No | Index of the match to use if multiple matches found (default: 0) |
| `selector_line_end` | `number` | No | End line number for range selection (text mode). Used with selector_line. |
| `selector_text` | `string` | No | Exact text to match (text mode). Must be unique or use selector_index. |
| `selector_pattern` | `string` | No | Regex pattern to match (text mode). Must be unique or use selector_index. |
**Examples:**
```json
{"file": "server.go", "operation": "replace", "selector_kind": "function_declaration", "selector_name": "Hello", "new_content": "func Hello() {\\n\\tprintln(\\"New Hello\\")\\n}"}
```
```json
{"file": "README.md", "operation": "replace", "selector_text": "## Installation", "new_content": "## Getting Started"}
```
```json
{"file": "package.json", "operation": "replace", "selector_pattern": "\\"version\\":\\\\s*\\"[^\\"]+\\"", "new_content": "\\"version\\": \\"2.0.0\\""}
```
**Returns:** `"**Edit Preview**"` followed by a unified diff showing proposed changes. Does not modify the file. For code files: uses AST-aware mode with syntax validation. For other files: uses text-based mode.
**Notes:**
- Use to validate changes before applying
---
#### `edit_apply` #### `edit_apply`
Apply an edit to a file. Uses AST-aware editing for code files (Go, TypeScript, JavaScript, Python, C, C++) with syntax validation, and text-based editing for other files (Markdown, JSON, YAML, config files, etc.). Apply an edit to a file. Uses AST-aware editing for code files (Go, TypeScript, JavaScript, Python, C, C++) with syntax validation, and text-based editing for other files (Markdown, JSON, YAML, config files, etc.).
@@ -356,10 +300,9 @@ Apply an edit to a file. Uses AST-aware editing for code files (Go, TypeScript,
{"file": "config.yaml", "operation": "replace", "selector_line": 5, "selector_line_end": 10, "new_content": "database:\\n host: production.db.example.com\\n port: 5432"} {"file": "config.yaml", "operation": "replace", "selector_line": 5, "selector_line_end": 10, "new_content": "database:\\n host: production.db.example.com\\n port: 5432"}
``` ```
**Returns:** `"**Edit Applied Successfully**"` followed by a unified diff of the changes made. For code files, validates syntax before writing — returns an error if the edit would produce invalid syntax.
**Notes:** **Notes:**
- For code files: validates syntax before and after edit
- Preserves file permissions - Preserves file permissions
- Uses atomic writes for safety - Uses atomic writes for safety
- File locking prevents concurrent edits - File locking prevents concurrent edits
+3 -5
View File
@@ -451,8 +451,7 @@ Review the edit request and ensure all required fields are provided.
**Remediation**: **Remediation**:
1. Validate `new_content` syntax independently 1. Validate `new_content` syntax independently
2. Use `edit_preview` to see the proposed changes 2. Ensure the edit maintains valid syntax
3. Ensure the edit maintains valid syntax
--- ---
@@ -622,9 +621,8 @@ mcp-filepuff -workspace /path/to/workspace
**Symptoms**: Edit operation rejected with syntax error **Symptoms**: Edit operation rejected with syntax error
**Solution**: **Solution**:
1. Use `edit_preview` first to see proposed changes 1. Validate `new_content` is syntactically correct
2. Validate `new_content` is syntactically correct 2. Check that surrounding code structure is maintained
3. Check that surrounding code structure is maintained
### Scenario 3: Search Returns Too Many Results ### Scenario 3: Search Returns Too Many Results
+6 -20
View File
@@ -22,18 +22,13 @@ func unescapeNewlines(s string) string {
return s return s
} }
// handleEditPreview handles the edit_preview tool.
func (s *Server) handleEditPreview(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return s.handleEdit(ctx, request, false)
}
// handleEditApply handles the edit_apply tool. // handleEditApply handles the edit_apply tool.
func (s *Server) handleEditApply(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { func (s *Server) handleEditApply(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
return s.handleEdit(ctx, request, true) return s.handleEdit(ctx, request)
} }
// handleEdit is the shared implementation for edit_preview and edit_apply. // handleEdit performs an edit operation (always applies changes).
func (s *Server) handleEdit(ctx context.Context, request mcp.CallToolRequest, apply bool) (*mcp.CallToolResult, error) { func (s *Server) handleEdit(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
file, err := request.RequireString("file") file, err := request.RequireString("file")
if err != nil { if err != nil {
return mcp.NewToolResultError("file is required"), nil return mcp.NewToolResultError("file is required"), nil
@@ -85,13 +80,8 @@ func (s *Server) handleEdit(ctx context.Context, request mcp.CallToolRequest, ap
}, },
} }
// Perform edit // Perform edit (always apply)
var result *edit.EditResult result, err := s.editor.Apply(ctx, astEdit)
if apply {
result, err = s.editor.Apply(ctx, astEdit)
} else {
result, err = s.editor.Preview(ctx, astEdit)
}
if err != nil { if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("edit failed: %s", errors.SanitizeError(err))), nil return mcp.NewToolResultError(fmt.Sprintf("edit failed: %s", errors.SanitizeError(err))), nil
@@ -103,11 +93,7 @@ func (s *Server) handleEdit(ctx context.Context, request mcp.CallToolRequest, ap
// Format output // Format output
var output strings.Builder var output strings.Builder
if apply { output.WriteString("**Edit Applied Successfully**\n\n")
output.WriteString("**Edit Applied Successfully**\n\n")
} else {
output.WriteString("**Edit Preview**\n\n")
}
output.WriteString("Diff:\n```diff\n") output.WriteString("Diff:\n```diff\n")
output.WriteString(result.Diff) output.WriteString(result.Diff)
+12 -34
View File
@@ -96,35 +96,17 @@ func Hello() string {
} }
}) })
// Test 4: Edit preview and apply // Test 4: Edit workflow
t.Run("edit_workflow", func(t *testing.T) { t.Run("edit_workflow", func(t *testing.T) {
// Preview edit // Apply edit directly (preview removed to avoid confusing LLMs)
previewReq := mcp.CallToolRequest{} applyReq := mcp.CallToolRequest{}
previewReq.Params.Arguments = map[string]interface{}{ applyReq.Params.Arguments = map[string]interface{}{
"file": testFile, "file": testFile,
"operation": "replace", "operation": "replace",
"selector_kind": "function_declaration", "selector_kind": "function_declaration",
"selector_name": "Hello", "selector_name": "Hello",
"new_content": "func Hello() string {\n\treturn \"goodbye\"\n}", "new_content": "func Hello() string {\n\treturn \"goodbye\"\n}",
} }
previewResult, err := srv.handleEditPreview(ctx, previewReq)
if err != nil {
t.Errorf("handleEditPreview() error = %v", err)
}
if previewResult == nil {
t.Fatal("handleEditPreview() returned nil")
return
}
// Verify file unchanged after preview
originalContent, _ := os.ReadFile(testFile)
if string(originalContent) != content {
t.Error("preview should not modify file")
}
// Apply edit
applyReq := mcp.CallToolRequest{}
applyReq.Params.Arguments = previewReq.Params.Arguments
applyResult, err := srv.handleEditApply(ctx, applyReq) applyResult, err := srv.handleEditApply(ctx, applyReq)
if err != nil { if err != nil {
t.Errorf("handleEditApply() error = %v", err) t.Errorf("handleEditApply() error = %v", err)
@@ -267,10 +249,8 @@ func TestMCPErrorResponses(t *testing.T) {
expectError: true, expectError: true,
}, },
{ {
name: "edit_missing_file", name: "edit_missing_file",
handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { handler: srv.handleEditApply,
return srv.handleEdit(ctx, req, false)
},
setupReq: func() mcp.CallToolRequest { setupReq: func() mcp.CallToolRequest {
req := mcp.CallToolRequest{} req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]interface{}{ req.Params.Arguments = map[string]interface{}{
@@ -281,10 +261,8 @@ func TestMCPErrorResponses(t *testing.T) {
expectError: true, expectError: true,
}, },
{ {
name: "edit_missing_operation", name: "edit_missing_operation",
handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { handler: srv.handleEditApply,
return srv.handleEdit(ctx, req, false)
},
setupReq: func() mcp.CallToolRequest { setupReq: func() mcp.CallToolRequest {
req := mcp.CallToolRequest{} req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]interface{}{ req.Params.Arguments = map[string]interface{}{
@@ -376,7 +354,7 @@ func Add(a, b int) int {
return return
} }
// 3. Preview edit // 3. Edit (no preview)
editReq := mcp.CallToolRequest{} editReq := mcp.CallToolRequest{}
editReq.Params.Arguments = map[string]interface{}{ editReq.Params.Arguments = map[string]interface{}{
"file": testFile, "file": testFile,
@@ -385,12 +363,12 @@ func Add(a, b int) int {
"selector_name": "Add", "selector_name": "Add",
"new_content": "func Add(a, b int) int {\n\treturn a + b + 1\n}", "new_content": "func Add(a, b int) int {\n\treturn a + b + 1\n}",
} }
editResult, err := srv.handleEditPreview(ctx, editReq) editResult, err := srv.handleEditApply(ctx, editReq)
if err != nil { if err != nil {
t.Fatalf("handleEditPreview() error = %v", err) t.Fatalf("handleEditApply() error = %v", err)
} }
if editResult == nil { if editResult == nil {
t.Fatal("handleEditPreview() returned nil") t.Fatal("handleEditApply() returned nil")
return return
} }
}) })
-48
View File
@@ -290,54 +290,6 @@ func (s *Server) registerTools() {
} }
// Register edit tools // Register edit tools
s.mcp.AddTool(
mcp.NewTool("edit_preview",
mcp.WithDescription("Preview an edit without applying it. Uses AST-aware editing for code files (Go, TypeScript, JavaScript, Python, C, C++, Rust), and text-based editing for other files (Markdown, JSON, YAML, config files, etc.).\n\n"+
"Returns: \"**Edit Preview**\" followed by a unified diff showing proposed changes. Does not modify the file. "+
"For code files: uses AST-aware mode with syntax validation. For other files: uses text-based mode.\n\n"+
"Examples:\n"+
" AST mode: {\"file\": \"main.go\", \"operation\": \"replace\", \"selector_kind\": \"function_declaration\", \"selector_name\": \"Hello\", \"new_content\": \"func Hello() {\\n\\treturn\\n}\"}\n"+
" Text mode: {\"file\": \"README.md\", \"operation\": \"replace\", \"selector_text\": \"## Old Header\", \"new_content\": \"## New Header\"}\n"+
" Line range: {\"file\": \"config.yaml\", \"operation\": \"replace\", \"selector_line\": 5, \"selector_line_end\": 10, \"new_content\": \"key: value\"}"),
mcp.WithString("file",
mcp.Required(),
mcp.Description("Path to the file to edit"),
),
mcp.WithString("operation",
mcp.Required(),
mcp.Description("Edit operation: replace, insert_before, insert_after, delete"),
),
mcp.WithString("new_content",
mcp.Description("New content (required for replace/insert operations)"),
),
// AST-mode selectors (for code files)
mcp.WithString("selector_kind",
mcp.Description("AST node type to match (e.g., function_declaration, class_declaration). For code files only."),
),
mcp.WithString("selector_name",
mcp.Description("Name of the symbol to match. For code files only."),
),
// Shared selectors
mcp.WithNumber("selector_line",
mcp.Description("Line number (1-indexed). For AST mode: narrows search. For text mode: start of line range."),
),
mcp.WithNumber("selector_index",
mcp.Description("Index of the match to use if multiple matches found (default: 0)"),
),
// Text-mode selectors (for non-code files or explicit text matching)
mcp.WithNumber("selector_line_end",
mcp.Description("End line number for range selection (text mode). Used with selector_line."),
),
mcp.WithString("selector_text",
mcp.Description("Exact text to match (text mode). Must be unique or use selector_index."),
),
mcp.WithString("selector_pattern",
mcp.Description("Regex pattern to match (text mode). Must be unique or use selector_index."),
),
),
s.handleEditPreview,
)
s.mcp.AddTool( s.mcp.AddTool(
mcp.NewTool("edit_apply", mcp.NewTool("edit_apply",
mcp.WithDescription("Apply an edit to a file. Uses AST-aware editing for code files (Go, TypeScript, JavaScript, Python, C, C++, Rust) with syntax validation, and text-based editing for other files (Markdown, JSON, YAML, config files, etc.).\n\n"+ mcp.WithDescription("Apply an edit to a file. Uses AST-aware editing for code files (Go, TypeScript, JavaScript, Python, C, C++, Rust) with syntax validation, and text-based editing for other files (Markdown, JSON, YAML, config files, etc.).\n\n"+
+4 -54
View File
@@ -283,56 +283,6 @@ func Goodbye() error {
} }
} }
func TestHandleEditPreview(t *testing.T) {
tmpDir := t.TempDir()
// Create a test file
testFile := filepath.Join(tmpDir, "test.go")
content := `package main
func Hello() {
println("Hello")
}
`
if err := os.WriteFile(testFile, []byte(content), 0600); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
cfg := &config.Config{WorkspaceRoot: tmpDir, EnableLSP: false}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
srv, err := New(cfg, logger)
if err != nil {
t.Fatalf("New() error = %v", err)
}
ctx := context.Background()
req := mcp.CallToolRequest{}
req.Params.Arguments = map[string]interface{}{
"file": testFile,
"operation": "replace",
"selector_kind": "function_declaration",
"selector_name": "Hello",
"new_content": "func Hello() {\n\tprintln(\"Goodbye\")\n}",
}
result, err := srv.handleEdit(ctx, req, false)
if err != nil {
t.Errorf("handleEdit(preview) error = %v", err)
}
if result == nil {
t.Fatal("handleEdit(preview) returned nil result")
return
}
// Verify file was NOT modified (it's just a preview)
fileContent, _ := os.ReadFile(testFile)
if string(fileContent) != content {
t.Error("handleEdit(preview) should not modify the file")
}
}
func TestHandleEditApply(t *testing.T) { func TestHandleEditApply(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
@@ -366,20 +316,20 @@ func Hello() {
"new_content": "func Hello() {\n\tprintln(\"Goodbye\")\n}", "new_content": "func Hello() {\n\tprintln(\"Goodbye\")\n}",
} }
result, err := srv.handleEdit(ctx, req, true) result, err := srv.handleEditApply(ctx, req)
if err != nil { if err != nil {
t.Errorf("handleEdit(apply) error = %v", err) t.Errorf("handleEditApply error = %v", err)
} }
if result == nil { if result == nil {
t.Fatal("handleEdit(apply) returned nil result") t.Fatal("handleEditApply returned nil result")
return return
} }
// Verify file WAS modified // Verify file WAS modified
fileContent, _ := os.ReadFile(testFile) fileContent, _ := os.ReadFile(testFile)
if string(fileContent) == content { if string(fileContent) == content {
t.Error("handleEdit(apply) should modify the file") t.Error("handleEditApply should modify the file")
} }
} }