Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/filesystem/__tests__/roots-utils.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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', () => {
Expand All @@ -81,4 +101,4 @@ describe('getValidRootDirectories', () => {
expect(result).toHaveLength(1);
});
});
});
});
40 changes: 26 additions & 14 deletions src/filesystem/roots-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null> {
async function parseRootUri(rootUri: string): Promise<string[] | null> {
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
}
Expand Down Expand Up @@ -53,25 +57,33 @@ export async function getValidRootDirectories(
requestedRoots: readonly Root[]
): Promise<string[]> {
const validatedDirectories: string[] = [];
const seenDirectories = new Set<string>();

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;
}
}
Loading