diff --git a/src/filesystem/__tests__/roots-utils.test.ts b/src/filesystem/__tests__/roots-utils.test.ts index 1a39483953..15511f557c 100644 --- a/src/filesystem/__tests__/roots-utils.test.ts +++ b/src/filesystem/__tests__/roots-utils.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { getValidRootDirectories } from '../roots-utils.js'; -import { mkdtempSync, rmSync, mkdirSync, writeFileSync, realpathSync } from 'fs'; +import { mkdtempSync, rmSync, mkdirSync, writeFileSync, realpathSync, symlinkSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; import type { Root } from '@modelcontextprotocol/sdk/types.js'; @@ -58,6 +58,26 @@ describe('getValidRootDirectories', () => { expect(result).toHaveLength(1); expect(result[0]).toBe(subDir); }); + + it('should preserve original and resolved root directory forms', async () => { + const actualDir = join(testDir1, 'actual-root'); + const aliasDir = join(testDir1, 'alias-root'); + mkdirSync(actualDir); + + try { + symlinkSync(actualDir, aliasDir, process.platform === 'win32' ? 'junction' : 'dir'); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'EPERM') { + return; + } + throw error; + } + + const result = await getValidRootDirectories([{ uri: aliasDir, name: 'Alias root' }]); + + expect(result).toContain(aliasDir); + expect(result).toContain(realpathSync(aliasDir)); + }); }); describe('error handling', () => { @@ -81,4 +101,4 @@ describe('getValidRootDirectories', () => { expect(result).toHaveLength(1); }); }); -}); \ No newline at end of file +}); diff --git a/src/filesystem/roots-utils.ts b/src/filesystem/roots-utils.ts index 5e26bb246b..9d400b5bff 100644 --- a/src/filesystem/roots-utils.ts +++ b/src/filesystem/roots-utils.ts @@ -8,17 +8,21 @@ import { fileURLToPath } from "url"; /** * 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 + * @returns Promise resolving to original and resolved paths, or null if invalid */ -async function parseRootUri(rootUri: string): Promise { +async function parseRootUri(rootUri: string): Promise { try { const rawPath = rootUri.startsWith('file://') ? fileURLToPath(rootUri) : rootUri; const expandedPath = rawPath.startsWith('~/') || rawPath === '~' ? path.join(os.homedir(), rawPath.slice(1)) : rawPath; const absolutePath = path.resolve(expandedPath); + const normalizedOriginal = normalizePath(absolutePath); const resolvedPath = await fs.realpath(absolutePath); - return normalizePath(resolvedPath); + const normalizedResolved = normalizePath(resolvedPath); + return normalizedOriginal === normalizedResolved + ? [normalizedResolved] + : [normalizedOriginal, normalizedResolved]; } catch { return null; // Path doesn't exist or other error } @@ -53,25 +57,33 @@ export async function getValidRootDirectories( requestedRoots: readonly Root[] ): Promise { const validatedDirectories: string[] = []; + const seenDirectories = new Set(); for (const requestedRoot of requestedRoots) { - const resolvedPath = await parseRootUri(requestedRoot.uri); - if (!resolvedPath) { + const rootPaths = await parseRootUri(requestedRoot.uri); + if (!rootPaths) { console.error(formatDirectoryError(requestedRoot.uri, undefined, 'invalid path or inaccessible')); continue; } - try { - const stats: Stats = await fs.stat(resolvedPath); - if (stats.isDirectory()) { - validatedDirectories.push(resolvedPath); - } else { - console.error(formatDirectoryError(resolvedPath, undefined, 'non-directory root')); + for (const rootPath of rootPaths) { + if (seenDirectories.has(rootPath)) { + continue; + } + + try { + const stats: Stats = await fs.stat(rootPath); + if (stats.isDirectory()) { + validatedDirectories.push(rootPath); + seenDirectories.add(rootPath); + } else { + console.error(formatDirectoryError(rootPath, undefined, 'non-directory root')); + } + } catch (error) { + console.error(formatDirectoryError(rootPath, error)); } - } catch (error) { - console.error(formatDirectoryError(resolvedPath, error)); } } return validatedDirectories; -} \ No newline at end of file +}