edit_file
Modify text files by replacing specific line sequences with new content, generating a git-style diff to track changes within allowed directories.
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
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | Path to the file to edit | |
| edits | Yes | List of edit operations to perform | |
| dryRun | No | Preview changes using git-style diff format |
Implementation Reference
- src/utils/tools.ts:196-276 (handler)The editFile function that executes the core logic of the 'edit_file' tool: validates path, reads file, applies sequential exact-match text replacements preserving indentation, generates unified diff, writes changes if not dry-run.export async function editFile( args: z.infer<typeof EditFileArgsSchema>, config: Config ): Promise<string> { const endMetric = metrics.startOperation('edit_file') try { const validPath = await validatePath(args.path, config) // Read the original content const content = await fs.readFile(validPath, 'utf-8') let modifiedContent = content // Track whether any edit was applied let appliedAnyEdit = false // Apply each edit for (const edit of args.edits) { const contentLines = modifiedContent.split('\n') let matchFound = false // Normalize line endings const normalizedOld = edit.oldText.replace(/\r\n/g, '\n') const normalizedNew = edit.newText.replace(/\r\n/g, '\n') const oldLines = normalizedOld.split('\n') // Validate edit if (oldLines.length === 0) { throw new InvalidArgumentsError('edit_file', 'Edit operation contains empty oldText') } // Find and replace the text for (let i = 0; i <= contentLines.length - oldLines.length; i++) { const potentialMatch = contentLines.slice(i, i + oldLines.length).join('\n') if (potentialMatch === normalizedOld) { // Preserve indentation const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '' const newLines = normalizedNew.split('\n').map((line, j) => { if (j === 0) return originalIndent + line.trimStart() 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 appliedAnyEdit = true break } } if (!matchFound) { throw new Error(`Could not find exact match for edit:\n${edit.oldText}`) } } // If no edits were applied, return early if (!appliedAnyEdit) { return 'No changes made - all edit patterns were empty or not found' } // Generate diff const diff = createUnifiedDiff(content, modifiedContent, validPath) // Write file if not a dry run if (!args.dryRun) { await fs.writeFile(validPath, modifiedContent, 'utf-8') await logger.debug(`Successfully edited file: ${validPath}`) } endMetric() return diff } catch (error) { metrics.recordError('edit_file') throw error } }
- src/utils/tools.ts:175-187 (schema)Zod schemas defining EditOperation (single replacement) and EditFileArgsSchema (full input: path, array of edits, dryRun flag).export const EditOperation = z.object({ oldText: z.string().describe('Text to search for - must match exactly'), newText: z.string().describe('Text to replace with'), }) /** * Schema for edit_file arguments */ export const EditFileArgsSchema = z.object({ path: z.string().describe('Path to the file to edit'), edits: z.array(EditOperation).describe('List of edit operations to perform'), dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format'), })
- src/index.ts:263-268 (registration)Tool registration in the list_tools handler: defines name 'edit_file', description, and converts EditFileArgsSchema to JSON schema for MCP protocol.name: '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: zodToJsonSchema(EditFileArgsSchema) as ToolInput,
- src/index.ts:456-470 (helper)Dispatch handler case in main CallToolRequest handler: parses arguments with EditFileArgsSchema and invokes editFile function.case 'edit_file': { const parsed = EditFileArgsSchema.safeParse(a) if (!parsed.success) { throw new FileSystemError(`Invalid arguments for ${name}`, 'INVALID_ARGS', undefined, { errors: parsed.error.format(), }) } const result = await editFile(parsed.data, config) endMetric() return { content: [{ type: 'text', text: result }], } }
- src/utils/tools.ts:286-307 (helper)createUnifiedDiff helper used by editFile to generate git-style unified diff wrapped in markdown code block.function createUnifiedDiff( originalContent: string, modifiedContent: string, filePath: string ): string { const diff = createTwoFilesPatch( filePath, filePath, originalContent, modifiedContent, 'Original', 'Modified' ) // Find enough backticks to safely wrap the diff let numBackticks = 3 while (diff.includes('`'.repeat(numBackticks))) { numBackticks++ } return `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n` }