Skip to main content
Glama
modelcontextprotocol

Filesystem MCP Server

Official

edit_file

Modify text files by replacing specific line sequences with new content. Generates git-style diffs to track changes and supports preview mode. Operates within designated directories for controlled file editing.

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
NameRequiredDescriptionDefault
pathYes
editsYes
dryRunNoPreview changes using git-style diff format

Implementation Reference

  • Zod schemas defining EditOperation and EditFileArgsSchema for validating inputs to the edit_file tool.
    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') });
  • Registers the 'edit_file' MCP tool with server.registerTool, including inline input schema, description, annotations, output schema, and inline handler function.
    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 } }; } );
  • Inline handler function registered for edit_file tool, which validates the path and delegates to applyFileEdits helper.
    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 } }; }
  • Core helper function implementing file editing logic: reads file, applies sequential text replacements with line-matching and indentation preservation, generates git-style unified diff, and performs atomic safe 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; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/modelcontextprotocol/filesystem'

If you have feedback or need assistance with the MCP directory API, please join our Discord server