From 00c37724c941087498cbacbf87e71c254027f424 Mon Sep 17 00:00:00 2001 From: Ev3lynx727 Date: Sat, 20 Jun 2026 22:03:53 +0700 Subject: [PATCH 1/3] feat(filesystem): add optional content_base64 / newText_base64 parameters Add optional content_base64 parameter to write_file and newText_base64 parameter to edit_file (per-edit). Base64 charset (A-Za-z0-9+/=) is fully JSON-safe, eliminating transport-level JSON serialization failures when file content contains backticks, quotes, template literals, or other special characters. - WriteFileArgsSchema: content becomes optional, content_base64 added - EditOperation: newText becomes optional, newText_base64 added - Both schemas use .refine() to require at least one of the two - Handlers decode base64 server-side via Buffer.from() - Fully backward-compatible: existing callers using content/newText continue unchanged Refs: #4394 --- src/filesystem/index.ts | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 7b67e63e58..2be3c35c0c 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -112,13 +112,21 @@ const ReadMultipleFilesArgsSchema = z.object({ const WriteFileArgsSchema = z.object({ path: z.string(), - content: z.string(), -}); + content: z.string().optional(), + content_base64: z.string().optional(), +}).refine( + args => args.content !== undefined || args.content_base64 !== undefined, + { message: "Must provide either content or content_base64" } +); 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 replacement text') +}).refine( + args => args.newText !== undefined || args.newText_base64 !== undefined, + { message: "Must provide either newText or newText_base64" } +); const EditFileArgsSchema = z.object({ path: z.string(), @@ -353,7 +361,10 @@ server.registerTool( }, async (args: z.infer) => { const validPath = await validatePath(args.path); - await writeFileContent(validPath, args.content); + const content = args.content_base64 + ? Buffer.from(args.content_base64, 'base64').toString('utf-8') + : args.content!; + await writeFileContent(validPath, content); const text = `Successfully wrote to ${args.path}`; return { content: [{ type: "text" as const, text }], @@ -383,7 +394,13 @@ 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 => ({ + ...edit, + newText: edit.newText_base64 + ? Buffer.from(edit.newText_base64, 'base64').toString('utf-8') + : edit.newText! + })); + const result = await applyFileEdits(validPath, edits, args.dryRun); return { content: [{ type: "text" as const, text: result }], structuredContent: { content: result } From 07368da0b64cd1748cced63cd1324abc04abf368 Mon Sep 17 00:00:00 2001 From: Ev3lynx727 Date: Sat, 20 Jun 2026 22:51:07 +0700 Subject: [PATCH 2/3] fix: pass full Zod schemas as inputSchema so content_base64 is exposed to MCP clients The write_file and edit_file tool registrations used plain-object inputSchema ({ path: z.string(), content: z.string() }) that excluded content_base64/newText_base64. The MCP SDK validates against inputSchema, so these parameters were silently stripped before the handler ran. Fixing by passing the full Zod schemas (WriteFileArgsSchema, EditFileArgsSchema) which include the optional base64 parameters. E2E verified: server exposes content_base64 in schema, client sends it, server decodes and writes correctly with all 147 existing tests passing. --- src/filesystem/index.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 2be3c35c0c..492bf83b62 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -352,10 +352,7 @@ server.registerTool( "Create a new file or completely overwrite an existing file with new content. " + "Use with caution as it will overwrite existing files without warning. " + "Handles text content with proper encoding. Only works within allowed directories.", - inputSchema: { - path: z.string(), - content: z.string() - }, + inputSchema: WriteFileArgsSchema, outputSchema: { content: z.string() }, annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true } }, @@ -381,14 +378,7 @@ server.registerTool( "Make line-based edits to a text file. Each edit replaces exact line sequences " + "with new content. Returns a git-style diff showing the changes made. " + "Only works within allowed directories.", - inputSchema: { - 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") - })), - dryRun: z.boolean().default(false).describe("Preview changes using git-style diff format") - }, + inputSchema: EditFileArgsSchema, outputSchema: { content: z.string() }, annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true } }, From 1035db585c4a676aa2e500ac99dba000eab9f1f2 Mon Sep 17 00:00:00 2001 From: Ev3lynx727 Date: Sat, 20 Jun 2026 23:05:33 +0700 Subject: [PATCH 3/3] feat: add auto-mkdir to write_file handler for parity with server-commands-rtk Before writing, ensure parent directory exists via fs.mkdir(recursive:true). Closes the gap between forked filesystem (validation + path security) and server-commands-rtk (auto-mkdir). --- src/filesystem/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 492bf83b62..af803621f8 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -361,6 +361,7 @@ server.registerTool( const content = args.content_base64 ? Buffer.from(args.content_base64, 'base64').toString('utf-8') : args.content!; + await fs.mkdir(path.dirname(validPath), { recursive: true }); await writeFileContent(validPath, content); const text = `Successfully wrote to ${args.path}`; return {