edit_file_content
Modify text files by replacing specified content with new text, preserving indentation and generating a git-style diff. Use it to preview changes or apply edits directly to files in the workspace.
Instructions
Make line-based edits to a text file in the workspace filesystem. Each edit attempts to replace an exact match of 'oldText' with 'newText'. If no exact match is found, it attempts a line-by-line match ignoring leading/trailing whitespace. Indentation of the first line is preserved, and relative indentation of subsequent lines is attempted. Returns a git-style diff showing the changes made (or previewed if dryRun is true).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| dryRun | No | If true, preview changes using git-style diff format without saving. | |
| edits | Yes | An array of edit operations to apply sequentially. | |
| path | Yes | The path of the file to edit (relative to the workspace directory). |
Input Schema (JSON Schema)
Implementation Reference
- src/index.ts:133-194 (handler)Core handler function that applies sequential edit operations to a file: reads content, performs replacements (exact or line-by-line whitespace-insensitive), preserves indentation, generates unified diff, writes if not dry-run, and formats output.async function applyFileEdits( filePath: string, edits: z.infer<typeof EditOperationSchema>[], dryRun = false ): Promise<string> { const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8')); let modifiedContent = content; for (const edit of edits) { const normalizedOld = normalizeLineEndings(edit.oldText); const normalizedNew = normalizeLineEndings(edit.newText); if (modifiedContent.includes(normalizedOld)) { modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew); continue; } 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); const isMatch = oldLines.every((oldLine, j) => oldLine.trim() === potentialMatch[j].trim()); if (isMatch) { 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; break; } } if (!matchFound) { throw new Error(`Could not find exact or whitespace-insensitive match for edit:\n${edit.oldText}`); } } const diff = createUnifiedDiff(content, modifiedContent, path.relative(process.cwd(), filePath)); if (!dryRun) { await fs.writeFile(filePath, modifiedContent, 'utf-8'); } let numBackticks = 3; while (diff.includes('`'.repeat(numBackticks))) { numBackticks++; } return `${'`'.repeat(numBackticks)}diff\n${diff}\n${'`'.repeat(numBackticks)}`; }
- src/index.ts:454-462 (handler)Tool dispatch handler in the main switch statement: validates arguments using EditFileArgsSchema, checks edits array, sanitizes path, and invokes applyFileEdits.case "edit_file_content": { const parsed = EditFileArgsSchema.parse(args); if (parsed.edits.length === 0) { throw new McpError(ErrorCode.InvalidParams, `'edits' array cannot be empty for ${toolName}.`); } const validPath = validateWorkspacePath(parsed.path); resultText = await applyFileEdits(validPath, parsed.edits, parsed.dryRun); break; }
- src/tools/edit_file.ts:6-20 (schema)Zod schemas for EditOperation (oldText, newText) and EditFileArgs (path, edits array, optional dryRun), converted to JSON schema for tool input validation.// Schema definitions (adapted from example.ts) - Exported export const EditOperationSchema = z.object({ oldText: z.string().describe('Text to search for - attempts exact match first, then line-by-line whitespace-insensitive match.'), newText: z.string().describe('Text to replace with, preserving indentation where possible.') }); export const EditFileArgsSchema = z.object({ path: z.string().describe("The path of the file to edit (relative to the workspace directory)."), edits: z.array(EditOperationSchema).describe("An array of edit operations to apply sequentially."), dryRun: z.boolean().optional().default(false).describe('If true, preview changes using git-style diff format without saving.') }); // Convert Zod schema to JSON schema const EditFileJsonSchema = zodToJsonSchema(EditFileArgsSchema);
- src/tools/index.ts:11-51 (registration)Imports the editFileTool definition and registers it in the allTools array, which is used by the MCP server for tool listing and mapping.import { editFileTool } from "./edit_file.js"; // import { createDirectoryTool } from "./create_directory.js"; // Removed import { listDirectoryTool } from "./list_directory.js"; import { directoryTreeTool } from "./directory_tree.js"; import { moveFileTool } from "./move_file.js"; import { searchFilesTool } from "./search_files.js"; import { getFileInfoTool } from "./get_file_info.js"; import { executeTerminalCommandTool } from "./execute_terminal_command.js"; // Renamed file and tool variable // Import the new combined tools import { saveGenerateProjectGuidelinesTool } from "./save_generate_project_guidelines.js"; import { saveDocSnippetTool } from "./save_doc_snippet.js"; import { saveTopicExplanationTool } from "./save_topic_explanation.js"; // Removed old save_query_answer, added new specific ones import { saveAnswerQueryDirectTool } from "./save_answer_query_direct.js"; import { saveAnswerQueryWebsearchTool } from "./save_answer_query_websearch.js"; // Import new research-oriented tools import { codeAnalysisWithDocsTool } from "./code_analysis_with_docs.js"; import { technicalComparisonTool } from "./technical_comparison.js"; import { architecturePatternRecommendationTool } from "./architecture_pattern_recommendation.js"; import { dependencyVulnerabilityScanTool } from "./dependency_vulnerability_scan.js"; import { databaseSchemaAnalyzerTool } from "./database_schema_analyzer.js"; import { securityBestPracticesAdvisorTool } from "./security_best_practices_advisor.js"; import { testingStrategyGeneratorTool } from "./testing_strategy_generator.js"; import { regulatoryComplianceAdvisorTool } from "./regulatory_compliance_advisor.js"; import { microserviceDesignAssistantTool } from "./microservice_design_assistant.js"; import { documentationGeneratorTool } from "./documentation_generator.js"; export const allTools: ToolDefinition[] = [ // Query & Generation Tools answerQueryWebsearchTool, answerQueryDirectTool, explainTopicWithDocsTool, getDocSnippetsTool, generateProjectGuidelinesTool, // Filesystem Tools readFileTool, // Handles single and multiple files now // readMultipleFilesTool, // Merged into readFileTool writeFileTool, editFileTool,
- src/tools/edit_file.ts:21-49 (helper)ToolDefinition object with name, description, inputSchema, and buildPrompt for parameter validation (used in generic path, but overridden by filesystem handler).export const editFileTool: ToolDefinition = { name: "edit_file_content", // Renamed slightly description: "Make line-based edits to a text file in the workspace filesystem. Each edit attempts to replace " + "an exact match of 'oldText' with 'newText'. If no exact match is found, it attempts a " + "line-by-line match ignoring leading/trailing whitespace. Indentation of the first line " + "is preserved, and relative indentation of subsequent lines is attempted. " + "Returns a git-style diff showing the changes made (or previewed if dryRun is true).", inputSchema: EditFileJsonSchema as any, // Cast as any if needed // Minimal buildPrompt as execution logic is separate buildPrompt: (args: any, modelId: string) => { const parsed = EditFileArgsSchema.safeParse(args); if (!parsed.success) { throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for edit_file_content: ${parsed.error}`); } // Add a check for empty edits array if (parsed.data.edits.length === 0) { throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for edit_file_content: 'edits' array cannot be empty.`); } return { systemInstructionText: "", userQueryText: "", useWebSearch: false, enableFunctionCalling: false }; }, // No 'execute' function here };