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
- Re-read the current file content and copy the exact text into oldText before calling edit_file.
- If batching multiple edits, make sure each edit's oldText still matches after all preceding edits apply; otherwise split into separate calls.
- Remove stray leading/trailing characters, line-number prefixes, or indentation that differs by more than per-line trim.
- 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
- Always read the current file content immediately before constructing oldText.
- For multi-edit batches, model each edit against the result of the previous one.
- Use dryRun:true first to validate all matches without mutating the file.
- Avoid copying text from sources that include line numbers or diff markers.
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
- Parent directory does not exist: ${parentDir}
- Invalid resourceType: ${args?.resourceType}. Must be ${RESOU
- Invalid resourceId: ${args?.resourceId}. Must be a finite po
- Unknown resource: ${uri.toString()}
- Invalid resourceType: ${args?.resourceType}. Must be ${RESOU
AI-assisted analysis of modelcontextprotocol/servers@76d64c822f (2026-08-12).
Data as JSON: /api/errors/62deb2e95ed13255.
Report an issue: GitHub.