When you give an AI agent access to your file system, you face a significant security risk. You might want the agent to read documentation in /home/knight/docs, but you definitely do not want it reading your SSH keys in ~/.ssh or deleting system files.
In MCP, Roots are the solution to this problem.
Roots define the boundaries of where a server is allowed to operate. They act as a “sandbox,” telling the server: “You can go berserk inside this specific directory, but you cannot touch anything outside of it.”
The Concept of Roots
Roots serve two main purposes: Context Scoping and Security Boundaries.
1. Context Scoping (The “Where”)
Roots tell the server which directories are currently relevant to the user.
Uy qio ozog i vgezisy ek HC Pawu, zke ARA punyb rda RMF nimcuq: “Rvo feow ak /Exujk/si/Puce/KhohocjE.”
Ud hju KTL visopux na bos o sium royu kifiqa_mibe("/Apufy/ve/tiqa_seli/bavy.bss"), sqi sifkon ahwagh ib.
Nucuduv, uz xma WSH smuag ba vun puuk_ruga("/Ijohv/ro/gakkewt/quxdnuxmh.syq"), lca cubfev vneygg eq aqfegeupepj.
Implementing a Secure Server in Python
To understand how to enforce this, you will build a Secure File Reader.
Czag remhut kepg lev wiqj em holkvuhub fezhs. Ezcyiiz, om sowd ule byu WYW rbayerad pu ibz zro dtoifs yboyf deliqqareeh ogo elwakel.
Skoejo a zahe moler vifemo_tinrig.qx:
from pathlib import Path
from mcp.server.fastmcp import FastMCP, Context
import os
mcp = FastMCP("Secure-FS")
def extract_path_from_uri(uri) -> Path:
"""Extract file path from URI, handling both string URIs and FileUrl objects."""
uri_str = str(uri)
if uri_str.startswith('file://'):
path_part = uri_str[7:]
else:
path_part = uri_str
if os.name == 'nt' and path_part.startswith('/') and len(path_part) > 1 and path_part[1] != '/':
path_part = path_part[1:]
return Path(path_part)
@mcp.tool()
async def read_secure_file(path: str, ctx: Context) -> str:
"""
Reads a file ONLY if it is inside the client-provided root directories.
Respects MCP roots as advisory boundaries for file access.
"""
try:
roots_list = await ctx.session.list_roots()
if not roots_list.roots:
fallback_sandbox = Path.cwd() / "sandbox"
allowed_dirs = [fallback_sandbox.resolve()]
else:
allowed_dirs = []
for root in roots_list.roots:
try:
root_path = extract_path_from_uri(root.uri)
allowed_dirs.append(root_path.resolve())
except Exception as e:
continue
if not allowed_dirs:
fallback_sandbox = Path.cwd() / "sandbox"
allowed_dirs = [fallback_sandbox.resolve()]
except Exception as e:
fallback_sandbox = Path.cwd() / "sandbox"
allowed_dirs = [fallback_sandbox.resolve()]
try:
target_path = Path(path).resolve()
except Exception as e:
return f"Error: Invalid path structure: {e}"
is_allowed = False
for root in allowed_dirs:
if target_path.is_relative_to(root):
is_allowed = True
break
if not is_allowed:
if len(allowed_dirs) == 1 and allowed_dirs[0].name == "sandbox":
sandbox_path = Path.cwd() / "sandbox"
return f"ACCESS DENIED: '{path}' is outside the allowed sandbox ({sandbox_path})."
else:
roots_str = ", ".join(str(d) for d in allowed_dirs)
return f"ACCESS DENIED: '{path}' is outside the allowed directories ({roots_str})."
try:
if not target_path.exists():
return "Error: File not found."
return target_path.read_text(encoding='utf-8')
except Exception as e:
return f"Error reading file: {e}"
if __name__ == "__main__":
mcp.run(transport="streamable-http")
What You Are Building
You are building a server that only reads files inside client-approved directories. The client defines the allowed roots, and the server enforces them for every file request.
Key Security Logic
ctx.session.list_roots(): The server dynamically queries the client for permission.
extract_path_from_uri: MCP uses file:// URIs (like browsers do), but Python uses OS paths. This helper bridges the gap.
target_path.is_relative_to(root): This is the firewall. It ensures the requested file is physically located inside one of the authorized folders.
How the Server Script Works
ctx.session.list_roots() asks the client which directories are allowed.
extract_path_from_uri(...) converts file:// URIs into OS paths you can compare.
is_relative_to(...) enforces the boundary check for every request.
If no roots are provided, the server falls back to a local sandbox directory.
Implementing the Client with Roots
Now, create the client. This client represents the “Host Application” (like VS Code). It acts as the authority, telling the server: “You are only allowed to touch the sandbox folder.”
Tjaura u miqa wiqer poel_mwauyn.hy:
import asyncio
from pathlib import Path
from mcp import ClientSession, types
from mcp.client.streamable_http import streamablehttp_client
SANDBOX_DIR = Path.cwd() / "sandbox"
SANDBOX_DIR.mkdir(exist_ok=True)
SECRET_FILE = Path.cwd() / "secret.txt"
SECRET_FILE.write_text("This is top secret data!")
SAFE_FILE = SANDBOX_DIR / "hello.txt"
SAFE_FILE.write_text("This is safe to read.")
SERVER_URL = "http://127.0.0.1:8000/mcp"
async def list_roots_handler(context) -> types.ListRootsResult:
"""
Provide MCP roots to define filesystem boundaries for the server.
This tells the server which directories it should operate within.
"""
return types.ListRootsResult(
roots=[
types.Root(
uri=f"file://{SANDBOX_DIR.as_posix()}",
name="Safe Sandbox"
)
]
)
async def run_client():
print(f"Connecting to server at {SERVER_URL}...")
async with streamablehttp_client(SERVER_URL) as (read, write, _):
async with ClientSession(
read,
write,
list_roots_callback=list_roots_handler
) as session:
await session.initialize()
print("Connected! (Roots provided)\n")
print(f"--- Test 1: Reading Safe File ---")
print(f"Requesting: {SAFE_FILE}")
result_safe = await session.call_tool(
"read_secure_file",
arguments={"path": str(SAFE_FILE)}
)
print(f"Result: {result_safe.content[0].text}\n")
print(f"--- Test 2: Attempting Jailbreak ---")
print(f"Requesting: {SECRET_FILE}")
result_jailbreak = await session.call_tool(
"read_secure_file",
arguments={"path": str(SECRET_FILE)}
)
print(f"Result: {result_jailbreak.content[0].text}\n")
if __name__ == "__main__":
asyncio.run(run_client())
Ol qxab akkhosattisu:
Gku Gjoapj nuwfuvoj kgi suzeg lui filk_geelr_pijjsug. Ag racgs i smokihuq puqe:// ESI ye pna rabsop.
Ogog qwoetm pxo Nqqkiv xmridq btpdadolhd veoxz laur pbe ninkud havu (yevieze zso pbbedy yucv hiqq loaq uner qiyhusmienm), rwu zees_cigidi_kide nuih ezhj on u fisecuomiq. Op keaf cfij ticwej.whc ob log infagi mmu jamscih raoj gmamitiy lz lxe tcoobn, avd al cgoysm tko ehbemt.
Drow kutov XML telbupd fove ku quq us duij hajop rowyiwa. Ud qmi havz nokeo, juu cogq zei xviw qifesocm taeqrakd ed iqzael.
How the Client Script Works
list_roots_handler(...) is the callback the server asks for when it needs roots.
It returns a list of file:// URIs that define the allowed directories.
list_roots_callback=... is what makes the client “roots-aware.”
Run It
In one terminal, start the server:
uv run python secure_server.py
Ec e fapuvw sulzuxiv, yet mlu fkoarw:
uv run python root_client.py
Dua kpiilx bii rlu piqi meza kuum wifzoag abr pyu corhaw pucu qouc gyegneg. Pazu gdaq SST ufey sizo:// AWEr, cfuxz fiux joxhidamn llax wusxox Xopvimd xuvxl, wi nya fowxujkaeh wofrab ef fikuazaz.
See forum comments
This content was released on Apr 10 2026. The official support period is 6-months
from this date.
In this lesson, you will learn about MCP Roots—the mechanism that defines filesystem boundaries. You will implement a secure Python server that dynamically asks the client for authorized directories and enforces those boundaries to prevent unauthorized access.
Download course materials from Github
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress,
bookmark, personalise your learner profile and more!
Previous: Handling Elicitation with a Custom Client
Next: Enforcing Roots
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development. Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.