diff --git a/src/filesystem/__tests__/roots-utils.test.ts b/src/filesystem/__tests__/roots-utils.test.ts index 1a39483953..7446295c91 100644 --- a/src/filesystem/__tests__/roots-utils.test.ts +++ b/src/filesystem/__tests__/roots-utils.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { getValidRootDirectories } from '../roots-utils.js'; +import { getValidRootDirectories, mergeAllowedDirectories } from '../roots-utils.js'; import { mkdtempSync, rmSync, mkdirSync, writeFileSync, realpathSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; @@ -81,4 +81,33 @@ describe('getValidRootDirectories', () => { expect(result).toHaveLength(1); }); }); -}); \ No newline at end of file +}); + +describe('mergeAllowedDirectories', () => { + it('preserves CLI directories while adding current client roots', () => { + const result = mergeAllowedDirectories( + ['/cli/one', '/cli/two'], + ['/roots/current'] + ); + + expect(result).toEqual(['/cli/one', '/cli/two', '/roots/current']); + }); + + it('deduplicates directories shared by CLI args and client roots', () => { + const result = mergeAllowedDirectories( + ['/shared', '/cli/only'], + ['/shared', '/roots/only'] + ); + + expect(result).toEqual(['/shared', '/cli/only', '/roots/only']); + }); + + it('falls back to the CLI baseline when the client provides no valid roots', () => { + const result = mergeAllowedDirectories( + ['/cli/one', '/cli/two'], + [] + ); + + expect(result).toEqual(['/cli/one', '/cli/two']); + }); +}); diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 7b67e63e58..43f2d25ae5 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -13,7 +13,7 @@ import path from "path"; import { z } from "zod"; import { minimatch } from "minimatch"; import { normalizePath, expandHome } from './path-utils.js'; -import { getValidRootDirectories } from './roots-utils.js'; +import { getValidRootDirectories, mergeAllowedDirectories } from './roots-utils.js'; import { // Function imports formatSize, @@ -89,6 +89,11 @@ if (accessibleDirectories.length === 0 && allowedDirectories.length > 0) { allowedDirectories = accessibleDirectories; +// Preserve CLI-provided directories for merging with MCP roots later. +// CLI directories are explicitly set by the administrator and must not be +// discarded when the client provides roots via the MCP roots protocol. +const cliAllowedDirectories = [...allowedDirectories]; + // Initialize the global allowedDirectories in lib.ts setAllowedDirectories(allowedDirectories); @@ -702,15 +707,17 @@ server.registerTool( } ); -// Updates allowed directories based on MCP client roots +// Updates allowed directories by merging CLI-provided directories with MCP client roots. +// CLI directories are always preserved; client roots are added on top of them. async function updateAllowedDirectoriesFromRoots(requestedRoots: Root[]) { const validatedRootDirs = await getValidRootDirectories(requestedRoots); + allowedDirectories = mergeAllowedDirectories(cliAllowedDirectories, validatedRootDirs); + setAllowedDirectories(allowedDirectories); + if (validatedRootDirs.length > 0) { - allowedDirectories = [...validatedRootDirs]; - setAllowedDirectories(allowedDirectories); // Update the global state in lib.ts - console.error(`Updated allowed directories from MCP roots: ${validatedRootDirs.length} valid directories`); + console.error(`Allowed directories: ${cliAllowedDirectories.length} from CLI + ${validatedRootDirs.length} from MCP roots (${allowedDirectories.length} total after dedup)`); } else { - console.error("No valid root directories provided by client"); + console.error(`No valid root directories provided by client, using ${cliAllowedDirectories.length} CLI directory${cliAllowedDirectories.length === 1 ? '' : 'ies'}`); } } diff --git a/src/filesystem/roots-utils.ts b/src/filesystem/roots-utils.ts index 5e26bb246b..fec58fe8d2 100644 --- a/src/filesystem/roots-utils.ts +++ b/src/filesystem/roots-utils.ts @@ -74,4 +74,17 @@ export async function getValidRootDirectories( } return validatedDirectories; -} \ No newline at end of file +} + +/** + * Merges the fixed CLI directory baseline with the current client-provided roots. + * + * The merge is recalculated from scratch each time so outdated roots do not linger + * after a roots/list_changed update. + */ +export function mergeAllowedDirectories( + cliAllowedDirectories: readonly string[], + rootDirectories: readonly string[] +): string[] { + return [...new Set([...cliAllowedDirectories, ...rootDirectories])]; +}