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