diff --git a/src/filesystem/__tests__/path-utils.test.ts b/src/filesystem/__tests__/path-utils.test.ts index 5530cba1c3..a4d4a0cc4b 100644 --- a/src/filesystem/__tests__/path-utils.test.ts +++ b/src/filesystem/__tests__/path-utils.test.ts @@ -195,7 +195,7 @@ describe('Path Utilities', () => { // UNC paths should preserve the leading double backslash const uncPath = '\\\\SERVER\\share\\folder'; expect(normalizePath(uncPath)).toBe('\\\\SERVER\\share\\folder'); - + // Test UNC path with double backslashes that need normalization const uncPathWithDoubles = '\\\\\\\\SERVER\\\\share\\\\folder'; expect(normalizePath(uncPathWithDoubles)).toBe('\\\\SERVER\\share\\folder'); @@ -222,6 +222,19 @@ describe('Path Utilities', () => { expect(result.length).toBeGreaterThan(0); }); + it('leaves literal ~MyFolder unchanged (not home expansion)', () => { + expect(expandHome('~MyFolder')).toBe('~MyFolder'); + }); + + it('leaves ~user unchanged (not home expansion)', () => { + expect(expandHome('~user')).toBe('~user'); + }); + + it('leaves paths with tilde in the middle unchanged', () => { + expect(expandHome('/Volumes/Drive/Projects/~MyFolder')).toBe('/Volumes/Drive/Projects/~MyFolder'); + expect(expandHome('/home/user/~backup')).toBe('/home/user/~backup'); + }); + it('leaves other paths unchanged', () => { expect(expandHome('C:/test')).toBe('C:/test'); }); diff --git a/src/filesystem/__tests__/path-validation.test.ts b/src/filesystem/__tests__/path-validation.test.ts index 81ad247ee2..32c01a4ac4 100644 --- a/src/filesystem/__tests__/path-validation.test.ts +++ b/src/filesystem/__tests__/path-validation.test.ts @@ -12,10 +12,10 @@ async function checkSymlinkSupport(): Promise { try { const targetFile = path.join(testDir, 'target.txt'); const linkFile = path.join(testDir, 'link.txt'); - + await fs.writeFile(targetFile, 'test'); await fs.symlink(targetFile, linkFile); - + // If we get here, symlinks are supported return true; } catch (error) { @@ -240,7 +240,11 @@ describe('Path Validation', () => { // But only on the same filesystem root if (path.sep === '\\') { - expect(isPathWithinAllowedDirectories('D:\\other', ['/'])).toBe(false); + // '/' resolves to the current drive root on Windows, so we must + // test with a drive letter that differs from the CWD's drive. + const cwdDrive = process.cwd().charAt(0).toUpperCase(); + const otherDrive = cwdDrive === 'D' ? 'C' : 'D'; + expect(isPathWithinAllowedDirectories(`${otherDrive}:\\other`, ['/'])).toBe(false); } }); }); diff --git a/src/filesystem/__tests__/roots-utils.test.ts b/src/filesystem/__tests__/roots-utils.test.ts index 1a39483953..1c930e797e 100644 --- a/src/filesystem/__tests__/roots-utils.test.ts +++ b/src/filesystem/__tests__/roots-utils.test.ts @@ -48,7 +48,7 @@ describe('getValidRootDirectories', () => { it('should normalize complex paths', async () => { const subDir = join(testDir1, 'subdir'); mkdirSync(subDir); - + const roots = [ { uri: `file://${testDir1}/./subdir/../subdir`, name: 'Complex Path' } ]; @@ -81,4 +81,36 @@ describe('getValidRootDirectories', () => { expect(result).toHaveLength(1); }); }); + + describe('tilde-prefixed directory names', () => { + let tildeDir: string; + + beforeEach(() => { + // Create a directory with a tilde-prefixed name inside testDir1 + tildeDir = join(testDir1, '~MyFolder'); + mkdirSync(tildeDir); + }); + + it('should handle directory names starting with tilde as literal names', async () => { + const roots = [ + { uri: tildeDir, name: 'Tilde Dir' } + ]; + + const result = await getValidRootDirectories(roots); + + expect(result).toHaveLength(1); + expect(result[0]).toContain('~MyFolder'); + }); + + it('should handle file:// URIs with tilde-prefixed directory names', async () => { + const roots = [ + { uri: `file://${tildeDir}`, name: 'Tilde Dir URI' } + ]; + + const result = await getValidRootDirectories(roots); + + expect(result).toHaveLength(1); + expect(result[0]).toContain('~MyFolder'); + }); + }); }); \ No newline at end of file diff --git a/src/filesystem/__tests__/startup-validation.test.ts b/src/filesystem/__tests__/startup-validation.test.ts index 3be283df74..26c3821cf1 100644 --- a/src/filesystem/__tests__/startup-validation.test.ts +++ b/src/filesystem/__tests__/startup-validation.test.ts @@ -97,4 +97,15 @@ describe('Startup Directory Validation', () => { // Should still start with the valid directory expect(result.stderr).toContain('Secure MCP Filesystem Server running on stdio'); }); + + it('should start successfully with tilde-prefixed directory name', async () => { + const tildeDir = path.join(testDir, '~MyFolder'); + await fs.mkdir(tildeDir, { recursive: true }); + + const result = await spawnServer([tildeDir]); + + // Server should start without errors + expect(result.stderr).toContain('Secure MCP Filesystem Server running on stdio'); + expect(result.stderr).not.toContain('Error:'); + }); }); diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index a515df7c61..acaa3f48e6 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -499,8 +499,7 @@ server.registerTool( // Format the output const formattedEntries = sortedEntries.map(entry => - `${entry.isDirectory ? "[DIR]" : "[FILE]"} ${entry.name.padEnd(30)} ${ - entry.isDirectory ? "" : formatSize(entry.size).padStart(10) + `${entry.isDirectory ? "[DIR]" : "[FILE]"} ${entry.name.padEnd(30)} ${entry.isDirectory ? "" : formatSize(entry.size).padStart(10) }` ); @@ -710,7 +709,8 @@ async function updateAllowedDirectoriesFromRoots(requestedRoots: Root[]) { setAllowedDirectories(allowedDirectories); // Update the global state in lib.ts console.error(`Updated allowed directories from MCP roots: ${validatedRootDirs.length} valid directories`); } else { - console.error("No valid root directories provided by client"); + console.error("Error: No valid root directories provided by client. Received roots:", + JSON.stringify(requestedRoots.map(r => r.uri))); } } @@ -745,7 +745,7 @@ server.server.oninitialized = async () => { } else { if (allowedDirectories.length > 0) { console.error("Client does not support MCP Roots, using allowed directories set from server args:", allowedDirectories); - }else{ + } else { throw new Error(`Server cannot operate: No allowed directories available. Server was started without command-line directories and client either does not support MCP roots protocol or provided empty roots. Please either: 1) Start server with directory arguments, or 2) Use a client that supports MCP roots protocol and provides valid root directories.`); } } diff --git a/src/filesystem/path-utils.ts b/src/filesystem/path-utils.ts index 50910b995b..9d21315dd4 100644 --- a/src/filesystem/path-utils.ts +++ b/src/filesystem/path-utils.ts @@ -106,7 +106,9 @@ export function normalizePath(p: string): string { } /** - * Expands home directory tildes in paths + * Expands home directory tildes in paths. + * Only expands `~` (bare) or `~/...` (home-prefixed) paths. + * Literal folder names like `~MyFolder` are intentionally left unchanged. * @param filepath The path to expand * @returns Expanded path */ diff --git a/src/filesystem/roots-utils.ts b/src/filesystem/roots-utils.ts index 5e26bb246b..445bc8a5e6 100644 --- a/src/filesystem/roots-utils.ts +++ b/src/filesystem/roots-utils.ts @@ -1,7 +1,6 @@ import { promises as fs, type Stats } from 'fs'; import path from 'path'; -import os from 'os'; -import { normalizePath } from './path-utils.js'; +import { normalizePath, expandHome } from './path-utils.js'; import type { Root } from '@modelcontextprotocol/sdk/types.js'; import { fileURLToPath } from "url"; @@ -13,14 +12,14 @@ import { fileURLToPath } from "url"; 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 expandedPath = expandHome(rawPath); const absolutePath = path.resolve(expandedPath); const resolvedPath = await fs.realpath(absolutePath); return normalizePath(resolvedPath); - } catch { - return null; // Path doesn't exist or other error + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`Failed to resolve root URI "${rootUri}": ${message}`); + return null; } } @@ -53,14 +52,14 @@ export async function getValidRootDirectories( requestedRoots: readonly Root[] ): Promise { const validatedDirectories: string[] = []; - + for (const requestedRoot of requestedRoots) { const resolvedPath = await parseRootUri(requestedRoot.uri); if (!resolvedPath) { console.error(formatDirectoryError(requestedRoot.uri, undefined, 'invalid path or inaccessible')); continue; } - + try { const stats: Stats = await fs.stat(resolvedPath); if (stats.isDirectory()) { @@ -72,6 +71,6 @@ export async function getValidRootDirectories( console.error(formatDirectoryError(resolvedPath, error)); } } - + return validatedDirectories; } \ No newline at end of file