Repository navigation
Description
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.wrapsto preserve the tool function's signature - Follows the same pattern as other FastMCP utilities (
logging,dependencies)
Checklist
- Searched existing issues — no duplicate found
- Implementation and tests written locally on branch
feat/roots-utility - Will submit a PR referencing this issue
References
No response
Activity
feat(mcpserver): add Context.assert_within_roots for server-side root…
test: mark unreachable return in test helpers as no-cover
policylayer-dan commentedon Apr 29, 2026
The roots capability has always had this asymmetry: the spec defines the advertisement, but enforcement is left to each server author. Your proposal is the right fix for server authors who control the code.
Worth noting that the enforcement problem looks different from the consumer side. If you're an organisation running agents against MCP servers you didn't build, you can't assume every server author has done this check, and many won't have, especially third-party or open-source servers. In that context, the safety boundary needs to sit outside the server: a tool allowlist that restricts which tools the agent can call in the first place, or a policy that rejects tool calls whose arguments don't match expected path patterns before they reach the server.
For teams operating that way, that consumer-side enforcement layer is what PolicyLayer provides. But for the SDK itself, your
assert_within_rootsdecorator approach is the cleanest path forward for server authors who want defence-in-depth at the source.maxisbey commentedon Aug 10, 2026
Thanks for the report. The v2 rework changed the code this targets, so it no longer applies as-is. Closing as part of a general backlog cleanup following the v2 release. If this is still relevant against v2, feel free to reopen.
AI Disclaimer