roots-utils.ts
1 import { promises as fs, type Stats } from 'fs'; 2 import path from 'path'; 3 import os from 'os'; 4 import { normalizePath } from './path-utils.js'; 5 import type { Root } from '@modelcontextprotocol/sdk/types.js'; 6 7 /** 8 * Converts a root URI to a normalized directory path with basic security validation. 9 * @param rootUri - File URI (file://...) or plain directory path 10 * @returns Promise resolving to validated path or null if invalid 11 */ 12 async function parseRootUri(rootUri: string): Promise<string | null> { 13 try { 14 const rawPath = rootUri.startsWith('file://') ? rootUri.slice(7) : rootUri; 15 const expandedPath = rawPath.startsWith('~/') || rawPath === '~' 16 ? path.join(os.homedir(), rawPath.slice(1)) 17 : rawPath; 18 const absolutePath = path.resolve(expandedPath); 19 const resolvedPath = await fs.realpath(absolutePath); 20 return normalizePath(resolvedPath); 21 } catch { 22 return null; // Path doesn't exist or other error 23 } 24 } 25 26 /** 27 * Formats error message for directory validation failures. 28 * @param dir - Directory path that failed validation 29 * @param error - Error that occurred during validation 30 * @param reason - Specific reason for failure 31 * @returns Formatted error message 32 */ 33 function formatDirectoryError(dir: string, error?: unknown, reason?: string): string { 34 if (reason) { 35 return `Skipping ${reason}: ${dir}`; 36 } 37 const message = error instanceof Error ? error.message : String(error); 38 return `Skipping invalid directory: ${dir} due to error: ${message}`; 39 } 40 41 /** 42 * Resolves requested root directories from MCP root specifications. 43 * 44 * Converts root URI specifications (file:// URIs or plain paths) into normalized 45 * directory paths, validating that each path exists and is a directory. 46 * Includes symlink resolution for security. 47 * 48 * @param requestedRoots - Array of root specifications with URI and optional name 49 * @returns Promise resolving to array of validated directory paths 50 */ 51 export async function getValidRootDirectories( 52 requestedRoots: readonly Root[] 53 ): Promise<string[]> { 54 const validatedDirectories: string[] = []; 55 56 for (const requestedRoot of requestedRoots) { 57 const resolvedPath = await parseRootUri(requestedRoot.uri); 58 if (!resolvedPath) { 59 console.error(formatDirectoryError(requestedRoot.uri, undefined, 'invalid path or inaccessible')); 60 continue; 61 } 62 63 try { 64 const stats: Stats = await fs.stat(resolvedPath); 65 if (stats.isDirectory()) { 66 validatedDirectories.push(resolvedPath); 67 } else { 68 console.error(formatDirectoryError(resolvedPath, undefined, 'non-directory root')); 69 } 70 } catch (error) { 71 console.error(formatDirectoryError(resolvedPath, error)); 72 } 73 } 74 75 return validatedDirectories; 76 }