From b37da4000399d76a836592e18ad5faaa4ba39d62 Mon Sep 17 00:00:00 2001 From: Nandha Reddy Date: Wed, 4 Jun 2025 00:39:41 +1000 Subject: [PATCH 1/4] feat(filesystem): implement MCP roots protocol for dynamic directory management - Add support for dynamic directory updates via MCP roots protocol - Allow clients to override command-line directories at runtime - Maintain backwards compatibility with existing command-line args - Add comprehensive error handling for edge cases - Update documentation to explain both configuration methods Fixes #401 --- src/filesystem/README.md | 44 ++++++++++++++++++++++- src/filesystem/index.ts | 77 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 115 insertions(+), 6 deletions(-) diff --git a/src/filesystem/README.md b/src/filesystem/README.md index d1621d1ef3..793ba15970 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 fc32477b52..3886d40bad 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) => { From f8dd74576b06b12fecb0342d3a6679b23f75b1a8 Mon Sep 17 00:00:00 2001 From: Nandha Reddy Date: Wed, 25 Jun 2025 01:13:07 +1000 Subject: [PATCH 2/4] feat(filesystem): implement MCP roots protocol for dynamic directory management - Extract roots processing logic from index.ts into testable roots-utils.ts module and add Test suite - Update README to recommend MCP roots protocol for dynamic directory management --- src/filesystem/README.md | 16 +++- src/filesystem/__tests__/roots-utils.test.ts | 84 ++++++++++++++++++++ src/filesystem/index.ts | 34 +++----- src/filesystem/roots-utils.ts | 61 ++++++++++++++ 4 files changed, 167 insertions(+), 28 deletions(-) create mode 100644 src/filesystem/__tests__/roots-utils.test.ts create mode 100644 src/filesystem/roots-utils.ts diff --git a/src/filesystem/README.md b/src/filesystem/README.md index 793ba15970..292fac6dd9 100644 --- a/src/filesystem/README.md +++ b/src/filesystem/README.md @@ -16,13 +16,19 @@ Node.js server implementing Model Context Protocol (MCP) for filesystem operatio 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: +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. +### Method 2: MCP Roots Protocol (Recommended) +MCP clients that support the roots protocol can dynamically update the Allowed directories. + +Roots notified by Client to Server, completely replace any server-side Allowed directories when provided. + +**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. + +This is the recommended method, as MCP roots protocol for dynamic directory management. This enables runtime directory updates via `roots/list_changed` notifications without server restart, providing a more flexible and modern integration experience. ### How It Works @@ -52,7 +58,9 @@ The server's directory access control follows this flow: - 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. +**Note**: The server will only allow operations within directories specified either via `args` or via Roots. + + ## API diff --git a/src/filesystem/__tests__/roots-utils.test.ts b/src/filesystem/__tests__/roots-utils.test.ts new file mode 100644 index 0000000000..758219588c --- /dev/null +++ b/src/filesystem/__tests__/roots-utils.test.ts @@ -0,0 +1,84 @@ +import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; +import { getValidRootDirectories } from '../roots-utils.js'; +import { mkdtempSync, rmSync, mkdirSync, writeFileSync, realpathSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import type { Root } from '@modelcontextprotocol/sdk/types.js'; + +describe('getValidRootDirectories', () => { + let testDir1: string; + let testDir2: string; + let testDir3: string; + let testFile: string; + + beforeEach(() => { + // Create test directories + testDir1 = realpathSync(mkdtempSync(join(tmpdir(), 'mcp-roots-test1-'))); + testDir2 = realpathSync(mkdtempSync(join(tmpdir(), 'mcp-roots-test2-'))); + testDir3 = realpathSync(mkdtempSync(join(tmpdir(), 'mcp-roots-test3-'))); + + // Create a test file (not a directory) + testFile = join(testDir1, 'test-file.txt'); + writeFileSync(testFile, 'test content'); + }); + + afterEach(() => { + // Cleanup + rmSync(testDir1, { recursive: true, force: true }); + rmSync(testDir2, { recursive: true, force: true }); + rmSync(testDir3, { recursive: true, force: true }); + }); + + describe('valid directory processing', () => { + it('should process all URI formats and edge cases', async () => { + const roots = [ + { uri: `file://${testDir1}`, name: 'File URI' }, + { uri: testDir2, name: 'Plain path' }, + { uri: testDir3 } // Plain path without name property + ]; + + const result = await getValidRootDirectories(roots); + + expect(result).toContain(testDir1); + expect(result).toContain(testDir2); + expect(result).toContain(testDir3); + expect(result).toHaveLength(3); + }); + + it('should normalize complex paths', async () => { + const subDir = join(testDir1, 'subdir'); + mkdirSync(subDir); + + const roots = [ + { uri: `file://${testDir1}/./subdir/../subdir`, name: 'Complex Path' } + ]; + + const result = await getValidRootDirectories(roots); + + expect(result).toHaveLength(1); + expect(result[0]).toBe(subDir); + }); + }); + + describe('error handling', () => { + + it('should handle various error types', async () => { + const nonExistentDir = join(tmpdir(), 'non-existent-directory-12345'); + const invalidPath = '\0invalid\0path'; // Null bytes cause different error types + const roots = [ + { uri: `file://${testDir1}`, name: 'Valid Dir' }, + { uri: `file://${nonExistentDir}`, name: 'Non-existent Dir' }, + { uri: `file://${testFile}`, name: 'File Not Dir' }, + { uri: `file://${invalidPath}`, name: 'Invalid Path' } + ]; + + const result = await getValidRootDirectories(roots); + + expect(result).toContain(testDir1); + expect(result).not.toContain(nonExistentDir); + expect(result).not.toContain(testFile); + expect(result).not.toContain(invalidPath); + expect(result).toHaveLength(1); + }); + }); +}); \ No newline at end of file diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 3886d40bad..25be849737 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -7,6 +7,7 @@ import { ListToolsRequestSchema, ToolSchema, RootsListChangedNotificationSchema, + type Root, } from "@modelcontextprotocol/sdk/types.js"; import fs from "fs/promises"; import path from "path"; @@ -17,6 +18,7 @@ import { zodToJsonSchema } from "zod-to-json-schema"; import { diffLines, createTwoFilesPatch } from 'diff'; import { minimatch } from 'minimatch'; import { isPathWithinAllowedDirectories } from './path-validation.js'; +import { getValidRootDirectories } from './roots-utils.js'; // Command line argument parsing const args = process.argv.slice(2); @@ -894,26 +896,10 @@ 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) { +// Updates allowed directories based on MCP client roots +async function updateAllowedDirectoriesFromRoots(roots: Root[]) { + const rootDirs = await getValidRootDirectories(roots); + if (rootDirs.length > 0) { allowedDirectories.splice(0, allowedDirectories.length, ...rootDirs); } } @@ -941,16 +927,16 @@ server.oninitialized = async () => { if (response && 'roots' in response) { await updateAllowedDirectoriesFromRoots(response.roots); } else { - console.error("Client returned no roots set, keeping current settings"); + console.log("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); + console.log("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.`); + throw new Error(`Server cannot operate: No allowed directories available. Server was started without command-line directories and client either does not support MCP roots protocol or provided empty roots. Please either: 1) Start server with directory arguments, or 2) Use a client that supports MCP roots protocol and provides valid root directories.`); } } }; @@ -961,7 +947,7 @@ async function runServer() { await server.connect(transport); console.error("Secure MCP Filesystem Server running on stdio"); if (allowedDirectories.length === 0) { - console.error("Started without allowed directories - waiting for client to provide roots via MCP protocol"); + console.log("Started without allowed directories - waiting for client to provide roots via MCP protocol"); } } diff --git a/src/filesystem/roots-utils.ts b/src/filesystem/roots-utils.ts new file mode 100644 index 0000000000..ae23be9867 --- /dev/null +++ b/src/filesystem/roots-utils.ts @@ -0,0 +1,61 @@ +import { promises as fs, type Stats } from 'fs'; +import path from 'path'; +import { normalizePath } from './path-utils.js'; +import type { Root } from '@modelcontextprotocol/sdk/types.js'; + +/** + * Converts a root URI to a normalized directory path. + * @param uri - File URI (file://...) or plain directory path + * @returns Normalized absolute directory path + */ +function parseRootUri(uri: string): string { + const rawPath = uri.startsWith('file://') ? uri.slice(7) : uri; + return normalizePath(path.resolve(rawPath)); +} + +/** + * Formats error message for directory validation failures. + * @param dir - Directory path that failed validation + * @param error - Error that occurred during validation + * @param reason - Specific reason for failure + * @returns Formatted error message + */ +function formatDirectoryError(dir: string, error?: unknown, reason?: string): string { + if (reason) { + return `Skipping ${reason}: ${dir}`; + } + const message = error instanceof Error ? error.message : String(error); + return `Skipping invalid directory: ${dir} due to error: ${message}`; +} + +/** + * Gets valid directory paths from MCP root specifications. + * + * Converts root URI specifications (file:// URIs or plain paths) into normalized + * directory paths, validating that each path exists and is a directory. + * + * @param roots - Array of root specifications with URI and optional name + * @returns Promise resolving to array of validated directory paths + */ +export async function getValidRootDirectories( + roots: readonly Root[] +): Promise { + const validDirectories: string[] = []; + + for (const root of roots) { + const dir = parseRootUri(root.uri); + + try { + const stats: Stats = await fs.stat(dir); + if (stats.isDirectory()) { + validDirectories.push(dir); + } else { + console.error(formatDirectoryError(dir, undefined, 'non-directory root')); + } + } catch (error) { + console.error(formatDirectoryError(dir, error)); + } + } + + return validDirectories; +} \ No newline at end of file From f3891aaf69fed1cfc063f44558ea5a980d622ea3 Mon Sep 17 00:00:00 2001 From: Nandha Reddy Date: Thu, 26 Jun 2025 00:02:16 +1000 Subject: [PATCH 3/4] Apply suggestions from code review comments on docs and logging Co-authored-by: Ola Hungerford --- src/filesystem/README.md | 10 +++++----- src/filesystem/index.ts | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/filesystem/README.md b/src/filesystem/README.md index 292fac6dd9..cd6d0a9f06 100644 --- a/src/filesystem/README.md +++ b/src/filesystem/README.md @@ -9,11 +9,11 @@ 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 +- Dynamic directory access control via [Roots](https://modelcontextprotocol.io/docs/concepts/roots) ## 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. +The server uses a flexible directory access control system. Directories can be specified via command-line arguments or dynamically via [Roots](https://modelcontextprotocol.io/docs/concepts/roots). ### Method 1: Command-line Arguments Specify Allowed directories when starting the server: @@ -21,14 +21,14 @@ Specify Allowed directories when starting the server: mcp-server-filesystem /path/to/dir1 /path/to/dir2 ``` -### Method 2: MCP Roots Protocol (Recommended) -MCP clients that support the roots protocol can dynamically update the Allowed directories. +### Method 2: MCP Roots (Recommended) +MCP clients that support [Roots](https://modelcontextprotocol.io/docs/concepts/roots) can dynamically update the Allowed directories. Roots notified by Client to Server, completely replace any server-side Allowed directories when provided. **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. -This is the recommended method, as MCP roots protocol for dynamic directory management. This enables runtime directory updates via `roots/list_changed` notifications without server restart, providing a more flexible and modern integration experience. +This is the recommended method, as this enables runtime directory updates via `roots/list_changed` notifications without server restart, providing a more flexible and modern integration experience. ### How It Works diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 25be849737..8dfca07c00 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -927,14 +927,14 @@ server.oninitialized = async () => { if (response && 'roots' in response) { await updateAllowedDirectoriesFromRoots(response.roots); } else { - console.log("Client returned no roots set, keeping current settings"); + 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.log("Client does not support MCP Roots, using allowed directories set from server args:", allowedDirectories); + 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 either does not support MCP roots protocol or provided empty roots. Please either: 1) Start server with directory arguments, or 2) Use a client that supports MCP roots protocol and provides valid root directories.`); } @@ -947,7 +947,7 @@ async function runServer() { await server.connect(transport); console.error("Secure MCP Filesystem Server running on stdio"); if (allowedDirectories.length === 0) { - console.log("Started without allowed directories - waiting for client to provide roots via MCP protocol"); + console.error("Started without allowed directories - waiting for client to provide roots via MCP protocol"); } } From 2c922a93f9ce7c00f36d69140597656bd33e2a1a Mon Sep 17 00:00:00 2001 From: Nandha Reddy Date: Wed, 2 Jul 2025 13:35:34 +1000 Subject: [PATCH 4/4] feat(filesystem): add symlink resolution and home directory support to roots protocol - Add symlink resolution using fs.realpath() for security consistency - Support home directory expansion (~/) in root URI specifications - Improve error handling with null checks, detailed error messages, and informative logging - Change allowedDirectories from constant to variable to support roots protocol directory management --- src/filesystem/index.ts | 13 ++++++---- src/filesystem/roots-utils.ts | 49 +++++++++++++++++++++++------------ 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 8dfca07c00..524c9c2608 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -43,7 +43,7 @@ function expandHome(filepath: string): string { } // Store allowed directories in normalized and resolved form -const allowedDirectories = await Promise.all( +let allowedDirectories = await Promise.all( args.map(async (dir) => { const expanded = expandHome(dir); const absolute = path.resolve(expanded); @@ -897,10 +897,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }); // Updates allowed directories based on MCP client roots -async function updateAllowedDirectoriesFromRoots(roots: Root[]) { - const rootDirs = await getValidRootDirectories(roots); - if (rootDirs.length > 0) { - allowedDirectories.splice(0, allowedDirectories.length, ...rootDirs); +async function updateAllowedDirectoriesFromRoots(requestedRoots: Root[]) { + const validatedRootDirs = await getValidRootDirectories(requestedRoots); + if (validatedRootDirs.length > 0) { + allowedDirectories = [...validatedRootDirs]; + console.error(`Updated allowed directories from MCP roots: ${validatedRootDirs.length} valid directories`); + } else { + console.error("No valid root directories provided by client"); } } diff --git a/src/filesystem/roots-utils.ts b/src/filesystem/roots-utils.ts index ae23be9867..8732997757 100644 --- a/src/filesystem/roots-utils.ts +++ b/src/filesystem/roots-utils.ts @@ -1,16 +1,26 @@ import { promises as fs, type Stats } from 'fs'; import path from 'path'; +import os from 'os'; import { normalizePath } from './path-utils.js'; import type { Root } from '@modelcontextprotocol/sdk/types.js'; /** - * Converts a root URI to a normalized directory path. - * @param uri - File URI (file://...) or plain directory path - * @returns Normalized absolute directory path + * Converts a root URI to a normalized directory path with basic security validation. + * @param rootUri - File URI (file://...) or plain directory path + * @returns Promise resolving to validated path or null if invalid */ -function parseRootUri(uri: string): string { - const rawPath = uri.startsWith('file://') ? uri.slice(7) : uri; - return normalizePath(path.resolve(rawPath)); +async function parseRootUri(rootUri: string): Promise { + try { + const rawPath = rootUri.startsWith('file://') ? rootUri.slice(7) : rootUri; + const expandedPath = rawPath.startsWith('~/') || rawPath === '~' + ? path.join(os.homedir(), rawPath.slice(1)) + : rawPath; + const absolutePath = path.resolve(expandedPath); + const resolvedPath = await fs.realpath(absolutePath); + return normalizePath(resolvedPath); + } catch { + return null; // Path doesn't exist or other error + } } /** @@ -29,33 +39,38 @@ function formatDirectoryError(dir: string, error?: unknown, reason?: string): st } /** - * Gets valid directory paths from MCP root specifications. + * Resolves requested root directories from MCP root specifications. * * Converts root URI specifications (file:// URIs or plain paths) into normalized * directory paths, validating that each path exists and is a directory. + * Includes symlink resolution for security. * - * @param roots - Array of root specifications with URI and optional name + * @param requestedRoots - Array of root specifications with URI and optional name * @returns Promise resolving to array of validated directory paths */ export async function getValidRootDirectories( - roots: readonly Root[] + requestedRoots: readonly Root[] ): Promise { - const validDirectories: string[] = []; + const validatedDirectories: string[] = []; - for (const root of roots) { - const dir = parseRootUri(root.uri); + for (const requestedRoot of requestedRoots) { + const resolvedPath = await parseRootUri(requestedRoot.uri); + if (!resolvedPath) { + console.error(formatDirectoryError(requestedRoot.uri, undefined, 'invalid path or inaccessible')); + continue; + } try { - const stats: Stats = await fs.stat(dir); + const stats: Stats = await fs.stat(resolvedPath); if (stats.isDirectory()) { - validDirectories.push(dir); + validatedDirectories.push(resolvedPath); } else { - console.error(formatDirectoryError(dir, undefined, 'non-directory root')); + console.error(formatDirectoryError(resolvedPath, undefined, 'non-directory root')); } } catch (error) { - console.error(formatDirectoryError(dir, error)); + console.error(formatDirectoryError(resolvedPath, error)); } } - return validDirectories; + return validatedDirectories; } \ No newline at end of file