11import { promises as fs , type Stats } from 'fs' ;
22import path from 'path' ;
3+ import os from 'os' ;
34import { normalizePath } from './path-utils.js' ;
45import type { Root } from '@modelcontextprotocol/sdk/types.js' ;
56
67/**
7- * Converts a root URI to a normalized directory path.
8- * @param uri - File URI (file://...) or plain directory path
9- * @returns Normalized absolute directory path
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
1011 */
11- function parseRootUri ( uri : string ) : string {
12- const rawPath = uri . startsWith ( 'file://' ) ? uri . slice ( 7 ) : uri ;
13- return normalizePath ( path . resolve ( rawPath ) ) ;
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+ }
1424}
1525
1626/**
@@ -29,33 +39,38 @@ function formatDirectoryError(dir: string, error?: unknown, reason?: string): st
2939}
3040
3141/**
32- * Gets valid directory paths from MCP root specifications.
42+ * Resolves requested root directories from MCP root specifications.
3343 *
3444 * Converts root URI specifications (file:// URIs or plain paths) into normalized
3545 * directory paths, validating that each path exists and is a directory.
46+ * Includes symlink resolution for security.
3647 *
37- * @param roots - Array of root specifications with URI and optional name
48+ * @param requestedRoots - Array of root specifications with URI and optional name
3849 * @returns Promise resolving to array of validated directory paths
3950 */
4051export async function getValidRootDirectories (
41- roots : readonly Root [ ]
52+ requestedRoots : readonly Root [ ]
4253) : Promise < string [ ] > {
43- const validDirectories : string [ ] = [ ] ;
54+ const validatedDirectories : string [ ] = [ ] ;
4455
45- for ( const root of roots ) {
46- const dir = parseRootUri ( root . uri ) ;
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+ }
4762
4863 try {
49- const stats : Stats = await fs . stat ( dir ) ;
64+ const stats : Stats = await fs . stat ( resolvedPath ) ;
5065 if ( stats . isDirectory ( ) ) {
51- validDirectories . push ( dir ) ;
66+ validatedDirectories . push ( resolvedPath ) ;
5267 } else {
53- console . error ( formatDirectoryError ( dir , undefined , 'non-directory root' ) ) ;
68+ console . error ( formatDirectoryError ( resolvedPath , undefined , 'non-directory root' ) ) ;
5469 }
5570 } catch ( error ) {
56- console . error ( formatDirectoryError ( dir , error ) ) ;
71+ console . error ( formatDirectoryError ( resolvedPath , error ) ) ;
5772 }
5873 }
5974
60- return validDirectories ;
75+ return validatedDirectories ;
6176}
0 commit comments