mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-06-05 22:23:50 +00:00
fixup! fixup! fixup! Add Docker usage instructions to README
This commit is contained in:
@@ -22,18 +22,13 @@ func unescapeNewlines(s string) string {
|
||||
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.
|
||||
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.
|
||||
func (s *Server) handleEdit(ctx context.Context, request mcp.CallToolRequest, apply bool) (*mcp.CallToolResult, error) {
|
||||
// handleEdit performs an edit operation (always applies changes).
|
||||
func (s *Server) handleEdit(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
file, err := request.RequireString("file")
|
||||
if err != 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
|
||||
var result *edit.EditResult
|
||||
if apply {
|
||||
result, err = s.editor.Apply(ctx, astEdit)
|
||||
} else {
|
||||
result, err = s.editor.Preview(ctx, astEdit)
|
||||
}
|
||||
// Perform edit (always apply)
|
||||
result, err := s.editor.Apply(ctx, astEdit)
|
||||
|
||||
if 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
|
||||
var output strings.Builder
|
||||
if apply {
|
||||
output.WriteString("**Edit Applied Successfully**\n\n")
|
||||
} else {
|
||||
output.WriteString("**Edit Preview**\n\n")
|
||||
}
|
||||
output.WriteString("**Edit Applied Successfully**\n\n")
|
||||
|
||||
output.WriteString("Diff:\n```diff\n")
|
||||
output.WriteString(result.Diff)
|
||||
|
||||
@@ -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) {
|
||||
// Preview edit
|
||||
previewReq := mcp.CallToolRequest{}
|
||||
previewReq.Params.Arguments = map[string]interface{}{
|
||||
// Apply edit directly (preview removed to avoid confusing LLMs)
|
||||
applyReq := mcp.CallToolRequest{}
|
||||
applyReq.Params.Arguments = map[string]interface{}{
|
||||
"file": testFile,
|
||||
"operation": "replace",
|
||||
"selector_kind": "function_declaration",
|
||||
"selector_name": "Hello",
|
||||
"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)
|
||||
if err != nil {
|
||||
t.Errorf("handleEditApply() error = %v", err)
|
||||
@@ -267,10 +249,8 @@ func TestMCPErrorResponses(t *testing.T) {
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "edit_missing_file",
|
||||
handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return srv.handleEdit(ctx, req, false)
|
||||
},
|
||||
name: "edit_missing_file",
|
||||
handler: srv.handleEditApply,
|
||||
setupReq: func() mcp.CallToolRequest {
|
||||
req := mcp.CallToolRequest{}
|
||||
req.Params.Arguments = map[string]interface{}{
|
||||
@@ -281,10 +261,8 @@ func TestMCPErrorResponses(t *testing.T) {
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "edit_missing_operation",
|
||||
handler: func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return srv.handleEdit(ctx, req, false)
|
||||
},
|
||||
name: "edit_missing_operation",
|
||||
handler: srv.handleEditApply,
|
||||
setupReq: func() mcp.CallToolRequest {
|
||||
req := mcp.CallToolRequest{}
|
||||
req.Params.Arguments = map[string]interface{}{
|
||||
@@ -376,7 +354,7 @@ func Add(a, b int) int {
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Preview edit
|
||||
// 3. Edit (no preview)
|
||||
editReq := mcp.CallToolRequest{}
|
||||
editReq.Params.Arguments = map[string]interface{}{
|
||||
"file": testFile,
|
||||
@@ -385,12 +363,12 @@ func Add(a, b int) int {
|
||||
"selector_name": "Add",
|
||||
"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 {
|
||||
t.Fatalf("handleEditPreview() error = %v", err)
|
||||
t.Fatalf("handleEditApply() error = %v", err)
|
||||
}
|
||||
if editResult == nil {
|
||||
t.Fatal("handleEditPreview() returned nil")
|
||||
t.Fatal("handleEditApply() returned nil")
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
@@ -290,54 +290,6 @@ func (s *Server) registerTools() {
|
||||
}
|
||||
|
||||
// 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(
|
||||
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"+
|
||||
|
||||
@@ -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) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
@@ -366,20 +316,20 @@ func Hello() {
|
||||
"new_content": "func Hello() {\n\tprintln(\"Goodbye\")\n}",
|
||||
}
|
||||
|
||||
result, err := srv.handleEdit(ctx, req, true)
|
||||
result, err := srv.handleEditApply(ctx, req)
|
||||
if err != nil {
|
||||
t.Errorf("handleEdit(apply) error = %v", err)
|
||||
t.Errorf("handleEditApply error = %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("handleEdit(apply) returned nil result")
|
||||
t.Fatal("handleEditApply returned nil result")
|
||||
return
|
||||
}
|
||||
|
||||
// Verify file WAS modified
|
||||
fileContent, _ := os.ReadFile(testFile)
|
||||
if string(fileContent) == content {
|
||||
t.Error("handleEdit(apply) should modify the file")
|
||||
t.Error("handleEditApply should modify the file")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user