You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When the MCP client supports the roots protocol, the filesystem server's oninitialized handler replaces all command-line allowed directories with only the roots provided by the client. This means any additional directories passed via CLI args are silently discarded.
Steps to Reproduce
Configure the filesystem MCP server with multiple allowed directories via CLI args:
Connect from a client that supports the MCP roots protocol (e.g., Claude Code) where the working directory is /home/user
Call list_allowed_directories
Expected Behavior
All four directories should be listed as allowed:
/home/user
/mnt/Storage
/mnt/Games
/mnt/Emulators
Actual Behavior
Only the client-provided root is listed:
/home/user
The other three directories are silently dropped and become inaccessible.
Root Cause
In dist/index.js, the oninitialized handler (around line 564) unconditionally replaces allowedDirectories with the client roots when the client supports the roots protocol:
And updateAllowedDirectoriesFromRoots does a full replacement:
allowedDirectories=[...validatedRootDirs];
Suggested Fix
CLI-provided directories should be preserved and merged with client roots, not replaced. For example:
asyncfunctionupdateAllowedDirectoriesFromRoots(requestedRoots){constvalidatedRootDirs=awaitgetValidRootDirectories(requestedRoots);if(validatedRootDirs.length>0){// Merge with existing CLI-provided directories instead of replacingconstmerged=newSet([...allowedDirectories, ...validatedRootDirs]);allowedDirectories=[...merged];setAllowedDirectories(allowedDirectories);}}
The same fix should apply to the RootsListChangedNotificationSchema handler.
The roots/list replacement composes into a working chain. After updateAllowedDirectoriesFromRoots() runs, list_allowed_directories reports the attacker-supplied scope as the operator's scope. The original CLI allowlist is gone.
PR #3970 proposed the intersection check and was closed unmerged in May. Given this is now part of a demonstrated chain, worth revisiting.
Filing full disclosure separately. Will reference this issue.
Confirmed with @modelcontextprotocol/server-filesystem 2026.8.31 on Windows 11 with Claude Code as the client. Claude Code answers ListRoots with the session working folder. With node dist/index.js C:\data\docs as the argument and a session started in C:\other\project, tool calls on C:\data\docs returned Access denied - path outside allowed directories: ... not in C:\other\project. A probe client that declares no roots got the CLI folder. So a user cannot pin the server to a folder when the client declares roots. A flag such as --no-roots or --roots=ignore, or a README note that roots replace the CLI folder, would prevent this surprise.
I reproduced this locally with @modelcontextprotocol/server-filesystem@1.0.0 and the MCP client SDK 2.3.1.
With the CLI scoped to one directory, supplying a different directory through client Roots replaced the effective scope. I verified the change with list_allowed_directories and actual read calls against synthetic files. Sending an empty Roots update also removed the previously allowed scope.
I'm trying to clarify the intended authorization boundary here. For deployments with explicit CLI directory settings, is there a recommended way to preserve the operator's approved scope when the client supplies different Roots?
Since Roots is now deprecated, I'd also be interested in the recommended migration path for this setup.
This is a security report, so it is handled by the maintainers and the details aren't discussed here. If you have more to add, please use the advisory form rather than this thread.
Board: Servers V2 (#43), Status Incoming, awaiting maintainer review (no milestone yet). Part of the backlog triage in #4875.
Activity
Bingtagui404 commentedon Mar 17, 2026
Hi, I'd like to take a shot at this. Will submit a PR soon.
john-kearney commentedon Sep 16, 2026
The roots/list replacement composes into a working chain. After
updateAllowedDirectoriesFromRoots()runs,list_allowed_directoriesreports the attacker-supplied scope as the operator's scope. The original CLI allowlist is gone.PR #3970 proposed the intersection check and was closed unmerged in May. Given this is now part of a demonstrated chain, worth revisiting.
Filing full disclosure separately. Will reference this issue.
test(filesystem): characterize every tool in-process and gate coverag…
test(filesystem): characterize every tool in-process and gate coverag…
test(filesystem): mark the tests that pin known bugs
test(filesystem): characterize every tool in-process and gate coverag…
test(filesystem): mark the tests that pin known bugs
batuhanocal commentedon Oct 12, 2026
Confirmed with @modelcontextprotocol/server-filesystem 2026.8.31 on Windows 11 with Claude Code as the client. Claude Code answers ListRoots with the session working folder. With
node dist/index.js C:\data\docsas the argument and a session started inC:\other\project, tool calls onC:\data\docsreturnedAccess denied - path outside allowed directories: ... not in C:\other\project. A probe client that declares no roots got the CLI folder. So a user cannot pin the server to a folder when the client declares roots. A flag such as--no-rootsor--roots=ignore, or a README note that roots replace the CLI folder, would prevent this surprise.Abdullah-m7 commentedon Oct 16, 2026
I reproduced this locally with
@modelcontextprotocol/server-filesystem@1.0.0and the MCP client SDK 2.3.1.With the CLI scoped to one directory, supplying a different directory through client Roots replaced the effective scope. I verified the change with
list_allowed_directoriesand actual read calls against synthetic files. Sending an empty Roots update also removed the previously allowed scope.I'm trying to clarify the intended authorization boundary here. For deployments with explicit CLI directory settings, is there a recommended way to preserve the operator's approved scope when the client supplies different Roots?
Since Roots is now deprecated, I'd also be interested in the recommended migration path for this setup.
I can share a small local reproduction if useful.
cliffhall commentedon Oct 19, 2026
Triage: Priority Medium (total 8)
This is a security report, so it is handled by the maintainers and the details aren't discussed here. If you have more to add, please use the advisory form rather than this thread.
Board: Servers V2 (#43), Status
Incoming, awaiting maintainer review (no milestone yet). Part of the backlog triage in #4875.