Skip to content

Commit 4127473

Browse files
Bingtagui404claude
andcommitted
fix(filesystem): preserve CLI directories when merging with MCP roots
The oninitialized and roots/list_changed handlers replaced all CLI-provided allowed directories with client roots, silently discarding any extra directories passed via command-line arguments. Store CLI directories as an immutable baseline and merge them with client roots using a Set for deduplication. When roots become empty or invalid, fall back to the CLI baseline instead of keeping stale roots. Fixes #3602 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b60eca1 commit 4127473

3 files changed

Lines changed: 58 additions & 9 deletions

File tree

src/filesystem/__tests__/roots-utils.test.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2-
import { getValidRootDirectories } from '../roots-utils.js';
2+
import { getValidRootDirectories, mergeAllowedDirectories } from '../roots-utils.js';
33
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, realpathSync } from 'fs';
44
import { tmpdir } from 'os';
55
import { join } from 'path';
@@ -81,4 +81,33 @@ describe('getValidRootDirectories', () => {
8181
expect(result).toHaveLength(1);
8282
});
8383
});
84-
});
84+
});
85+
86+
describe('mergeAllowedDirectories', () => {
87+
it('preserves CLI directories while adding current client roots', () => {
88+
const result = mergeAllowedDirectories(
89+
['/cli/one', '/cli/two'],
90+
['/roots/current']
91+
);
92+
93+
expect(result).toEqual(['/cli/one', '/cli/two', '/roots/current']);
94+
});
95+
96+
it('deduplicates directories shared by CLI args and client roots', () => {
97+
const result = mergeAllowedDirectories(
98+
['/shared', '/cli/only'],
99+
['/shared', '/roots/only']
100+
);
101+
102+
expect(result).toEqual(['/shared', '/cli/only', '/roots/only']);
103+
});
104+
105+
it('falls back to the CLI baseline when the client provides no valid roots', () => {
106+
const result = mergeAllowedDirectories(
107+
['/cli/one', '/cli/two'],
108+
[]
109+
);
110+
111+
expect(result).toEqual(['/cli/one', '/cli/two']);
112+
});
113+
});

src/filesystem/index.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import path from "path";
1313
import { z } from "zod";
1414
import { minimatch } from "minimatch";
1515
import { normalizePath, expandHome } from './path-utils.js';
16-
import { getValidRootDirectories } from './roots-utils.js';
16+
import { getValidRootDirectories, mergeAllowedDirectories } from './roots-utils.js';
1717
import {
1818
// Function imports
1919
formatSize,
@@ -89,6 +89,11 @@ if (accessibleDirectories.length === 0 && allowedDirectories.length > 0) {
8989

9090
allowedDirectories = accessibleDirectories;
9191

92+
// Preserve CLI-provided directories for merging with MCP roots later.
93+
// CLI directories are explicitly set by the administrator and must not be
94+
// discarded when the client provides roots via the MCP roots protocol.
95+
const cliAllowedDirectories = [...allowedDirectories];
96+
9297
// Initialize the global allowedDirectories in lib.ts
9398
setAllowedDirectories(allowedDirectories);
9499

@@ -702,15 +707,17 @@ server.registerTool(
702707
}
703708
);
704709

705-
// Updates allowed directories based on MCP client roots
710+
// Updates allowed directories by merging CLI-provided directories with MCP client roots.
711+
// CLI directories are always preserved; client roots are added on top of them.
706712
async function updateAllowedDirectoriesFromRoots(requestedRoots: Root[]) {
707713
const validatedRootDirs = await getValidRootDirectories(requestedRoots);
714+
allowedDirectories = mergeAllowedDirectories(cliAllowedDirectories, validatedRootDirs);
715+
setAllowedDirectories(allowedDirectories);
716+
708717
if (validatedRootDirs.length > 0) {
709-
allowedDirectories = [...validatedRootDirs];
710-
setAllowedDirectories(allowedDirectories); // Update the global state in lib.ts
711-
console.error(`Updated allowed directories from MCP roots: ${validatedRootDirs.length} valid directories`);
718+
console.error(`Allowed directories: ${cliAllowedDirectories.length} from CLI + ${validatedRootDirs.length} from MCP roots (${allowedDirectories.length} total after dedup)`);
712719
} else {
713-
console.error("No valid root directories provided by client");
720+
console.error(`No valid root directories provided by client, using ${cliAllowedDirectories.length} CLI directory${cliAllowedDirectories.length === 1 ? '' : 'ies'}`);
714721
}
715722
}
716723

src/filesystem/roots-utils.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,17 @@ export async function getValidRootDirectories(
7474
}
7575

7676
return validatedDirectories;
77-
}
77+
}
78+
79+
/**
80+
* Merges the fixed CLI directory baseline with the current client-provided roots.
81+
*
82+
* The merge is recalculated from scratch each time so outdated roots do not linger
83+
* after a roots/list_changed update.
84+
*/
85+
export function mergeAllowedDirectories(
86+
cliAllowedDirectories: readonly string[],
87+
rootDirectories: readonly string[]
88+
): string[] {
89+
return [...new Set([...cliAllowedDirectories, ...rootDirectories])];
90+
}

0 commit comments

Comments
 (0)