Skip to content

feat: Add roots enforcement utility to FastMCP (get_roots, assert_within_roots, @within_roots_check) #2453

Closed as not planned

Description

@NeelakandanNC

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

  • 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

  1. ghost added
    enhancementRequest for a new feature that's not currently supported
    on Apr 17, 2026
  2. ghost added
    needs decisionIssue is actionable, needs maintainer decision on whether to implement
    on Apr 17, 2026
  3. ghost added
    P2Moderate issues affecting some users, edge cases, potentially valuable feature
    on Apr 17, 2026
  4. policylayer-dan commented on Apr 29, 2026

    @policylayer-dan

    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_roots decorator approach is the cleanest path forward for server authors who want defence-in-depth at the source.

  5. maxisbey commented on Aug 10, 2026

    @maxisbey
    Contributor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Moderate issues affecting some users, edge cases, potentially valuable featureenhancementRequest for a new feature that's not currently supportedneeds decisionIssue is actionable, needs maintainer decision on whether to implement

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions