• Uncategorised

Roots in MCP — when a server needs to touch a client file (and how the client stays in control)

When integrating external model servers with local applications using the Model Context Protocol (MCP), one of the most important safety primitives is roots.
Roots are how a client tells a connected server: “these are the only locations you may work with on my machine.”

They are a defensive boundary — not a sharing protocol.
This post explains, step-by-step, how a server can attempt to access client files, what messages are exchanged, and how the client enforces the boundary.

(This article assumes familiarity with MCP’s JSON-RPC messaging model and the idea that the client runs the LLM/agent that decides which tool calls to make.)


What roots are (short)

  • Roots are URIs (typically file:// paths) the client exposes to a server to define the allowable filesystem boundaries on the client machine.
  • They act like whitelisted workspaces or sandbox fences the client offers to the server.

Why roots exist

  1. Security & privacy: the server must never be able to arbitrarily access the client filesystem. Roots force the client to declare what’s acceptable.
  2. Context: roots tell the server which parts of the client environment are relevant (project folders, docs, mounts).
  3. Deterministic enforcement: the client enforces the rule — the server may reference paths, but the client decides whether to act on them.

Two roles, one boundary

  • Client: declares roots and enforces them. Runs the LLM/agent and controls whether or how files leave the machine.
  • Server: may propose or refer to client-side URIs (for example, by returning a suggestion or path in a normal response). The server does not get direct filesystem access; it only provides structure or suggestions that the client can follow or ignore.

Typical message flows involving roots

Below are practical flows you’ll see in real MCP deployments.


1️⃣ Client declares roots capability at startup

The client announces it supports roots and (optionally) includes an initial list or will answer roots/list requests.

Example capability snapshot the client sends during handshake:

{
  "capabilities": {
    "roots": { "listChanged": true }
  }
}

That tells the server: “I support roots; ask me or wait for notifications when they change.”


2️⃣ Server asks “what are your roots?”

A server can request the current roots so it knows the client’s workspace anchors:

// Server -> Client
{ "id": 1, "method": "roots/list", "params": {} }

The client responds with an explicit list:

// Client -> Server
{
  "id": 1,
  "result": {
    "roots": [
      { "name": "workspace", "uri": "file:///home/uday/projects/demo/" },
      { "name": "notes",     "uri": "file:///home/uday/docs/notes/" }
    ]
  }
}

This exchange gives the server knowledge of the allowed client paths; it does not give the server access — it only informs the server about where it may reference files.


3️⃣ Server references a client path in a normal response

Servers commonly reply with suggestions, diagnostics, or search results that include candidate paths under those roots:

// Server -> Client (normal response body)
{
  "id": 23,
  "result": {
    "suggestion": "Check the implementation at file:///home/uday/projects/demo/src/transport/StreamableHTTPServerTransport.java"
  }
}

The server is only referring to that URI.
It cannot read it or fetch its contents directly.
The response is merely a pointer the client can choose to follow.


4️⃣ Client decides (LLM/agent) whether to act

This is the crucial control point:

  • The client’s LLM/agent inspects the server’s response.
  • If the model decides that the next step is to fetch the file contents locally (for reading, summarization, analysis, etc.), the client will initiate a local operation — after verifying the URI is inside an allowed root.

Example client-side call (internal to client):

{
  "id": 42,
  "method": "client/readLocalFile",
  "params": { "uri": "file:///home/uday/projects/demo/src/transport/StreamableHTTPServerTransport.java" }
}

The client runtime verifies the URI against its declared roots before reading and returning content.
If the URI is outside roots, the client must refuse.

This is how the client keeps control and enforces the boundary.


Validation & enforcement (what the client must do)

Whenever a client acts on a server-supplied URI, it must:

  1. Resolve and normalize the URI (handle .., symlinks, and platform specifics).
  2. Check whether the normalized URI is within any declared root.
  3. Apply policy checks (read-only vs read-write, file size limits, user confirmation prompts).
  4. If allowed, perform the local read and use/send/forward the result; otherwise, return a clear error or ignore.

Clients that fail to canonicalize and check paths can be tricked into leaking files — so this enforcement is non-negotiable.


UX & consent: make access explicit

Good client UX patterns include:

  • Showing the user which roots the client is exposing and letting them edit it.
  • Asking for user confirmation before sending sensitive file contents.
  • Offering read-only or temporary roots to reduce long-term risk.

These keep the user firmly in control and prevent silent data leaks.


Example end-to-end (compact)

  1. Client starts and announces roots capability.
  2. Server asks roots/list and learns /home/uday/projects/demo/.
  3. Server returns a suggestion referencing file:///home/uday/projects/demo/README.md.
  4. Client LLM decides it needs the file → verifies it’s inside the root → reads it locally and uses it (summarization, display, etc.).

At no point did the server directly open a file on the client machine — it only provided a pointer, and the client chose to follow it within the declared boundary.


Final takeaways

  • Roots are a client-side safety fence. They let the client tell the server “these are the only client paths you may refer to.”
  • Servers can refer to client paths, but they cannot directly access them. Any actual file operation must be performed by the client after validation.
  • The LLM/agent on the client decides whether to act. The client must validate and enforce roots strictly and present clear consent to the user for sensitive actions.

You may also like...