Skip to main content
Glama
donliggett

mcp-filesystem

by donliggett

mcp-filesystem

A hardened filesystem MCP server. Gives a local model read and write access to a set of directories you choose — and nothing else.

Built on the MCP TypeScript SDK v2 against the 2026-07-28 protocol revision, with backward compatibility for 2025-era clients on the same endpoint. Runs over stdio (for LM Studio, Claude Desktop, and anything else that spawns a local process) or Streamable HTTP (for a containerised shared endpoint).


Why this one

Most filesystem MCP servers check that a path starts with an allowed prefix and call it a day. That misses three things that matter:

  • Symlinks. A link planted inside the sandbox pointing at /etc defeats a prefix check entirely.

  • Writes through symlinked directories. realpath throws on paths that don't exist yet, so servers that only resolve existing files will happily create sandbox/linkdir/payload.sh outside the sandbox.

  • Prefix collisions. /data-secrets starts with /data.

This server resolves every path to its physical location before deciding — walking up to the deepest existing ancestor when the target doesn't exist yet — and compares against realpath'd roots with separator-aware matching. The test suite asserts each of those escapes fails.


Related MCP server: MCP Filesystem Server

Tools

Tool

Purpose

read_file

Read a text file, with line numbers, paging (offset/limit) and tail

read_multiple_files

Read up to 50 files in one call, sharing a byte budget

get_file_info

Size, type, timestamps, permissions, text/binary detection

list_allowed_directories

What's reachable, and the active limits

list_directory

One level, dirs first, optional sizes and timestamps

directory_tree

Indented recursive tree, skipping node_modules/.git/dist/…

search_files

Find by glob (**/*.ts)

grep_files

Search file contents by regex, with context lines

write_file

Atomic whole-file write

append_file

Append, with optional newline normalisation

edit_file

Exact-string replacement, returns a unified diff, supports dry_run

create_directory

mkdir -p

move_file

Move/rename, cross-filesystem safe

copy_file

Copy file or tree

delete_file

Delete, with an explicit recursive gate

Writes are atomic: content goes to a temp file in the same directory, is fsync'd, then renamed over the target. A crash or full disk leaves the original intact rather than truncated.


Quick start

npm install
npm run build
npm test

Then point a client at it:

node dist/index.js --root ./workspace

Or try it interactively without configuring a client:

npx @modelcontextprotocol/inspector node dist/index.js --root ./workspace

LM Studio

LM Studio reads ~/.lmstudio/mcp.json (on Windows, C:\Users\<you>\.lmstudio\mcp.json). Open it from Program → Install → Edit mcp.json, add an entry under mcpServers, then reload LM Studio.

Running it natively

The lowest-friction option, and the one to start with.

{
  "mcpServers": {
    "filesystem": {
      "command": "node",
      "args": [
        "/absolute/path/to/mcp-file-system/dist/index.js",
        "--root", "/absolute/path/to/your/project",
        "--read-only"
      ]
    }
  }
}

Drop --read-only once you trust it. Add more --root flags for more directories.

These two paths must be absolute. The host spawns the server as a child process with an unpredictable working directory, so a relative path will not resolve. On the command line, where you control the working directory, relative paths like --root ./workspace are fine.

On Windows either write forward slashes (C:/Users/you/projects) or double the backslashes, since a single \ is an escape character inside a JSON string.

Running it in Docker

Docker gives you a kernel-enforced boundary underneath the server's own checks, which is the real argument for it: even a bug in the sandbox code can't reach anything you didn't mount.

docker build -t mcp-filesystem:latest .
{
  "mcpServers": {
    "filesystem": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm", "--init",
        "--network", "none",
        "-v", "/absolute/path/to/your/project:/data:ro",
        "mcp-filesystem:latest",
        "--stdio", "--read-only"
      ]
    }
  }
}

Notes:

  • -i is required. Without it the container gets no stdin and the JSON-RPC handshake never happens — this is the single most common misconfiguration.

  • --network none is worth setting: this server has no reason to reach the network, and removing the interface removes a whole class of exfiltration.

  • :ro on the mount makes read-only enforcement the kernel's job. To allow writes, drop :ro and drop --read-only.

  • Docker requires the host side of -v to be an absolute path.

  • On Docker Desktop for Windows, the drive you are mounting from must be shared under Settings → Resources → File sharing.

  • On a Linux host, add --user "$(id -u):$(id -g)" so written files are owned by you rather than uid 1000.

Mount several directories by repeating -v and passing matching --root flags:

"-v", "/absolute/path/to/your/code:/data/code:ro",
"-v", "/absolute/path/to/your/notes:/data/notes",
"mcp-filesystem:latest",
"--stdio", "--root", "/data/code", "--root", "/data/notes"

HTTP transport

For a long-lived container that several clients share:

docker compose up -d
curl http://127.0.0.1:3000/health

Point a client at http://127.0.0.1:3000/.

This server has no authentication. Anyone who can reach the port has whatever filesystem access the server has. docker-compose.yml publishes to 127.0.0.1 only. If you bind it anywhere else, put an authenticating reverse proxy in front, and expect the startup log to warn you.

When bound to loopback the server validates Host and Origin headers to block DNS-rebinding — a web page you visit resolving an attacker-controlled domain to 127.0.0.1 and POSTing to this port.


Configuration

Every flag has an environment-variable equivalent, which is what the container uses. CLI flags win.

Flag

Env

Default

Meaning

--root <dir>

FS_ALLOWED_ROOTS (comma-separated)

required

Allowed directory. Repeatable.

--read-only

FS_READ_ONLY

false

Refuse all mutating tools

--deny <glob>

FS_DENY_PATTERNS

see below

Additional blocked patterns

--allow-default-denied

FS_ALLOW_DEFAULT_DENIED

false

Drop the built-in deny list

--follow-symlinks

FS_FOLLOW_SYMLINKS

false

Allow symlinks that stay in the sandbox

--max-read-bytes <n>

FS_MAX_READ_BYTES

10485760

Per-file read cap

--max-write-bytes <n>

FS_MAX_WRITE_BYTES

10485760

Per-file write cap

--max-results <n>

FS_MAX_RESULTS

1000

Cap on list/search/grep results

--max-depth <n>

FS_MAX_DEPTH

20

Recursion depth

--stdio / --http

FS_TRANSPORT

stdio

Transport

--host / --port

FS_HTTP_HOST / FS_HTTP_PORT

127.0.0.1 / 3000

HTTP bind

--audit / --no-audit

FS_AUDIT

true

JSON audit line per call on stderr

The server refuses to start with no roots configured. A filesystem server with no sandbox is not a safe default, and defaulting to the working directory just makes the mistake quiet.

Default deny list

Blocked unless you pass --allow-default-denied: .env and .env.*, *.pem, *.key, *.p12, *.pfx, *.keystore, id_rsa/id_dsa/id_ecdsa/ id_ed25519, .ssh/, .aws/, .gnupg/, .kube/config, .npmrc, .netrc, .pypirc, .docker/config.json, .git/, .svn/, .hg/, shadow.

This exists so that a careless -v $HOME:/data is survivable. It is a safety net, not a substitute for mounting the right directory.


Security model

What's enforced

  • Physical path resolution (realpath) before every containment decision, including for paths that don't exist yet

  • Separator-aware root matching (/data never matches /data-secrets)

  • Symlinks rejected by default, in any path position — not just the leaf

  • NUL-byte rejection (safe.txt\0/../../etc/passwd truncates in the syscall)

  • Windows: alternate data streams (file:stream), reserved device names (CON, NUL, COM1…), device-namespace paths (\\?\, \\.\), and case-insensitive containment

  • Read-only mode gates mutating tools before the handler runs

  • Both operands checked on move/copy — a source-only check is a write primitive for the whole host

  • Allowed roots cannot themselves be deleted or moved

  • Size caps checked via stat before allocating

  • Binary detection, so binaries aren't returned as token-burning garbage

  • Regex screening and a wall-clock deadline on grep_files

  • Error messages never echo host paths; SecurityError returns a vague message to the model and logs the real reason to the audit stream, so the sandbox isn't an oracle for mapping your filesystem

What isn't

  • TOCTOU. Between resolving a path and opening it, a local attacker who can write inside your allowed roots could swap a file for a symlink. Closing this needs openat2(RESOLVE_BENEATH) on Linux, which Node doesn't expose. The practical mitigation is the container boundary — mount only what you mean to expose.

  • Authentication. Neither transport authenticates. stdio inherits the trust of whoever spawned the process; HTTP is loopback-only for that reason.

  • Resource exhaustion. Caps and deadlines bound most things, but a pathological regex can still burn one 15-second deadline of CPU. The compose file sets memory and CPU limits.

  • Prompt injection. If a file inside your sandbox contains instructions and your model follows them, this server will faithfully execute whatever tools the model calls next. Read-only mode is the mitigation that actually works.

Container hardening (in docker-compose.yml): non-root user, read_only root filesystem, all capabilities dropped, no-new-privileges, tmpfs /tmp, memory and CPU limits.


Audit log

One JSON object per line on stderr — never stdout, which is the JSON-RPC channel under stdio. console.log is monkey-patched to redirect to stderr at startup so a stray debug statement can't corrupt the protocol stream.

{"ts":"2026-08-21T19:12:03.441Z","tool":"read_file","outcome":"ok","durationMs":3,"paths":["src/index.ts"],"bytes":4821}
{"ts":"2026-08-21T19:12:07.882Z","tool":"read_file","outcome":"denied","durationMs":1,"detail":"physical containment failed: /data/../etc/passwd -> /etc/passwd"}

Logged paths are sandbox-relative. The detail field carries the full reason and is only ever written here, never returned to the model.

docker compose logs -f filesystem | jq 'select(.outcome=="denied")'

Tests

npm run build && npm test

test/sandbox.test.ts is the suite that matters — every case is an attempt to reach a file outside the root. If one of them starts passing where it should throw, the server is broken in the only way that's genuinely dangerous.

Symlink tests skip themselves on Windows unless Developer Mode is on, since creating symlinks otherwise needs admin rights.


Project layout

src/
  index.ts              entrypoint, transport selection, shutdown
  config.ts             CLI + env parsing, root resolution
  security/
    sandbox.ts          path resolution and containment — the security core
    audit.ts            structured stderr logging, stdout protection
  tools/
    context.ts          registration wrapper: read-only gate, errors, audit
    read.ts             read_file, read_multiple_files, get_file_info, ...
    write.ts            write_file, append_file, edit_file
    listing.ts          list_directory, directory_tree
    manage.ts           create_directory, move_file, copy_file, delete_file
    search.ts           search_files, grep_files
  util/
    walk.ts             sandbox-aware directory traversal with cycle guard
    binary.ts           binary detection, BOM handling
    errors.ts           error taxonomy and fs error translation
    format.ts           output formatting for model consumption

License

MIT

A
license - permissive license
Not graded
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    A
    quality
    D
    maintenance
    Enables secure filesystem operations with directory sandboxing and optional read-only mode. Supports file reading/writing, directory management, file searching, and text operations while restricting access to specified directories.
    12
  • A
    license
    A
    quality
    D
    maintenance
    Provides secure filesystem access for AI models through the Model Context Protocol with strict path validation, file operations, directory management, and system command execution within predefined directories.
    16
    7
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Provides sandboxed access to local filesystem operations including directory and file management, content search with glob and regex patterns, and binary file support with configurable safety limits.
  • A
    license
    Not graded
    quality
    C
    maintenance
    Provides a secure, constrained filesystem workspace for LLM agents to manage files, notes, and code artifacts via stdio or remote HTTP. It features granular access controls, including extension whitelisting, storage quotas, and immutable paths for safe automated file operations.
    BSD 3-Clause

View all related MCP servers

Related MCP Connectors

  • Securely search and manage workspace context files for AI agents and teams.

  • Operate Linux, macOS and Windows from your LLM. Every action runs through an auditable allowlist.

  • Cross-agent artifact workspace with provenance across Claude Code, Codex, Cursor, LangGraph.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/donliggett/mcp-file-system'

If you have feedback or need assistance with the MCP directory API, please join our Discord server