edit_file
Modify text files by replacing exact line sequences with new content. Generates a git-style diff to track changes. Works only within specified directories for controlled editing tasks.
Instructions
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.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| dryRun | No | Preview changes using git-style diff format | |
| edits | Yes | ||
| path | Yes |
Input Schema (JSON Schema)
{
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"dryRun": {
"default": false,
"description": "Preview changes using git-style diff format",
"type": "boolean"
},
"edits": {
"items": {
"additionalProperties": false,
"properties": {
"newText": {
"description": "Text to replace with",
"type": "string"
},
"oldText": {
"description": "Text to search for - must match exactly",
"type": "string"
}
},
"required": [
"oldText",
"newText"
],
"type": "object"
},
"type": "array"
},
"path": {
"type": "string"
}
},
"required": [
"path",
"edits"
],
"type": "object"
}
Implementation Reference
- src/filesystem/index.ts:365-372 (handler)The main handler function for the 'edit_file' tool. It validates the file path and calls the applyFileEdits helper to perform the edits, returning the result (diff or confirmation).async (args: z.infer<typeof EditFileArgsSchema>) => { const validPath = await validatePath(args.path); const result = await applyFileEdits(validPath, args.edits, args.dryRun); return { content: [{ type: "text" as const, text: result }], structuredContent: { content: result } }; }
- src/filesystem/index.ts:99-108 (schema)Zod schemas defining the input structure for the 'edit_file' tool, including EditOperation for individual edits.const EditOperation = z.object({ oldText: z.string().describe('Text to search for - must match exactly'), newText: z.string().describe('Text to replace with') }); const EditFileArgsSchema = z.object({ path: z.string(), edits: z.array(EditOperation), dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format') });
- src/filesystem/index.ts:346-373 (registration)Registration of the 'edit_file' tool using server.registerTool, including title, description, input/output schemas, and annotations.server.registerTool( "edit_file", { title: "Edit File", description: "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") }, outputSchema: { content: z.string() }, annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true } }, async (args: z.infer<typeof EditFileArgsSchema>) => { const validPath = await validatePath(args.path); const result = await applyFileEdits(validPath, args.edits, args.dryRun); return { content: [{ type: "text" as const, text: result }], structuredContent: { content: result } }; } );
- src/filesystem/lib.ts:171-259 (helper)The core helper function that implements the file editing logic: reads content, applies sequential edits (exact or line-trim matched), generates unified git-style diff, and performs atomic write if not dry-run.export async function applyFileEdits( filePath: string, edits: FileEdit[], dryRun: boolean = false ): Promise<string> { // Read file content and normalize line endings const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8')); // Apply edits sequentially let modifiedContent = content; for (const edit of edits) { const normalizedOld = normalizeLineEndings(edit.oldText); const normalizedNew = normalizeLineEndings(edit.newText); // If exact match exists, use it if (modifiedContent.includes(normalizedOld)) { modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew); continue; } // Otherwise, try line-by-line matching with flexibility for whitespace const oldLines = normalizedOld.split('\n'); const contentLines = modifiedContent.split('\n'); let matchFound = false; for (let i = 0; i <= contentLines.length - oldLines.length; i++) { const potentialMatch = contentLines.slice(i, i + oldLines.length); // Compare lines with normalized whitespace const isMatch = oldLines.every((oldLine, j) => { const contentLine = potentialMatch[j]; return oldLine.trim() === contentLine.trim(); }); if (isMatch) { // Preserve original indentation of first line const originalIndent = contentLines[i].match(/^\s*/)?.[0] || ''; const newLines = normalizedNew.split('\n').map((line, j) => { if (j === 0) return originalIndent + line.trimStart(); // For subsequent lines, try to preserve relative indentation const oldIndent = oldLines[j]?.match(/^\s*/)?.[0] || ''; const newIndent = line.match(/^\s*/)?.[0] || ''; if (oldIndent && newIndent) { const relativeIndent = newIndent.length - oldIndent.length; return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart(); } return line; }); contentLines.splice(i, oldLines.length, ...newLines); modifiedContent = contentLines.join('\n'); matchFound = true; break; } } if (!matchFound) { throw new Error(`Could not find exact match for edit:\n${edit.oldText}`); } } // Create unified diff const diff = createUnifiedDiff(content, modifiedContent, filePath); // Format diff with appropriate number of backticks let numBackticks = 3; while (diff.includes('`'.repeat(numBackticks))) { numBackticks++; } const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`; if (!dryRun) { // Security: Use atomic rename to prevent race conditions where symlinks // could be created between validation and write. Rename operations // replace the target file atomically and don't follow symlinks. const tempPath = `${filePath}.${randomBytes(16).toString('hex')}.tmp`; try { await fs.writeFile(tempPath, modifiedContent, 'utf-8'); await fs.rename(tempPath, filePath); } catch (error) { try { await fs.unlink(tempPath); } catch {} throw error; } } return formattedDiff; }