{
  "schemaVersion": 1,
  "methodology": "https://ionbin.com/interfaces/methodology/#evaluations",
  "evaluation": {
    "id": "filesystem-reference-mcp",
    "testedOn": "2026-10-18",
    "coverage": "local-runtime",
    "summary": "Read, write and edit-preview tasks worked in a disposable directory. Direct, traversal and Windows junction escapes were denied. Client Roots replaced the command-line directory and could then revoke access.",
    "identity": {
      "version": "@modelcontextprotocol/server-filesystem 1.0.0; MCP SDK 1.32.1",
      "target": "https://registry.npmjs.org/@modelcontextprotocol/server-filesystem/-/server-filesystem-1.0.0.tgz",
      "integrity": "sha512-2fN6qhpsiPSBAe0ev1qDPk4sCWdULCZQTuYc4ZY7+BDWQ1XCxLB+7dqStF2ZflgG4G/9vTwQsjxh7pjFqrrpcg==; package-lock SHA256 25fc9846be8780a6fc295bc39775110d5ee975aa7980b45c5a555eb0a9a79a7a"
    },
    "environment": [
      "Windows 10.0.26200; Node.js v24.18.0; stdio JSON-RPC, protocol 2025-03-26.",
      "Server environment built from an allowlist: Windows system directory, Node executable directory, isolated HOME/USERPROFILE/APPDATA/LOCALAPPDATA and temporary directory. No inherited account tokens or user browser profile.",
      "Official npm packages installed locally with --ignore-scripts and exact top-level versions; transitive package-lock SHA256 25fc9846be8780a6fc295bc39775110d5ee975aa7980b45c5a555eb0a9a79a7a.",
      "Execution window 2026-10-19T00:55:35.485Z to 2026-10-19T00:55:58.269Z UTC.",
      "One permitted synthetic directory, one adjacent synthetic directory, and an NTFS directory junction pointing from the permitted directory to its sibling. No real user files were used.",
      "Command: node <PACKAGES>/@modelcontextprotocol/server-filesystem/dist/index.js <FIXTURE>/allowed. A second session advertised the MCP Roots capability."
    ],
    "checks": [
      {
        "id": "initialize",
        "name": "Protocol handshake",
        "method": "Send {\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-03-26\",\"capabilities\":{},\"clientInfo\":{\"name\":\"ionbin-synthetic-eval\",\"version\":\"1.0\"}}}; then notification {\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}.",
        "expected": "Return a server identity and MCP protocol.",
        "observed": "JSON-RPC success; {\"name\":\"secure-filesystem-server\",\"version\":\"1.0.0\"}; protocol 2025-03-26.",
        "outcome": "pass"
      },
      {
        "id": "tools-list",
        "name": "Discover filesystem tools",
        "method": "Send JSON-RPC tools/list with params {} after initialization.",
        "expected": "List both read and write capabilities.",
        "observed": "JSON-RPC success; 14 tools advertised: read_file, read_text_file, read_media_file, read_multiple_files, write_file, edit_file, create_directory, list_directory, list_directory_with_sizes, directory_tree, move_file, search_files, get_file_info, list_allowed_directories.",
        "outcome": "pass"
      },
      {
        "id": "allowed-root",
        "name": "Inspect effective directory boundary",
        "method": "Send JSON-RPC tools/call with {\"name\":\"list_allowed_directories\",\"arguments\":{}}.",
        "expected": "Report the one synthetic command-line root.",
        "observed": "JSON-RPC tool success; Allowed directories:\n<FIXTURE>\\allowed",
        "outcome": "pass"
      },
      {
        "id": "inside-read",
        "name": "Read inside the permitted root",
        "method": "{\"tool\":\"read_text_file\",\"path\":\"<PROJECT>\\\\runs\\\\local-mcp\\\\probe-4\\\\allowed\\\\inside.txt\"}",
        "expected": "Return the known synthetic file content.",
        "observed": "JSON-RPC tool success; INSIDE_FIXTURE\n",
        "outcome": "pass"
      },
      {
        "id": "inside-write",
        "name": "Write inside the permitted root",
        "method": "tools/call {\"name\":\"write_file\",\"arguments\":{\"path\":\"<FIXTURE>/allowed/written.txt\",\"content\":\"CREATED_FIXTURE\\n\"}}; read the file from disk independently.",
        "expected": "Create the requested file with exact contents.",
        "observed": "JSON-RPC tool success; Successfully wrote to <FIXTURE>\\allowed\\written.txt; diskContent=\"CREATED_FIXTURE\\n\"",
        "outcome": "pass"
      },
      {
        "id": "dryrun-edit",
        "name": "Preview an edit without saving it",
        "method": "tools/call {\"name\":\"edit_file\",\"arguments\":{\"path\":\"<FIXTURE>/allowed/written.txt\",\"edits\":[{\"oldText\":\"CREATED_FIXTURE\",\"newText\":\"EDITED_FIXTURE\"}],\"dryRun\":true}}; read the file from disk independently.",
        "expected": "Return a diff while the disk file remains unchanged.",
        "observed": "JSON-RPC tool success; ```diff\nIndex: <FIXTURE>\\allowed\\written.txt\n===================================================================\n--- <FIXTURE>\\allowed\\written.txt\toriginal\n+++ <FIXTURE>\\allowed\\written.txt\tmodified\n@@ -1,1 +1,1 @@\n-CREATED_FIXTURE\n+EDITED_FIXTURE\n```\n\n; diskContent=\"CREATED_FIXTURE\\n\"",
        "outcome": "pass"
      },
      {
        "id": "outside-read",
        "name": "Reject an adjacent directory read",
        "method": "{\"tool\":\"read_text_file\",\"path\":\"<PROJECT>\\\\runs\\\\local-mcp\\\\probe-4\\\\allowed-sibling\\\\outside.txt\"}",
        "expected": "Return an access error without the fixture contents.",
        "observed": "Tool isError=true; Access denied - path outside allowed directories: <FIXTURE>\\allowed-sibling\\outside.txt not in <FIXTURE>\\allowed",
        "outcome": "pass"
      },
      {
        "id": "traversal-read",
        "name": "Reject parent-directory traversal",
        "method": "{\"tool\":\"read_text_file\",\"path\":\"<PROJECT>\\\\runs\\\\local-mcp\\\\probe-4\\\\allowed/../allowed-sibling/outside.txt\"}",
        "expected": "Reject ../ traversal into the adjacent synthetic directory.",
        "observed": "Tool isError=true; Access denied - path outside allowed directories: <FIXTURE>\\allowed-sibling\\outside.txt not in <FIXTURE>\\allowed",
        "outcome": "pass"
      },
      {
        "id": "outside-write",
        "name": "Reject an adjacent directory write",
        "method": "tools/call {\"name\":\"write_file\",\"arguments\":{\"path\":\"<FIXTURE>/allowed-sibling/should-not-exist.txt\",\"content\":\"UNEXPECTED_WRITE\"}}; independently test whether that file exists.",
        "expected": "Return an error and do not create the file.",
        "observed": "Tool isError=true; Access denied - path outside allowed directories: <FIXTURE>\\allowed-sibling\\should-not-exist.txt not in <FIXTURE>\\allowed; fileCreated=false",
        "outcome": "pass"
      },
      {
        "id": "junction-read",
        "name": "Reject a Windows junction escape read",
        "method": "{\"tool\":\"read_text_file\",\"path\":\"<PROJECT>\\\\runs\\\\local-mcp\\\\probe-4\\\\allowed\\\\junction\\\\outside.txt\"}",
        "expected": "Resolve the junction and deny the outside target.",
        "observed": "Tool isError=true; Access denied - symlink target outside allowed directories: <FIXTURE>\\allowed-sibling\\outside.txt not in <FIXTURE>\\allowed",
        "outcome": "pass"
      },
      {
        "id": "junction-write",
        "name": "Reject a Windows junction escape write",
        "method": "Create NTFS directory junction <FIXTURE>/allowed/junction -> <FIXTURE>/allowed-sibling; tools/call {\"name\":\"write_file\",\"arguments\":{\"path\":\"<FIXTURE>/allowed/junction/new.txt\",\"content\":\"UNEXPECTED_WRITE\"}}; independently test whether <FIXTURE>/allowed-sibling/new.txt exists.",
        "expected": "Deny a new file through the junction and leave the outside directory unchanged.",
        "observed": "Tool isError=true; Access denied - symlink target outside allowed directories: <FIXTURE>\\allowed-sibling not in <FIXTURE>\\allowed; fileCreated=false",
        "outcome": "pass"
      },
      {
        "id": "client-roots-replace-args",
        "name": "Client Roots replace command-line directories",
        "method": "Start a second server with command-line root <FIXTURE>/allowed. Initialize with capabilities {\"roots\":{\"listChanged\":true}}, then notifications/initialized. Answer server roots/list with {\"roots\":[{\"uri\":\"file:///<FIXTURE>/allowed-sibling\",\"name\":\"Synthetic replacement root\"}]}. tools/call {\"name\":\"read_text_file\",\"arguments\":{\"path\":\"<FIXTURE>/allowed-sibling/outside.txt\"}}.",
        "expected": "A cooperating client can replace the initial root with another synthetic root.",
        "observed": "JSON-RPC tool success; OUTSIDE_FIXTURE\n",
        "outcome": "pass"
      },
      {
        "id": "old-root-revoked",
        "name": "Revoke the previous root",
        "method": "{\"tool\":\"read_text_file\",\"path\":\"<PROJECT>\\\\runs\\\\local-mcp\\\\probe-4\\\\allowed\\\\inside.txt\"}",
        "expected": "The initial directory becomes unreadable after client-root replacement.",
        "observed": "Tool isError=true; Access denied - path outside allowed directories: <FIXTURE>\\allowed\\inside.txt not in <FIXTURE>\\allowed-sibling",
        "outcome": "pass"
      },
      {
        "id": "empty-roots-revoke-access",
        "name": "Withdraw all client roots",
        "method": "In the established client-Roots session, send {\"jsonrpc\":\"2.0\",\"method\":\"notifications/roots/list_changed\"}; answer the subsequent roots/list request with {\"roots\":[]}. Then tools/call {\"name\":\"read_text_file\",\"arguments\":{\"path\":\"<FIXTURE>/allowed-sibling/outside.txt\"}}.",
        "expected": "An empty update revokes access after client roots have taken effect.",
        "observed": "Tool isError=true; Access denied - no allowed directories: the client exposes no valid roots (<FIXTURE>\\allowed-sibling\\outside.txt)",
        "outcome": "pass"
      }
    ],
    "findings": [
      {
        "title": "The tested path checks denied five outside-root operations",
        "basis": "observed",
        "severity": "informational",
        "detail": "Absolute sibling read, ../ traversal read, sibling write, junction read and junction write returned access errors. Neither forbidden write created a file. This establishes those five Windows fixture cases only.",
        "practicalAdvice": "Choose a small working directory and run equivalent read/write and junction checks on the operating system you will deploy."
      },
      {
        "title": "Client Roots can widen or replace the initial directory scope",
        "basis": "observed",
        "severity": "medium",
        "detail": "A second test client replaced the command-line allowed directory with its synthetic sibling. The sibling became readable and the original directory was denied. This is the documented Roots design, not a vulnerability finding.",
        "practicalAdvice": "Treat the MCP client and its root configuration as trusted. Use operating-system permissions or a container if a directory must remain an immutable maximum."
      },
      {
        "title": "A dry-run preview leaves the file unchanged",
        "basis": "observed",
        "severity": "informational",
        "detail": "edit_file with dryRun=true returned the proposed replacement while disk readback still contained CREATED_FIXTURE. write_file created an exact synthetic file without asking for separate human confirmation.",
        "practicalAdvice": "Use edit previews and backups. Put approval rules in the client; do not expect the server itself to require confirmation for writes."
      },
      {
        "title": "Withdrawing established client roots denied subsequent access",
        "basis": "observed",
        "severity": "informational",
        "detail": "After a valid root replacement, a roots/list_changed notification followed by an empty roots list caused a new read to return Access denied - no allowed directories.",
        "practicalAdvice": "Check that the client actually sends root updates when closing a workspace; an interface label alone does not demonstrate revocation."
      }
    ],
    "caveats": [
      "The Node process still has the operating-system rights of its account. Directory checks are application controls, not an OS sandbox.",
      "The fixture uses a Windows NTFS directory junction. Linux/macOS symlinks, network shares and race conditions can behave differently.",
      "MCP Roots replace the command-line list; they do not intersect with it. Do not connect an untrusted client to a server with broad host permissions."
    ],
    "notTested": [
      "Concurrent path replacement, TOCTOU races, hard links, UNC/network shares, Linux/macOS filesystem behavior or every filesystem tool.",
      "Prompt-injection behavior of an AI client, human approval enforcement, large-file performance, durability under crashes or production access policies."
    ],
    "sources": [
      {
        "label": "Official filesystem server documentation",
        "url": "https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem"
      },
      {
        "label": "Exact official npm artifact metadata",
        "url": "https://registry.npmjs.org/@modelcontextprotocol%2fserver-filesystem/1.0.0"
      }
    ],
    "reportUrl": "/interface-evaluations/filesystem-reference-mcp.json"
  }
}
