Package: @modelcontextprotocol/server-filesystem
Version: 2026.7.10
Node: v25.2.1
OS: Windows 11 (server process on Windows; reached over HTTP via supergateway)
Summary
move_file overwrites an existing destination file without warning and reports success. The README states the opposite:
- move_file
- Move or rename files and directories
- Inputs:
source (string), destination (string)
- Fails if destination exists
The tool description returned over the wire says the same thing:
If the destination exists, the operation will fail.
Because no existence check is performed, the destination file's contents are destroyed and unrecoverable. This is a data-loss bug, not just a documentation error: an agent that reads "fails if destination exists" will reasonably treat move_file as a safe, non-clobbering operation and use it without first checking whether the target is occupied.
The impact is amplified by the fact that this server ships no delete tool. Users (and agents) reasonably infer that the toolset cannot destroy existing files, only create and modify them. move_file quietly provides arbitrary file deletion: move anything onto a target and the target is gone.
That inference is actively encouraged by the README, which is itself inconsistent here. The Features section advertises:
Read/write files · Create/list/delete directories · Move files/directories
No delete tool exists in the API section or in the tool list returned at runtime. So a reader is told deletion is available, cannot find it, concludes the toolset is non-destructive by design — and then loses data to move_file.
Reproduction
Two files with different names, different contents, and different sizes, so the result is unambiguous.
-
write_file -> <allowed_dir>/_movetest/alpha_source.txt
FILE ALPHA
role: SOURCE
unique marker: AAAA-1111-SOURCE
length: this file is deliberately short.
-
write_file -> <allowed_dir>/_movetest/beta_destination.txt
FILE BETA
role: DESTINATION
unique marker: BBBB-2222-DESTINATION
...(padding so the sizes differ)...
-
list_directory_with_sizes on _movetest:
[FILE] alpha_source.txt 97 B
[FILE] beta_destination.txt 307 B
Total: 2 files, 0 directories
-
move_file with source = alpha_source.txt, destination = beta_destination.txt
Expected: error — destination exists.
Actual: success, no warning:
Successfully moved <...>/_movetest/alpha_source.txt to <...>/_movetest/beta_destination.txt
-
list_directory_with_sizes on _movetest:
[FILE] beta_destination.txt 97 B
Total: 1 files, 0 directories
-
read_text_file on beta_destination.txt returns ALPHA's contents. The marker BBBB-2222-DESTINATION is gone; 307 bytes were destroyed by an operation documented to refuse.
Cause
Presumably a bare fs.rename with no prior existence check — which clobbers on every platform Node supports.
POSIX rename(2) replaces an existing destination by definition. On Windows, libuv's fs__rename calls MoveFileEx with MOVEFILE_REPLACE_EXISTING (added in joyent/libuv#283 precisely to match Linux behaviour), and per the Win32 docs that flag replaces the contents of the destination file. The libuv docs accordingly describe uv_fs_rename as equivalent to rename(2).
So the overwrite is the platform default in both cases, and the README documents an intent that was never implemented in code.
Note on tool annotations
move_file currently carries destructiveHint: true, but the README's own rationale for it is "Deletes source file" — i.e. the annotation is justified by the loss of the source, and the destination is still assumed safe. See #2988, which proposed destructiveHint: false on the explicit grounds that "a repeat typically errors because source no longer exists and README says 'fails if destination exists'".
So the reasoning around this tool's safety has been anchored on a README claim the code does not honour. Worth revisiting the note and rationale even though the flag itself happens to be correct.
Suggested fix
Either:
(a) Implement the documented behaviour — stat the destination first and reject if it exists:
try {
await fs.stat(validDestPath);
throw new Error(`Destination already exists: ${destination}`);
} catch (e) {
if (e.code !== "ENOENT") throw e;
}
await fs.rename(validSourcePath, validDestPath);
Note this is TOCTOU-racy but adequate for this server's threat model; a stricter option on Linux is renameat2 with RENAME_NOREPLACE.
(b) Keep the overwrite behaviour but fix the README and the tool description to say so plainly, mirroring write_file's existing wording ("exercise caution with this"), and add an explicit overwrite: boolean input defaulting to false.
(a) is preferable: it matches every published description of the tool, and clients that gate on destructiveHint today do so believing the destination is protected.
Package:
@modelcontextprotocol/server-filesystemVersion:
2026.7.10Node:
v25.2.1OS: Windows 11 (server process on Windows; reached over HTTP via supergateway)
Summary
move_fileoverwrites an existing destination file without warning and reports success. The README states the opposite:The tool description returned over the wire says the same thing:
Because no existence check is performed, the destination file's contents are destroyed and unrecoverable. This is a data-loss bug, not just a documentation error: an agent that reads "fails if destination exists" will reasonably treat
move_fileas a safe, non-clobbering operation and use it without first checking whether the target is occupied.The impact is amplified by the fact that this server ships no delete tool. Users (and agents) reasonably infer that the toolset cannot destroy existing files, only create and modify them.
move_filequietly provides arbitrary file deletion: move anything onto a target and the target is gone.That inference is actively encouraged by the README, which is itself inconsistent here. The Features section advertises:
No delete tool exists in the API section or in the tool list returned at runtime. So a reader is told deletion is available, cannot find it, concludes the toolset is non-destructive by design — and then loses data to
move_file.Reproduction
Two files with different names, different contents, and different sizes, so the result is unambiguous.
write_file-><allowed_dir>/_movetest/alpha_source.txtwrite_file-><allowed_dir>/_movetest/beta_destination.txtlist_directory_with_sizeson_movetest:move_filewithsource = alpha_source.txt,destination = beta_destination.txtExpected: error — destination exists.
Actual: success, no warning:
list_directory_with_sizeson_movetest:read_text_fileonbeta_destination.txtreturns ALPHA's contents. The markerBBBB-2222-DESTINATIONis gone; 307 bytes were destroyed by an operation documented to refuse.Cause
Presumably a bare
fs.renamewith no prior existence check — which clobbers on every platform Node supports.POSIX
rename(2)replaces an existing destination by definition. On Windows, libuv'sfs__renamecallsMoveFileExwithMOVEFILE_REPLACE_EXISTING(added in joyent/libuv#283 precisely to match Linux behaviour), and per the Win32 docs that flag replaces the contents of the destination file. The libuv docs accordingly describeuv_fs_renameas equivalent torename(2).So the overwrite is the platform default in both cases, and the README documents an intent that was never implemented in code.
Note on tool annotations
move_filecurrently carriesdestructiveHint: true, but the README's own rationale for it is "Deletes source file" — i.e. the annotation is justified by the loss of the source, and the destination is still assumed safe. See #2988, which proposeddestructiveHint: falseon the explicit grounds that "a repeat typically errors because source no longer exists and README says 'fails if destination exists'".So the reasoning around this tool's safety has been anchored on a README claim the code does not honour. Worth revisiting the note and rationale even though the flag itself happens to be correct.
Suggested fix
Either:
(a) Implement the documented behaviour —
statthe destination first and reject if it exists:Note this is TOCTOU-racy but adequate for this server's threat model; a stricter option on Linux is
renameat2withRENAME_NOREPLACE.(b) Keep the overwrite behaviour but fix the README and the tool description to say so plainly, mirroring
write_file's existing wording ("exercise caution with this"), and add an explicitoverwrite: booleaninput defaulting tofalse.(a) is preferable: it matches every published description of the tool, and clients that gate on
destructiveHinttoday do so believing the destination is protected.