# How the MCP Filesystem Server Handles Symlinks on macOS

> Discover how the MCP Filesystem server securely handles symlinks on macOS. It resolves paths and validates directories to prevent attacks, ensuring safe access for macOS users.

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

---

**The MCP Filesystem server prevents symlink attacks by resolving all paths to their real locations using `fs.realpath` and validating them against both original and dereferenced allowed directories, ensuring transparent support for macOS paths like `/tmp` while blocking unauthorized access.**

The MCP Filesystem server in the `modelcontextprotocol/servers` repository implements a defense-in-depth strategy for symbolic link handling on macOS. By normalizing paths and resolving symlinks before any file operation, the server protects against directory traversal attacks while accommodating platform-specific filesystem conventions such as `/tmp` being a symlink to `/private/tmp`.

## Allowed Directory Preparation with Dual Path Storage

When the server initializes, it processes each user-provided directory to establish the security boundary.

### Storing Original and Resolved Paths

In [`src/filesystem/index.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts), lines 41-58, the server expands each allowed directory and resolves any symlinks **once** during startup. It stores **both** the original path and its resolved target in the allowed directories list. This dual storage ensures that requests using either the symbolic path (e.g., `/tmp`) or its real location (e.g., `/private/tmp`) correctly match the security policy.

This approach specifically addresses macOS behavior where system directories are symlinks. By caching both representations, the server avoids repeated filesystem calls while maintaining strict validation capabilities.

## Per-Request Validation with Real Path Resolution

Every file operation triggers a validation routine that dereferences symlinks in real-time to prevent escape attacks.

### The validatePath Function

Located in [`src/filesystem/lib.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/lib.ts), lines 113-121, the `validatePath` function executes the following security checks:

1. **Expands** tilde (`~`) notation and converts the path to absolute form.
2. **Calls** `fs.realpath` to obtain the actual filesystem location, following all symlinks.
3. **Verifies** that the resolved path falls within any of the previously stored allowed directories (matching against either the original or resolved variants).
4. **Rejects** the request with an "Access denied – symlink target outside allowed directories" error if the real path points outside the authorized boundary.

This ensures that even if a user provides a path like `/tmp/symlink-to-etc-passwd`, the server resolves the true target and blocks access if it lies outside the allowed directory tree.

## Root Directory Validation

When allowed directories are supplied via the MCP Roots protocol, the server applies additional symlink scrutiny.

### Securing MCP Roots

In [`src/filesystem/roots-utils.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/roots-utils.ts), lines 45-48, the `getValidRootDirectories` function dereferences root paths using `fs.realpath` during the validation phase. This prevents a malicious or misconfigured root directory that is itself a symlink to an unauthorized location (such as `~/allowed-dir` pointing to `/etc`) from bypassing security controls.

By resolving roots before adding them to the allowed set, the server ensures that the security boundary remains intact regardless of how the directories are accessed.

## Practical Implementation Example

The following example demonstrates starting the server with a directory that may be a symlink and performing validated operations:

```typescript
// Start the server with /tmp, which on macOS is a symlink to /private/tmp
$ mcp-server-filesystem /tmp

// The server internally stores both "/tmp" and "/private/tmp" as allowed entries

// Request using the symbolic path
await server.callTool("read_text_file", {
  path: "/tmp/example.txt"
});

// Request using the resolved path
await server.callTool("read_text_file", {
  path: "/private/tmp/example.txt"
});

// Both requests pass validation because validatePath:
// 1. Resolves the input to /private/tmp/example.txt via fs.realpath
// 2. Confirms it matches an allowed directory (either /tmp or /private/tmp)
// 3. Allows the operation to proceed

```

If a client attempts to access a file through a symlink that escapes the allowed boundary, the validation chain halts the operation before any filesystem access occurs.

## Summary

- **Dual path storage** in [`src/filesystem/index.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts) (lines 41-58) caches both original and resolved allowed directories to handle macOS symlinks transparently.
- **Real-time resolution** via `validatePath` in [`src/filesystem/lib.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/lib.ts) (lines 113-121) uses `fs.realpath` to dereference symlinks on every request and validate the true path location.
- **Root validation** in [`src/filesystem/roots-utils.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/roots-utils.ts) (lines 45-48) prevents symlink-based root directory attacks by resolving paths before adding them to the allowed set.
- **Security boundary** enforcement returns an explicit "Access denied" error when symlink targets fall outside authorized directories.

## Frequently Asked Questions

### How does the MCP Filesystem server prevent symlink attacks on macOS?

The server prevents symlink attacks by calling `fs.realpath` in the `validatePath` function to resolve all symlinks to their actual locations before performing any file operation. It then checks that the resolved path falls within the allowed directory boundaries, rejecting requests with an "Access denied – symlink target outside allowed directories" error if the target lies outside the authorized tree.

### Why does the server store both the original and resolved paths for allowed directories?

The server stores both variants to accommodate macOS filesystem conventions where directories like `/tmp` are symlinks to `/private/tmp`. By caching both the symbolic and real paths during startup (in [`src/filesystem/index.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts), lines 41-58), the server allows clients to use either representation while maintaining a single consistent security check during request validation.

### What happens if a symlink points to a location outside the allowed directories?

If `fs.realpath` resolves a requested path to a location outside the allowed directory set, the `validatePath` function immediately rejects the request. This check occurs before any file content is read or written, ensuring that symbolic links cannot be used to traverse into unauthorized areas of the filesystem such as `/etc` or `/home/other-user`.

### How does the server handle MCP Roots that are symlinks?

When processing directories supplied via the MCP Roots protocol, the `getValidRootDirectories` function in [`src/filesystem/roots-utils.ts`](https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/roots-utils.ts) (lines 45-48) resolves each root using `fs.realpath` before adding it to the allowed list. This prevents a root directory that is a symlink to an unauthorized location from being accepted as a valid security boundary.