diff --git a/src/filesystem/__tests__/structured-content.test.ts b/src/filesystem/__tests__/structured-content.test.ts index 4b8f92b0a3..eaec739c5f 100644 --- a/src/filesystem/__tests__/structured-content.test.ts +++ b/src/filesystem/__tests__/structured-content.test.ts @@ -123,6 +123,32 @@ describe('structuredContent schema compliance', () => { }); }); + describe('edit_file', () => { + it('should decode newText_base64 before applying edits', async () => { + const filePath = path.join(testDir, 'script.ps1'); + const replacement = 'if ($null -eq $toolObject.Config) { Write-Output "$($toolObject.Name)" }'; + await fs.writeFile(filePath, 'PLACEHOLDER\n'); + + const result = await client.callTool({ + name: 'edit_file', + arguments: { + path: filePath, + edits: [{ + oldText: 'PLACEHOLDER', + newText_base64: Buffer.from(replacement, 'utf-8').toString('base64') + }], + dryRun: false + } + }); + + expect(result.structuredContent).toBeDefined(); + expect(await fs.readFile(filePath, 'utf-8')).toBe(`${replacement}\n`); + + const structuredContent = result.structuredContent as { content: unknown }; + expect(structuredContent.content).toContain(replacement); + }); + }); + describe('list_directory (control - already working)', () => { it('should return structuredContent.content as a string', async () => { const result = await client.callTool({ diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 7b67e63e58..ed01f92d42 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -117,7 +117,8 @@ const WriteFileArgsSchema = z.object({ const EditOperation = z.object({ oldText: z.string().describe('Text to search for - must match exactly'), - newText: z.string().describe('Text to replace with') + newText: z.string().optional().describe('Text to replace with'), + newText_base64: z.string().optional().describe('Base64-encoded UTF-8 replacement text. Use when newText contains characters that are hard to serialize safely in JSON.') }); const EditFileArgsSchema = z.object({ @@ -374,7 +375,8 @@ server.registerTool( path: z.string(), edits: z.array(z.object({ oldText: z.string().describe("Text to search for - must match exactly"), - newText: z.string().describe("Text to replace with") + newText: z.string().optional().describe("Text to replace with"), + newText_base64: z.string().optional().describe("Base64-encoded UTF-8 replacement text. Use when newText contains characters that are hard to serialize safely in JSON.") })), dryRun: z.boolean().default(false).describe("Preview changes using git-style diff format") }, @@ -383,7 +385,21 @@ server.registerTool( }, async (args: z.infer) => { const validPath = await validatePath(args.path); - const result = await applyFileEdits(validPath, args.edits, args.dryRun); + const edits = args.edits.map((edit) => { + const newText = edit.newText_base64 !== undefined + ? Buffer.from(edit.newText_base64, "base64").toString("utf-8") + : edit.newText; + + if (newText === undefined) { + throw new Error("Each edit must provide either newText or newText_base64"); + } + + return { + oldText: edit.oldText, + newText, + }; + }); + const result = await applyFileEdits(validPath, edits, args.dryRun); return { content: [{ type: "text" as const, text: result }], structuredContent: { content: result }