diff --git a/src/filesystem/README.md b/src/filesystem/README.md index d1621d1e..793ba159 100644 --- a/src/filesystem/README.md +++ b/src/filesystem/README.md @@ -9,8 +9,50 @@ Node.js server implementing Model Context Protocol (MCP) for filesystem operatio - Move files/directories - Search files - Get file metadata +- Dynamic directory access control via MCP roots protocol -**Note**: The server will only allow operations within directories specified via `args`. +## Directory Access Control + +The server uses a flexible directory access control system. Directories can be specified via command-line arguments or dynamically via the MCP roots protocol. + +### Method 1: Command-line Arguments +Specify allowed directories when starting the server: +```bash +mcp-server-filesystem /path/to/dir1 /path/to/dir2 +``` + +### Method 2: MCP Roots Protocol +MCP clients that support the roots protocol can dynamically provide allowed directories. Client roots completely replace any command-line directories when provided. + +### How It Works + +The server's directory access control follows this flow: + +1. **Server Startup** + - Server starts with directories from command-line arguments (if provided) + - If no arguments provided, server starts with empty allowed directories + +2. **Client Connection & Initialization** + - Client connects and sends `initialize` request with capabilities + - Server checks if client supports roots protocol (`capabilities.roots`) + +3. **Roots Protocol Handling** (if client supports roots) + - **On initialization**: Server requests roots from client via `roots/list` + - Client responds with its configured roots + - Server replaces ALL allowed directories with client's roots + - **On runtime updates**: Client can send `notifications/roots/list_changed` + - Server requests updated roots and replaces allowed directories again + +4. **Fallback Behavior** (if client doesn't support roots) + - Server continues using command-line directories only + - No dynamic updates possible + +5. **Access Control** + - All filesystem operations are restricted to allowed directories + - Use `list_allowed_directories` tool to see current directories + - Server requires at least ONE allowed directory to operate + +**Important**: If server starts without command-line arguments AND client doesn't support roots protocol (or provides empty roots), the server will throw an error during initialization. ## API diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index fc32477b..3886d40b 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -6,6 +6,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema, + RootsListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js"; import fs from "fs/promises"; import path from "path"; @@ -20,8 +21,11 @@ import { isPathWithinAllowedDirectories } from './path-validation.js'; // Command line argument parsing const args = process.argv.slice(2); if (args.length === 0) { - console.error("Usage: mcp-server-filesystem [additional-directories...]"); - process.exit(1); + console.error("Usage: mcp-server-filesystem [allowed-directory] [additional-directories...]"); + console.error("Note: Allowed directories can be provided via:"); + console.error(" 1. Command-line arguments (shown above)"); + console.error(" 2. MCP roots protocol (if client supports it)"); + console.error("At least one directory must be provided by EITHER method for the server to operate."); } // Normalize all paths consistently @@ -573,8 +577,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { { name: "list_allowed_directories", description: - "Returns the list of directories that this server is allowed to access. " + - "Use this to understand which directories are available before trying to access files.", + "Returns the list of root directories that this server is allowed to access. " + + "Use this to understand which directories are available before trying to access files. ", inputSchema: { type: "object", properties: {}, @@ -890,12 +894,75 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { } }); +// Replaces any existing allowed directories based on roots provided by the MCP client. +async function updateAllowedDirectoriesFromRoots(roots: Array<{ uri: string; name?: string }>) { + const rootDirs: string[] = []; + for (const root of roots) { + let dir: string; + // Handle both file:// URIs (MCP standard) and plain directory paths (for flexibility) + dir = normalizePath(path.resolve(root.uri.startsWith('file://')? root.uri.slice(7) : root.uri)); + try { + const stats = await fs.stat(dir); + if (stats.isDirectory()) { + rootDirs.push(dir); + }else { + console.error(`Skipping non-directory root: ${dir}`); + } + } catch (error) { + // Skip invalid directories + console.error(`Skipping invalid directory: ${dir} due to error:`, error instanceof Error ? error.message : String(error)); + } + } + if(rootDirs.length > 0) { + allowedDirectories.splice(0, allowedDirectories.length, ...rootDirs); + } +} + +// Handles dynamic roots updates during runtime, when client sends "roots/list_changed" notification, server fetches the updated roots and replaces all allowed directories with the new roots. +server.setNotificationHandler(RootsListChangedNotificationSchema, async () => { + try { + // Request the updated roots list from the client + const response = await server.listRoots(); + if (response && 'roots' in response) { + await updateAllowedDirectoriesFromRoots(response.roots); + } + } catch (error) { + console.error("Failed to request roots from client:", error instanceof Error ? error.message : String(error)); + } +}); + +// Handles post-initialization setup, specifically checking for and fetching MCP roots. +server.oninitialized = async () => { + const clientCapabilities = server.getClientCapabilities(); + + if (clientCapabilities?.roots) { + try { + const response = await server.listRoots(); + if (response && 'roots' in response) { + await updateAllowedDirectoriesFromRoots(response.roots); + } else { + console.error("Client returned no roots set, keeping current settings"); + } + } catch (error) { + console.error("Failed to request initial roots from client:", error instanceof Error ? error.message : String(error)); + } + } else { + if (allowedDirectories.length > 0) { + console.error("Client does not support MCP Roots, using allowed directories set from server args:", allowedDirectories); + }else{ + throw new Error(`Server cannot operate: No allowed directories available. Server was started without command-line directories and client does not support MCP roots protocol. Please either: 1) Start server with directory arguments, or 2) Use a client that supports MCP roots protocol.`); + } + } +}; + // Start server async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Secure MCP Filesystem Server running on stdio"); - console.error("Allowed directories:", allowedDirectories); + if (allowedDirectories.length === 0) { + console.error("Started without allowed directories - waiting for client to provide roots via MCP protocol"); + } } runServer().catch((error) => {