diff --git a/src/McpContext.ts b/src/McpContext.ts index fa15ff67..8a67d0cb 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -537,9 +537,14 @@ export class McpContext implements Context, BrowserlessContext { if (result.status === 'fulfilled') { const canonicalRoot = result.value; + // A filesystem root such as `/` or `C:\` already ends with the + // separator; appending another one would match no path below it. + const rootPrefix = canonicalRoot.endsWith(path.sep) + ? canonicalRoot + : canonicalRoot + path.sep; if ( canonicalPath === canonicalRoot || - canonicalPath.startsWith(canonicalRoot + path.sep) + canonicalPath.startsWith(rootPrefix) ) { allowed = true; break; diff --git a/tests/roots.test.ts b/tests/roots.test.ts index b09ebdd8..7afbddd2 100644 --- a/tests/roots.test.ts +++ b/tests/roots.test.ts @@ -181,6 +181,23 @@ describe('McpContext path validation escaping', () => { }); } + it('allows paths below a workspace root that is the filesystem root', async () => { + const context = await createContext(); + const targetPath = path.resolve( + os.homedir(), + 'a_very_unlikely_path_name_filesystem_root', + ); + const canonicalPath = await resolveCanonicalPath(targetPath); + // Use the root of the canonical path so the test also holds when the home + // directory goes through a symlink or junction onto another drive. + const filesystemRoot = path.parse(canonicalPath).root; + context.setRoots([ + {uri: pathToFileURL(filesystemRoot).href, name: 'filesystem root'}, + ]); + const resolved = await context.validatePath(targetPath); + assert.strictEqual(resolved, canonicalPath); + }); + it('escapes the file path when it cannot be resolved', async () => { const context = await createContext(); const filePath = path.join(os.tmpdir(), 'file.txt', INJECTED);