Skip to content

fix(filesystem): include error reason + enumerate failed dirs at startup (#4152)#4156

Open
daveCode-dot wants to merge 2 commits into
modelcontextprotocol:mainfrom
daveCode-dot:fix/4152-filesystem-actionable-startup-errors
Open

fix(filesystem): include error reason + enumerate failed dirs at startup (#4152)#4156
daveCode-dot wants to merge 2 commits into
modelcontextprotocol:mainfrom
daveCode-dot:fix/4152-filesystem-actionable-startup-errors

Conversation

@daveCode-dot
Copy link
Copy Markdown

Summary

Closes #4152. The secure-filesystem-server's startup validation already filters inaccessible allowed_directories entries, but the diagnostic surface is too thin: per-directory warnings omit the underlying OS error and the aggregate fatal error doesn't enumerate which paths failed. Hosts that surface only the final stderr line (e.g. Claude Desktop in the original report) end up showing users an opaque "Server disconnected" toast with nothing to act on.

This change keeps the existing log strings stable for grep-based consumers and adds the missing reason / enumeration so the diagnostic is actionable.

Repro

cd src/filesystem && npm run build

# 1) Single missing directory: silent-ish exit (no reason, no enumeration)
node dist/index.js /nonexistent/foo
# stderr (before): Warning: Cannot access directory /nonexistent/foo, skipping
#                  Error: None of the specified directories are accessible
# exit code 1, but a host showing only the final line cannot tell the user
# *which* directory was bad or *why*.

# 2) Mixed valid/invalid: only the warning line names the path, no reason
node dist/index.js /nonexistent/bar /tmp
# stderr (before): Warning: Cannot access directory /nonexistent/bar, skipping

Fix

src/filesystem/index.ts — startup validation now tracks failed entries with their reason and:

  • Appends the underlying error message to the per-directory warning, e.g. Cannot access directory /missing, skipping (ENOENT: no such file or directory, stat '/missing').
  • Enumerates every failed entry inside the aggregate fatal error so a host capturing only the final stderr line gets the full list:
Error: None of the specified directories are accessible:
  - /nonexistent/foo (ENOENT: no such file or directory, stat '/nonexistent/foo')
  - /tmp/secret.txt (not a directory)

The four pre-existing startup-validation.test.ts cases continue to pass unchanged — the legacy Warning: Cannot access directory and Error: None of the specified directories are accessible substrings are preserved.

Tests

src/filesystem/__tests__/startup-validation.test.ts adds two regression cases pinned to #4152:

  • should include the underlying error reason in the per-directory warning (#4152) — asserts /ENOENT|no such file or directory/i shows up in stderr alongside the path.
  • should list every offending directory inside the aggregate startup error (#4152) — slices stderr from the Error: marker and verifies both failing paths appear in the aggregate, not only as scattered earlier warnings.

Local result, ran from src/filesystem:

npx vitest run
Test Files  7 passed (7)
     Tests  148 passed (148)

No regression in the other six test files (directory-tree, lib, path-utils, path-validation, roots-utils, structured-content).

Adjacent gaps (out of scope · raising for maintainer decision)

While auditing this code path I noticed roots-utils.ts already exposes a formatDirectoryError(dir, error?, reason?) helper used by getValidRootDirectories(). There are now two error-formatting styles for the same domain:

  • index.ts startup: Cannot access directory <dir>, skipping (<reason>) and - <dir> (<reason>) enumeration
  • roots-utils.ts MCP roots: Skipping invalid directory: <dir> due to error: <message>

Standardising on a single format (either by exporting formatDirectoryError and adopting it in index.ts, or vice-versa) would remove this drift, but it changes user-facing log strings and the right shape is a maintainer call rather than a unilateral choice in this PR. Happy to follow up with a separate PR once you indicate which way you'd like to consolidate.

Notes

  • No package.json / version bump — leaving release cadence to the maintainers.
  • No new dependencies.
  • The failedEntries list is built only during startup so there is no runtime cost on the hot path.

🤖 Co-authored with Claude Code

David and others added 2 commits May 14, 2026 04:05
…tup (modelcontextprotocol#4152)

When the secure-filesystem-server starts, `allowed_directories` entries
that fail `fs.stat` are skipped with only `Warning: Cannot access
directory <dir>, skipping`. When **every** entry fails the server exits
with `Error: None of the specified directories are accessible`. Neither
line tells the host which underlying error happened (ENOENT, EACCES, …)
nor — in the aggregate fatal case — which entries were the culprits, so
hosts that surface only the final stderr line (Claude Desktop is the
example in the issue) show users an opaque "Server disconnected" toast.

This change keeps the existing wording (so dependent tests / log-grep
consumers stay valid) and:

* Adds the underlying `error.message` to the per-directory warning so
  e.g. `Cannot access directory /missing, skipping (ENOENT: …)` is
  immediately actionable.
* Tracks failed entries in a `failedEntries` list and enumerates them
  inside the aggregate fatal error, so a host capturing only that final
  line sees every offending path with its reason.

Two regression tests in `startup-validation.test.ts` pin the new
behaviour to issue modelcontextprotocol#4152 (per-directory reason + aggregate enumeration);
the four pre-existing tests continue to pass unchanged.

Co-authored-by: Claude Code <noreply@anthropic.com>
…ntextprotocol#4152

Adds a regression case alongside the ENOENT one so both common host
failure modes (missing dir, locked dir) are pinned. Skipped on Windows
(chmod has no effect on dir access there) and when running as root
(0o000 doesn't restrict root reads).

Co-authored-by: Claude Code <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

filesystem: server exits silently when an allowed_directories entry does not exist on disk

1 participant