@modelcontextprotocol/server-filesystem resolves allowed-dir symlinks but not input paths — silent false denials on macOS
Surface: @modelcontextprotocol/server-filesystem (secure-filesystem-server) via npx -y @modelcontextprotocol/server-filesystem <allowed-dir>
Bug: The server resolves the allowed directory argument to its canonical (realpath) form on startup — e.g. /tmp/foo becomes /private/tmp/foo on macOS. But input paths in tools/call requests are NOT resolved the same way. The sandbox check compares the raw input path against the canonicalized allowed dir, so any input using the symlink form (/tmp/...) is always denied, even for files genuinely inside the sandbox.
Repro (2×2 matrix, 4 sessions):
SERVER ARG INPUT PATH RESULT
/tmp/…/sandbox /tmp/…/sandbox/test.txt → DENIED
/tmp/…/sandbox /private/tmp/…/sandbox/test.txt → OK
/private/tmp/…/sandbox /tmp/…/sandbox/test.txt → DENIED
/private/tmp/…/sandbox /private/tmp/…/sandbox/test.txt → OKThe result depends ONLY on the input path form — not the server arg form. Both server arg forms get canonicalized to /private/tmp/... internally. Input paths are never canonicalized.
Error message returned:
Access denied - path outside allowed directories:
/tmp/crucible-fs-test/sandbox/test.txt not in /private/tmp/crucible-fs-test/sandboxThe error itself reveals the asymmetry: it shows the raw input path on the left and the canonicalized allowed dir on the right.
Impact for LLM agents: An agent composing file paths on macOS will naturally use /tmp/... (what mktemp returns, what the user types). Every such path silently fails the sandbox check, producing a confusing "access denied" that looks like a permission problem rather than a symlink normalization bug. The agent has no way to know it needs to call realpath first unless it's been told about this specific macOS behavior.
Security note: The sandbox is otherwise solid — path traversal (../../), absolute escapes (/etc/hosts), symlink-target escapes, move_file across boundary, and create_directory outside boundary are all correctly rejected. The symlink check explicitly resolves targets: "Access denied - symlink target outside allowed directories". The bug is strictly a false-denial (denying valid access), not a false-allow.
Tested with: @modelcontextprotocol/server-filesystem via npx, Node v22.22.3, macOS Darwin 25.4.0.