Lead · 7 chapters
Roots
Scoping a server to specific parts of the client's workspace or filesystem
Treat roots as a useful scope contract, never as a security boundary; production isolation still requires enforcement outside the MCP server.
Read these first
What you will know
Scope a server to specific roots and verify it cannot read outside them
Path traversal, symlinks, alternate URI forms, and normalization bugs can defeat a boundary that only checks string prefixes.
Run a cooperating server with roots configured and record which operations it permits and rejects
A server may ignore, reinterpret, or only partially honor the client-provided roots, so its observed behavior matters more than the declaration.
Configure roots for a monorepo
Overlapping package roots and shared tooling directories can unintentionally expose sibling packages or repository-wide secrets.
Test the boundary rather than assuming it
Negative tests should cover traversal, symlink escapes, absolute paths, malformed URIs, and operations that use a different filesystem API.
Pair roots with an OS-level sandbox and demonstrate the difference against a malicious server
Roots communicate intended scope, while a sandbox applies permissions even when the server is buggy or hostile.
Chapters
- 1
The roots capability
Roots run the opposite way from most MCP features: the client declares the roots capability at initialize, answers roots/list when the server asks, and sends notifications/roots/list_changed when the set moves, while the server asks and then decides what to do with the answer. The specification only says a server should respect the boundary, so the declaration binds nobody — point a cooperating server at one package and record which operations it permits and rejects, because its observed behaviour is the only evidence there is.
- 2
Roots and OS-level sandboxes
Configure file:///srv/app/web as the only root and a third-party server can still return /etc/passwd without violating the protocol, because no component in the stack inspects its filesystem calls against your list. Roots communicate intended scope, which a sandbox cannot express; a sandbox applies permissions to a buggy or hostile server, which roots cannot. A pull request description merging the two gets approved with nothing confining the workspace, and the reachable set stays three orders of magnitude wider than the root implies.
- 3
Converting a root URI to a path
Roots arrive as file:// URIs and every containment check operates on a bare filesystem path, so something has to convert one into the other — and that conversion is its own place for the boundary to fail. Percent-decoding has to happen before any prefix comparison, or the encoded form of a separator walks straight through it; a non-empty host component means the URI is not yours to serve, so file://localhost/etc/passwd and file://attacker/share are rejected rather than trimmed; and Windows drive letters and UNC forms parse by different rules than POSIX paths do. Get this step wrong and the careful check that follows is applied to a path you were never actually handed.
- 4
Lexical path checks and realpath
Resolving an incoming path and requiring the result to start with the root correctly rejects ../ traversal and the /srv/app/webhooks prefix trick, then passes /srv/app/web/uploads/logs where uploads is a symlink to /var/lib — the escape happening when open follows the link, and 1.4 MB coming back from outside the root. Switching to realpath closes that hole and opens a 40–90 µs window between the check and the syscall, which is a real improvement and not a closed class; hard links and bind mounts are outside what any path resolution can see.
- 5
Unguarded filesystem entry points
read_file and list_directory refuse to leave the root while search_code returns hits from a sibling package — not a contradiction, but a second filesystem API that never passes the read path's check: a spawned ripgrep, a prebuilt index, a glob library following symlinks by default. This is why the negative-test matrix runs traversal, symlinks, encoded paths, absolute paths and the malformed URI forms against every operation the server exposes, generated from tools/list rather than hand-maintained, and why a system-call trace finds what no matrix imagined.
- 6
Confinement as evidence
An auditor asking whether a third-party server can read ~/.aws/credentials is not answered by roots plus a green negative-test matrix, since both measure the server's cooperation at higher and higher resolution. Show the read failing with EACCES or ENOENT under a container mounting only the project directory, or a seatbelt profile denying the rest, with the server's code untouched. That holds whether the server is careful, buggy or hostile, and it survives the dependency bump that none of the other artefacts survive.
- 7
Multiple roots and the union rule
roots/list returns an array with no ordering, precedence or nesting rules, so membership is a union: a path is allowed if it lies inside any entry, and adding the repository root to a list of package roots makes the package entries decoration. Rooting packages/checkout in a pnpm monorepo breaks typechecking where node_modules symlinks leave the root, and both tempting fixes grant the whole repository — 340 readable files become roughly 61,000, plus .env, .git and infra. Mount the store read-only into the server's filesystem view instead, and price what widening actually hands over.
External resources
- Roots — Model Context Protocol specification (2025-06-18)modelcontextprotocol.io
How clients declare root URIs and notify servers when the list changes.
- What are MCP roots?speakeasy.com
Implementation view of requesting and reacting to roots in a server.
- Understanding Roots in Model Context Protocol (MCP)workos.com
Why roots are hints for scoping, not an enforcement boundary.
- Roots: Telling the Server Where It May Lookimti.co
Practical framing of workspace scoping and who stays in control.
- Model Context Protocol (MCP) Client Deep Dive & Primitives Explained (Part 5 of 10)NamrataHShah
Covers roots as a client capability alongside sampling and elicitation.