Skip to content

Commit 00fed76

Browse files
committed
Preserve original root paths after realpath
1 parent 64b1cb0 commit 00fed76

2 files changed

Lines changed: 48 additions & 16 deletions

File tree

src/filesystem/__tests__/roots-utils.test.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
22
import { getValidRootDirectories } from '../roots-utils.js';
3-
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, realpathSync } from 'fs';
3+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, realpathSync, symlinkSync } from 'fs';
44
import { tmpdir } from 'os';
55
import { join } from 'path';
66
import type { Root } from '@modelcontextprotocol/sdk/types.js';
@@ -58,6 +58,26 @@ describe('getValidRootDirectories', () => {
5858
expect(result).toHaveLength(1);
5959
expect(result[0]).toBe(subDir);
6060
});
61+
62+
it('should preserve original and resolved root directory forms', async () => {
63+
const actualDir = join(testDir1, 'actual-root');
64+
const aliasDir = join(testDir1, 'alias-root');
65+
mkdirSync(actualDir);
66+
67+
try {
68+
symlinkSync(actualDir, aliasDir, process.platform === 'win32' ? 'junction' : 'dir');
69+
} catch (error) {
70+
if ((error as NodeJS.ErrnoException).code === 'EPERM') {
71+
return;
72+
}
73+
throw error;
74+
}
75+
76+
const result = await getValidRootDirectories([{ uri: aliasDir, name: 'Alias root' }]);
77+
78+
expect(result).toContain(aliasDir);
79+
expect(result).toContain(realpathSync(aliasDir));
80+
});
6181
});
6282

6383
describe('error handling', () => {
@@ -81,4 +101,4 @@ describe('getValidRootDirectories', () => {
81101
expect(result).toHaveLength(1);
82102
});
83103
});
84-
});
104+
});

src/filesystem/roots-utils.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,21 @@ import { fileURLToPath } from "url";
88
/**
99
* Converts a root URI to a normalized directory path with basic security validation.
1010
* @param rootUri - File URI (file://...) or plain directory path
11-
* @returns Promise resolving to validated path or null if invalid
11+
* @returns Promise resolving to original and resolved paths, or null if invalid
1212
*/
13-
async function parseRootUri(rootUri: string): Promise<string | null> {
13+
async function parseRootUri(rootUri: string): Promise<string[] | null> {
1414
try {
1515
const rawPath = rootUri.startsWith('file://') ? fileURLToPath(rootUri) : rootUri;
1616
const expandedPath = rawPath.startsWith('~/') || rawPath === '~'
1717
? path.join(os.homedir(), rawPath.slice(1))
1818
: rawPath;
1919
const absolutePath = path.resolve(expandedPath);
20+
const normalizedOriginal = normalizePath(absolutePath);
2021
const resolvedPath = await fs.realpath(absolutePath);
21-
return normalizePath(resolvedPath);
22+
const normalizedResolved = normalizePath(resolvedPath);
23+
return normalizedOriginal === normalizedResolved
24+
? [normalizedResolved]
25+
: [normalizedOriginal, normalizedResolved];
2226
} catch {
2327
return null; // Path doesn't exist or other error
2428
}
@@ -53,25 +57,33 @@ export async function getValidRootDirectories(
5357
requestedRoots: readonly Root[]
5458
): Promise<string[]> {
5559
const validatedDirectories: string[] = [];
60+
const seenDirectories = new Set<string>();
5661

5762
for (const requestedRoot of requestedRoots) {
58-
const resolvedPath = await parseRootUri(requestedRoot.uri);
59-
if (!resolvedPath) {
63+
const rootPaths = await parseRootUri(requestedRoot.uri);
64+
if (!rootPaths) {
6065
console.error(formatDirectoryError(requestedRoot.uri, undefined, 'invalid path or inaccessible'));
6166
continue;
6267
}
6368

64-
try {
65-
const stats: Stats = await fs.stat(resolvedPath);
66-
if (stats.isDirectory()) {
67-
validatedDirectories.push(resolvedPath);
68-
} else {
69-
console.error(formatDirectoryError(resolvedPath, undefined, 'non-directory root'));
69+
for (const rootPath of rootPaths) {
70+
if (seenDirectories.has(rootPath)) {
71+
continue;
72+
}
73+
74+
try {
75+
const stats: Stats = await fs.stat(rootPath);
76+
if (stats.isDirectory()) {
77+
validatedDirectories.push(rootPath);
78+
seenDirectories.add(rootPath);
79+
} else {
80+
console.error(formatDirectoryError(rootPath, undefined, 'non-directory root'));
81+
}
82+
} catch (error) {
83+
console.error(formatDirectoryError(rootPath, error));
7084
}
71-
} catch (error) {
72-
console.error(formatDirectoryError(resolvedPath, error));
7385
}
7486
}
7587

7688
return validatedDirectories;
77-
}
89+
}

0 commit comments

Comments
 (0)