diff --git a/internal/server/handlers_edit.go b/internal/server/handlers_edit.go index 2549f88..3e1fe19 100644 --- a/internal/server/handlers_edit.go +++ b/internal/server/handlers_edit.go @@ -11,6 +11,17 @@ import ( "github.com/mark3labs/mcp-go/mcp" ) +// unescapeNewlines converts literal \n, \t, \" sequences to actual characters. +// This handles cases where MCP clients send double-escaped JSON strings. +func unescapeNewlines(s string) string { + // Replace common escape sequences + s = strings.ReplaceAll(s, "\\n", "\n") + s = strings.ReplaceAll(s, "\\t", "\t") + s = strings.ReplaceAll(s, "\\\"", "\"") + s = strings.ReplaceAll(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) @@ -52,10 +63,15 @@ func (s *Server) handleEdit(ctx context.Context, request mcp.CallToolRequest, ap // The edit engine automatically detects whether to use AST or text mode. // Build edit request with both AST and text-mode selectors + newContent := request.GetString("new_content", "") + + // Unescape common escape sequences that may be double-encoded by MCP clients + newContent = unescapeNewlines(newContent) + astEdit := &edit.ASTEdit{ File: file, Operation: edit.EditOperation(operation), - NewContent: request.GetString("new_content", ""), + NewContent: newContent, Selector: edit.ASTSelector{ // AST-mode selectors Kind: request.GetString("selector_kind", ""), diff --git a/internal/server/handlers_edit_test.go b/internal/server/handlers_edit_test.go new file mode 100644 index 0000000..7787bd0 --- /dev/null +++ b/internal/server/handlers_edit_test.go @@ -0,0 +1,47 @@ +package server + +import "testing" + +func TestUnescapeNewlines(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "no escapes", + input: "func test() {\n\treturn true;\n}", + expected: "func test() {\n\treturn true;\n}", + }, + { + name: "literal backslash n", + input: "func test() {\\n\\treturn true;\\n}", + expected: "func test() {\n\treturn true;\n}", + }, + { + name: "literal backslash t", + input: "func test() {\\n\\treturn true;\\n}", + expected: "func test() {\n\treturn true;\n}", + }, + { + name: "literal quotes", + input: `func test() {\n\treturn \"true\";\n}`, + expected: "func test() {\n\treturn \"true\";\n}", + }, + + { + name: "mixed content", + input: "line1\\nline2\\tindented\\nline3", + expected: "line1\nline2\tindented\nline3", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := unescapeNewlines(tt.input) + if result != tt.expected { + t.Errorf("unescapeNewlines(%q) = %q, expected %q", tt.input, result, tt.expected) + } + }) + } +}