modelcontextprotocol/servers · error · Error

Could not find exact match for edit:\n${edit.oldText}

Error message

Could not find exact match for edit:\n${edit.oldText}

What it means

Thrown by applyFileEdits() in the filesystem server (the edit_file tool) when neither an exact substring match nor the whitespace-tolerant line-by-line matcher can locate edit.oldText in the file. Edits are applied sequentially to the in-memory content, so a match can also fail when an earlier edit in the same batch already mutated the text the later edit expected. The message echoes the unmatched oldText to aid diagnosis.

Source

Thrown at src/filesystem/lib.ts:251

          // 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`;

View on GitHub (pinned to 76d64c822f)

Solutions

  1. Re-read the current file content and copy the exact text into oldText before calling edit_file.
  2. If batching multiple edits, make sure each edit's oldText still matches after all preceding edits apply; otherwise split into separate calls.
  3. Remove stray leading/trailing characters, line-number prefixes, or indentation that differs by more than per-line trim.
  4. Validate first with dryRun:true, which runs matching without writing, to confirm every edit resolves.

Example fix

// before
await applyFileEdits(path, [{ oldText: 'function foo() {', newText: 'function bar() {' }], false);
// oldText not present verbatim -> throws

// after: read first, copy exact text, optionally dryRun
const current = await fs.readFile(path, 'utf-8');
const oldText = current.match(/function \w+\(\) \{/)[0]; // exact substring
await applyFileEdits(path, [{ oldText, newText: 'function bar() {' }], false);
Defensive patterns

Strategy: validation

Validate before calling

import * as fs from 'node:fs/promises';
async function editsWillApply(path: string, edits: {oldText:string;newText:string}[]): Promise<{ok:boolean; missing?: string}> {
  let content = (await fs.readFile(path,'utf-8')).replace(/\r\n/g,'\n');
  for (const e of edits) {
    const old = e.oldText.replace(/\r\n/g,'\n');
    if (content.includes(old)) { content = content.replace(old, e.newText.replace(/\r\n/g,'\n')); continue; }
    // replicate whitespace-tolerant line match
    const oldLines = old.split('\n');
    const lines = content.split('\n');
    let found = false;
    for (let i=0; i<=lines.length-oldLines.length; i++) {
      if (oldLines.every((ol,j)=> ol.trim()===lines[i+j].trim())) { found = true; break; }
    }
    if (!found) return { ok:false, missing: old };
  }
  return { ok:true };
}

Type guard

function isFileEditArray(v: unknown): v is { oldText: string; newText: string }[] {
  return Array.isArray(v) && v.every(e => e && typeof (e as any).oldText === 'string' && typeof (e as any).newText === 'string');
}

Try / catch

try {
  await applyFileEdits(path, edits, dryRun);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Could not find exact match for edit:')) {
    // extract the unmatched oldText from e.message and re-read the file to reconcile
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling edit_file with oldText that does not occur in the file; supplying several edits where a later edit's oldText no longer exists after an earlier edit ran; oldText differs by characters that are not per-line whitespace (e.g. renamed identifier, extra symbol) so the trimmed-line comparison also fails.

Common situations: oldText was copied from a different branch or a stale read; the file changed on disk between the read and the edit; the caller copied text that included line-number prefixes or other artifacts; large multi-edit batches where ordering was not accounted for.

Related errors


AI-assisted analysis of modelcontextprotocol/servers@76d64c822f (2026-08-12). Data as JSON: /api/errors/62deb2e95ed13255. Report an issue: GitHub.