{"record":{"id":"62deb2e95ed13255","repo":"modelcontextprotocol/servers","slug":"could-not-find-exact-match-for-edit-n-edit-oldte","errorCode":null,"errorMessage":"Could not find exact match for edit:\\n${edit.oldText}","messagePattern":"Could not find exact match for edit:\\\\n(.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/filesystem/lib.ts","lineNumber":251,"sourceCode":"          // For subsequent lines, try to preserve relative indentation\n          const oldIndent = oldLines[j]?.match(/^\\s*/)?.[0] || '';\n          const newIndent = line.match(/^\\s*/)?.[0] || '';\n          if (oldIndent && newIndent) {\n            const relativeIndent = newIndent.length - oldIndent.length;\n            return originalIndent + ' '.repeat(Math.max(0, relativeIndent)) + line.trimStart();\n          }\n          return line;\n        });\n\n        contentLines.splice(i, oldLines.length, ...newLines);\n        modifiedContent = contentLines.join('\\n');\n        matchFound = true;\n        break;\n      }\n    }\n\n    if (!matchFound) {\n      throw new Error(`Could not find exact match for edit:\\n${edit.oldText}`);\n    }\n  }\n\n  // Create unified diff\n  const diff = createUnifiedDiff(content, modifiedContent, filePath);\n\n  // Format diff with appropriate number of backticks\n  let numBackticks = 3;\n  while (diff.includes('`'.repeat(numBackticks))) {\n    numBackticks++;\n  }\n  const formattedDiff = `${'`'.repeat(numBackticks)}diff\\n${diff}${'`'.repeat(numBackticks)}\\n\\n`;\n\n  if (!dryRun) {\n    // Security: Use atomic rename to prevent race conditions where symlinks\n    // could be created between validation and write. Rename operations\n    // replace the target file atomically and don't follow symlinks.\n    const tempPath = `${filePath}.${randomBytes(16).toString('hex')}.tmp`;","sourceCodeStart":233,"sourceCodeEnd":269,"githubUrl":"https://github.com/modelcontextprotocol/servers/blob/76d64c822f5125032f89eb71dbdb94e42b434821/src/filesystem/lib.ts#L233-L269","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait applyFileEdits(path, [{ oldText: 'function foo() {', newText: 'function bar() {' }], false);\n// oldText not present verbatim -> throws\n\n// after: read first, copy exact text, optionally dryRun\nconst current = await fs.readFile(path, 'utf-8');\nconst oldText = current.match(/function \\w+\\(\\) \\{/)[0]; // exact substring\nawait applyFileEdits(path, [{ oldText, newText: 'function bar() {' }], false);","handlingStrategy":"validation","validationCode":"import * as fs from 'node:fs/promises';\nasync function editsWillApply(path: string, edits: {oldText:string;newText:string}[]): Promise<{ok:boolean; missing?: string}> {\n  let content = (await fs.readFile(path,'utf-8')).replace(/\\r\\n/g,'\\n');\n  for (const e of edits) {\n    const old = e.oldText.replace(/\\r\\n/g,'\\n');\n    if (content.includes(old)) { content = content.replace(old, e.newText.replace(/\\r\\n/g,'\\n')); continue; }\n    // replicate whitespace-tolerant line match\n    const oldLines = old.split('\\n');\n    const lines = content.split('\\n');\n    let found = false;\n    for (let i=0; i<=lines.length-oldLines.length; i++) {\n      if (oldLines.every((ol,j)=> ol.trim()===lines[i+j].trim())) { found = true; break; }\n    }\n    if (!found) return { ok:false, missing: old };\n  }\n  return { ok:true };\n}","typeGuard":"function isFileEditArray(v: unknown): v is { oldText: string; newText: string }[] {\n  return Array.isArray(v) && v.every(e => e && typeof (e as any).oldText === 'string' && typeof (e as any).newText === 'string');\n}","tryCatchPattern":"try {\n  await applyFileEdits(path, edits, dryRun);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Could not find exact match for edit:')) {\n    // extract the unmatched oldText from e.message and re-read the file to reconcile\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["filesystem","typescript","content-mismatch","edit"],"backgroundTag":null,"analyzedSha":"76d64c822f5125032f89eb71dbdb94e42b434821","analyzedAt":"2026-08-12T10:02:41.718Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}