Description
Problem
FastMCP has no built-in way to enforce that file paths or resource URIs stay
within client-declared roots. The MCP spec defines a Roots capability so clients
can advertise which filesystem boundaries a server should respect — but nothing
in the SDK enforces those boundaries.
Server authors who want to honour roots today must:
- Manually call
ctx.session.list_roots() on every tool invocation
- Write their own path normalisation and prefix-checking logic
- Repeat this across every tool that handles file paths
This makes it easy to accidentally bypass the security intent of the Roots
capability.
Example of the gap
# Client declares roots: ["/home/user/project"]
# But a tool call arrives with an arbitrary path:
@mcp.tool()
async def read_file(path: str, ctx: Context) -> str:
with open(path) as f: # No enforcement.
return f.read() # Happily reads /etc/passwd
# or ../../.ssh/id_rsa.
The client declared a boundary. The SDK exposed that boundary via ctx.session.list_roots(). But nothing checked the incoming path against it — because that check is the server author's responsibility, and the SDK ships no helper for it.
Proposed Solution
Add a utility module at src/mcp/server/fastmcp/utilities/roots.py that exposes:
get_roots(ctx) — async helper that fetches the current roots from the session.
assert_within_roots(path, roots) — validates a path is within at least one
declared root; raises PermissionError with a clear message if not.
@within_roots_check — decorator for tool functions that runs the above two
automatically, so a tool can be protected with one line.
Example
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.utilities.roots import within_roots_check
mcp = FastMCP("file-server")
@mcp.tool()
@within_roots_check
async def read_file(path: str, ctx: Context) -> str:
with open(path) as f:
return f.read()
If path is outside any client-declared root, the decorator raises before the
body runs.
Implementation Notes
- Roots are fetched via
ctx.session.list_roots() (the existing SDK pattern)
- Path comparison uses
pathlib.Path.resolve() to normalise symlinks and
relative segments before prefix-checking
- The decorator uses
functools.wraps to preserve the tool function's signature
- Follows the same pattern as other FastMCP utilities (
logging, dependencies)
Checklist
References
No response
Description
Problem
FastMCP has no built-in way to enforce that file paths or resource URIs stay
within client-declared roots. The MCP spec defines a Roots capability so clients
can advertise which filesystem boundaries a server should respect — but nothing
in the SDK enforces those boundaries.
Server authors who want to honour roots today must:
ctx.session.list_roots()on every tool invocationThis makes it easy to accidentally bypass the security intent of the Roots
capability.
Example of the gap
The client declared a boundary. The SDK exposed that boundary via
ctx.session.list_roots(). But nothing checked the incomingpathagainst it — because that check is the server author's responsibility, and the SDK ships no helper for it.Proposed Solution
Add a utility module at
src/mcp/server/fastmcp/utilities/roots.pythat exposes:get_roots(ctx)— async helper that fetches the current roots from the session.assert_within_roots(path, roots)— validates a path is within at least onedeclared root; raises
PermissionErrorwith a clear message if not.@within_roots_check— decorator for tool functions that runs the above twoautomatically, so a tool can be protected with one line.
Example
If
pathis outside any client-declared root, the decorator raises before thebody runs.
Implementation Notes
ctx.session.list_roots()(the existing SDK pattern)pathlib.Path.resolve()to normalise symlinks andrelative segments before prefix-checking
functools.wrapsto preserve the tool function's signaturelogging,dependencies)Checklist
feat/roots-utilityReferences
No response