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
33 changes: 31 additions & 2 deletions src/filesystem/__tests__/roots-utils.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -81,4 +81,33 @@ describe('getValidRootDirectories', () => {
expect(result).toHaveLength(1);
});
});
});
});

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']);
});
});
19 changes: 13 additions & 6 deletions src/filesystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);

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

Expand Down
15 changes: 14 additions & 1 deletion src/filesystem/roots-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,17 @@ export async function getValidRootDirectories(
}

return validatedDirectories;
}
}

/**
* 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])];
}
Loading