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
15 changes: 14 additions & 1 deletion src/filesystem/__tests__/path-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
});
Expand Down
10 changes: 7 additions & 3 deletions src/filesystem/__tests__/path-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ async function checkSymlinkSupport(): Promise<boolean> {
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) {
Expand Down Expand Up @@ -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);
}
});
});
Expand Down
34 changes: 33 additions & 1 deletion src/filesystem/__tests__/roots-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
];
Expand Down Expand Up @@ -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');
});
});
});
11 changes: 11 additions & 0 deletions src/filesystem/__tests__/startup-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:');
});
});
8 changes: 4 additions & 4 deletions src/filesystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}`
);

Expand Down Expand Up @@ -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)));
}
}

Expand Down Expand Up @@ -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.`);
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/filesystem/path-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
19 changes: 9 additions & 10 deletions src/filesystem/roots-utils.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -13,14 +12,14 @@ import { fileURLToPath } from "url";
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 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;
}
}

Expand Down Expand Up @@ -53,14 +52,14 @@ export async function getValidRootDirectories(
requestedRoots: readonly Root[]
): Promise<string[]> {
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()) {
Expand All @@ -72,6 +71,6 @@ export async function getValidRootDirectories(
console.error(formatDirectoryError(resolvedPath, error));
}
}

return validatedDirectories;
}