modelcontextprotocol/servers · error · Error
Could not find exact match for edit:\n
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.
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
- -32042
- Access denied - parent directory outside allowed directories
- Access denied - path outside allowed directories
- Access denied - symlink target outside allowed directories
AI-assisted analysis of modelcontextprotocol/servers@579c3903f3 (2026-08-12).
Data as JSON: /api/errors/62deb2e95ed13255.
Report an issue: GitHub.
Appendix: source
Thrown at src/filesystem/lib.ts:322
// 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 origStats = await fs.stat(filePath);View on GitHub (pinned to 579c3903f3)