Description
Problem
edit_file's tool description (since #5379) states the matching rule: "whole lines match with leading and trailing whitespace ignored". An oldText made of whole lines that ends with a newline breaks it. When the text differs from the file only in indentation, the match fails. Without the final newline, the same oldText matches.
The whitespace-tolerant matcher (#4990 / #4991) splits oldText on \n, so the trailing newline becomes an empty final "line". That empty line then has to equal the next line of the file. Exact matching is unaffected; only the fallback that ignores leading and trailing whitespace is.
Models often end oldText with a newline when they copy whole lines, so this costs a failed call and a retry. Since #5379 the error at least points at the extra empty line.
Found while verifying the v2.3.0 release candidate (#5425). It is not a regression: @modelcontextprotocol/server-filesystem 2.1.0 fails the same way.
Reproduce
Server: server-filesystem 2.2.0 candidate (cc602786) and published 2.1.0. Client: Inspector CLI 2.13.0, --protocol-era legacy and modern (same result in both).
Fixture fx/code.py, as a JSON string (tabs for indentation, two trailing spaces on line 3):
"def f():\n\tif x:\n\t\treturn 1 \n\treturn 2\n"npx -y @modelcontextprotocol/inspector@2.13.0 --cli node src/filesystem/dist/index.js fx \
--method tools/call --tool-name edit_file --format json \
--tool-args-json '{"path":"fx/code.py","edits":[{"oldText":"if x:\n return 1\n","newText":"if x:\n return 9\n"}],"dryRun":true}'2.2.0 candidate (exit 5, isError: true):
Could not find exact match for edit 1 of 1. oldText must match the file's text exactly; only line endings, and leading and trailing whitespace on whole lines, may differ. No edits were applied.
…
First difference, oldText line 3 against file line 4:
oldText: ""
file: "\treturn 2"
2.1.0: Could not find exact match for edit: (exit 5).
Control: the same call with oldText "if x:\n return 1" (no final newline) returns the diff on both versions.
Expected
A trailing newline ends the last line of oldText; it does not add an empty line. The whitespace-tolerant match should succeed and keep the file's indentation, as it does without the final newline. newText's trailing newline should be handled consistently with it.
Activity
fix(filesystem): match edit_file oldText that ends with a newline