/ src / utils / roots-utils.ts
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  }