# MCP Roots Protocol Constraints: Security and Validation Rules Explained

> Understand MCP roots protocol constraints. Discover how directory lists replace allow lists, validate URIs, and support dynamic updates for modelcontextprotocol/servers.

- Repository: [Model Context Protocol/servers](https://github.com/modelcontextprotocol/servers)
- Tags: deep-dive
- Published: 2026-03-01

---

**The MCP Roots protocol constraints enforce that client-provided directory lists completely replace server-side allow lists, require at least one valid directory to exist, validate that all root URIs resolve to real filesystem paths, and support dynamic updates through standardized notifications.**

The Model Context Protocol (MCP) Roots capability enables clients to specify which filesystem locations a server may access. According to the `modelcontextprotocol/servers` reference implementation, strict **MCP Roots protocol constraints** ensure that only verified, existing directories are exposed and that the security boundary remains deterministic across client sessions.

## How Roots Replace Server-Side Allow Lists

When a client connects with Roots capabilities, the server **completely overwrites** any directories supplied via command-line arguments. In [`src/filesystem/index.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts), the `updateAllowedDirectoriesFromRoots()` function handles this atomic replacement (lines 730–749):

```typescript
// The server calls this during initialization
await updateAllowedDirectoriesFromRoots(response.roots);
// allowedDirectories is replaced entirely, not merged

```

This design ensures the client has explicit, predictable control over the filesystem view. The server never merges or accumulates directories from multiple sources—it uses exactly what the client specifies.

## Mandatory Directory Requirements

The server enforces a hard requirement: **at least one allowed directory must exist**. If neither command-line arguments nor a non-empty roots list are present, the server aborts immediately with a clear error (lines 749–750 in [`src/filesystem/index.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts)):

```typescript
if (allowedDirectories.length === 0) {
  throw new Error("No allowed directories provided");
}

```

Even when the client supports roots but sends an empty list, the server logs a warning and retains the previous configuration, though it still requires at least one valid directory to remain operational (lines 40–42).

## URI Validation and Path Resolution

All root URIs undergo strict validation in `getValidRootDirectories()` ([`src/filesystem/roots-utils.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/roots-utils.ts), lines 13–23 and 52–68). The server:

1. Parses the URI through `parseRootUri()` (lines 13–22)
2. Expands and resolves symlinks using `fs.realpath()`
3. Normalizes paths via `normalizePath` (lines 20–22) to handle platform-specific quirks like macOS `/tmp` versus `/private/tmp`
4. Verifies the target is a directory using `fs.stat()`

This prevents clients from specifying non-existent paths, files instead of directories, or paths that bypass security through symlink manipulation.

## Client Capability Detection

The server only attempts to fetch roots when the client explicitly advertises the capability. During the `oninitialized` phase ([`src/filesystem/index.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts), lines 33–45), the server checks:

```typescript
if (clientCapabilities?.roots) {
  const response = await server.server.listRoots();
  // Process roots only if client supports them
}

```

If the client does not advertise `roots` capability, the server falls back to command-line supplied directories, maintaining backward compatibility while preserving security constraints.

## Dynamic Root Updates

Clients can modify the filesystem access list at runtime by sending a `roots/list_changed` notification. The handler at lines 17–28 in [`src/filesystem/index.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts) immediately re-requests the full list:

```json
{
  "method": "notification/roots/list_changed"
}

```

The server then repeats the validation cycle through `updateAllowedDirectoriesFromRoots()`, ensuring dynamic updates meet the same security standards as the initial configuration.

## Supported URI Schemes

The protocol restricts root specifications to **local filesystem resources only**. The `parseRootUri()` function ([`src/filesystem/roots-utils.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/roots-utils.ts), lines 13–22) accepts:

- `file://` URIs (e.g., `file:///Users/alice/projects`)
- Plain absolute or relative paths (e.g., `/var/data` or `./projects`)

Any URI scheme that cannot be resolved to a real filesystem path is rejected during the validation phase.

## Practical Configuration Examples

**Starting with command-line directories (fallback mode):**

```bash

# Provide directories directly when client lacks Roots support

mcp-server-filesystem /Users/alice/projects /var/data

```

**Client-specified roots during initialization:**

```json
{
  "capabilities": {
    "roots": {}
  },
  "roots": [
    { "uri": "file:///Users/alice/projects" },
    { "uri": "file:///var/data" }
  ]
}

```

**Querying current allowed directories:**

```bash

# Using the built-in tool to verify active constraints

mcp> list_allowed_directories

```

Output:

```

Allowed directories:
/Users/alice/projects
/var/data

```

## Summary

- **Atomic replacement**: Client roots overwrite command-line directories completely via `updateAllowedDirectoriesFromRoots()` in [`src/filesystem/index.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts) (lines 730–749).
- **Non-empty requirement**: The server refuses to start without at least one valid directory (lines 749–750).
- **Real path validation**: `getValidRootDirectories()` ensures all roots exist, resolve symlinks, and normalize paths ([`src/filesystem/roots-utils.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/roots-utils.ts)).
- **Capability-gated**: The server checks `clientCapabilities?.roots` before fetching any directory lists (lines 33–45).
- **Dynamic updates**: Runtime changes are supported through `roots/list_changed` notifications (lines 17–28).
- **Scheme restrictions**: Only `file://` URIs and plain paths are accepted by `parseRootUri()`.

## Frequently Asked Questions

### What happens if a client sends an empty roots list?

If the client supports roots but provides an empty array, the server logs a warning (lines 40–42 in [`src/filesystem/index.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts)) and retains the current configuration. However, if this results in zero allowed directories total, the server will throw an error and refuse to operate, maintaining the mandatory directory constraint.

### Can a server merge client roots with command-line directories?

No. According to the source code in [`src/filesystem/index.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts) (lines 730–749), the server assigns the returned list directly to `allowedDirectories`, completely replacing any directories supplied via command-line arguments. This ensures deterministic security boundaries where the client has explicit control.

### How does the server handle symbolic links in root paths?

The server resolves all symbolic links using `fs.realpath()` before normalization ([`src/filesystem/roots-utils.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/roots-utils.ts), lines 20–22). This prevents path traversal attacks where a symlink might point outside the intended directory, and ensures that different textual representations of the same directory (like `/tmp` versus `/private/tmp` on macOS) are treated equivalently.

### What URI schemes are supported for MCP roots?

Only `file://` URIs and plain absolute or relative filesystem paths are accepted. The `parseRootUri()` function in [`src/filesystem/roots-utils.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/roots-utils.ts) (lines 13–22) explicitly rejects any URI scheme that cannot be converted to a real filesystem path, ensuring the server only accesses local directories.