How the MCP Roots Protocol Enables Dynamic Directory Access
The MCP roots protocol enables servers to discover, validate, and update authorized filesystem directories at runtime without requiring restarts or pre-declared command-line arguments.
The modelcontextprotocol/servers repository implements the MCP roots protocol to eliminate static directory configurations. Instead of launching with hardcoded paths, servers dynamically request authorized roots from the client, validate them, and maintain a live whitelist that updates throughout the session.
Server Initialization and Root Discovery
When the filesystem server starts in src/filesystem/index.ts, it checks for client capabilities during the oninitialized lifecycle hook. If the client advertises roots support, the server immediately requests the authorized directory list.
// src/filesystem/index.ts
server.server.oninitialized = async () => {
const clientCapabilities = server.server.getClientCapabilities();
if (clientCapabilities?.roots) {
// Client supports MCP Roots → request the list
const response = await server.server.listRoots();
// Process validated roots...
}
};
The listRoots() method returns an array of Root objects, each containing a uri (e.g., file:///home/user/project) and an optional name. This request happens automatically during connection setup, allowing the server to bootstrap with zero pre-configured directories.
Validating Root Directories with getValidRootDirectories
Raw URIs from the client undergo strict validation before becoming active access points. The getValidRootDirectories function in src/filesystem/roots-utils.ts handles this sanitization:
// src/filesystem/roots-utils.ts
export async function getValidRootDirectories(
requestedRoots: readonly Root[]
): Promise<string[]> {
// ...
const resolvedPath = await parseRootUri(requestedRoot.uri);
// ...
const stats = await fs.stat(resolvedPath);
if (stats.isDirectory()) validatedDirectories.push(resolvedPath);
}
The validation pipeline performs several critical steps:
parseRootUriexpandsfile://schemes, resolves tilde (~) home directories, follows symlinks, and normalizes paths- Each path must exist on the filesystem and be a directory (not a file)
- Invalid or inaccessible entries are filtered out with diagnostic logging
Only directories passing all validation checks enter the active whitelist.
Runtime Directory Updates via roots/list_changed
The protocol supports live directory changes without server restarts. After initialization, the server registers a notification handler for roots/list_changed events:
// src/filesystem/index.ts
server.server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
const response = await server.server.listRoots(); // re‑fetch current roots
if (response && 'roots' in response) {
await updateAllowedDirectoriesFromRoots(response.roots);
}
});
When the client sends a roots/list_changed notification, the server executes updateAllowedDirectoriesFromRoots. This function validates the new root set and calls setAllowedDirectories from src/filesystem/lib.ts to atomically replace the global whitelist. All subsequent tool calls immediately respect the new boundaries.
Session-Based Root Tracking in the Everything Server
The Everything server package extends this pattern to support multi-session environments. In src/everything/server/roots.ts, roots are stored per session ID:
// src/everything/server/roots.ts
export const roots: Map<string | undefined, Root[]> = new Map();
export async function syncRoots(sessionId?: string) {
if (!clientCapabilities?.roots) return;
const response = await server.listRoots();
if (response?.roots) roots.set(sessionId, response.roots);
// ...
}
The syncRoots function maintains isolated root sets for different client connections, enabling concurrent sessions with distinct directory access rights.
Querying Current Roots with Diagnostic Tools
Servers expose their current root configuration back to clients through dedicated tools. The Everything server implements get-roots-list in src/everything/tools/get-roots-list.ts to return a formatted view of active roots. This bidirectional visibility allows clients to verify that the server correctly interpreted the root list before executing sensitive filesystem operations.
Practical Implementation Examples
Starting Without Pre-Declared Directories
Launch the filesystem server without command-line arguments:
$ mcp-server-filesystem
# Server prints:
# "Started without allowed directories - waiting for client to provide roots via MCP protocol"
The server remains operational but blocks filesystem access until the client provides valid roots through the protocol.
Client-Side Root Updates
A client implementation sends new directories and triggers updates:
// Client sends initial roots during capability negotiation
const myRoots = [
{ uri: 'file:///home/alice/workspace', name: 'workspace' },
{ uri: 'file:///tmp/shared', name: 'shared' }
];
// Later, after adding /var/log to the list:
await client.notify('roots/list_changed'); // Server refreshes automatically
The server receives the notification, calls listRoots(), validates the expanded set including /var/log, and updates allowedDirectories instantly.
Summary
- Dynamic discovery: The
modelcontextprotocol/serversimplementation requests authorized directories vialistRoots()during theoninitializedphase rather than parsing static command-line arguments. - Strict validation: The
getValidRootDirectoriesfunction insrc/filesystem/roots-utils.tssanitizes URIs, resolves symlinks, and verifies directory existence before granting access. - Live updates: The
roots/list_changednotification handler enables runtime root replacement without server restarts throughupdateAllowedDirectoriesFromRoots. - Session isolation: The Everything server tracks roots per session in
src/everything/server/roots.ts, supporting concurrent clients with different access permissions. - Bidirectional protocol: Servers expose current root states through tools like
get-roots-list, allowing clients to verify access boundaries before executing operations.
Frequently Asked Questions
What is the MCP roots protocol?
The MCP roots protocol is a capability that allows MCP servers to request a list of authorized filesystem directories (roots) from the client rather than requiring those paths to be specified at server startup. This enables dynamic, per-session access control where the client determines which directories the server may touch.
How does the server validate root directories?
The server validates roots through getValidRootDirectories in src/filesystem/roots-utils.ts, which calls parseRootUri to convert file:// URIs to absolute paths, expand tildes, resolve symlinks, and verify that each entry exists and is a directory. Invalid entries are silently skipped with diagnostic logging.
Can root directories change after the server starts?
Yes. Clients send a roots/list_changed notification whenever they modify their root list. The server's notification handler re-fetches the current roots via listRoots(), validates them, and updates the internal whitelist immediately. No server restart is required for these changes to take effect.
Which files implement dynamic root management in modelcontextprotocol/servers?
Key implementations include src/filesystem/index.ts for the main initialization and update logic, src/filesystem/roots-utils.ts for URI parsing and validation, src/filesystem/lib.ts for the setAllowedDirectories state management, and src/everything/server/roots.ts for multi-session root tracking.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →