FastMCP Resources
Resources are data an MCP client can read by URI, as opposed to tools, which are functions it calls. A resource is a function decorated with @mcp.resource(uri); the client lists resources with resources/list and reads one with resources/read, naming its URI.
from fastmcp import FastMCP
mcp = FastMCP("internal-tools")
@mcp.resource("config://database")
def database_config() -> str:
return "postgresql://app:hunter2@db.internal:5432/prod"
Resource URIs can also be templated, with parameters drawn from the URI the client requests:
@mcp.resource("file://logs/{name}")
def read_log(name: str) -> str:
# `name` comes straight from the requested URI.
return open(f"/var/log/app/{name}.log").read()
The client supplies the URI, so name here is attacker-controlled in exactly the way a query parameter is.
Why resources matter from an offensive security perspective
A resource is a read primitive exposed over the protocol, and on a network-reachable server it is readable by anyone who can speak MCP. Two distinct problems follow.
First, resources/list is enumeration that runs before any tool is called. It discloses the internal structure of the server: the names and URIs of every resource, which routinely reveal file paths, configuration keys, and the existence of internal systems. The first example above does worse, returning a live database URL with credentials in it, so simply reading the resource leaks a secret. Resource listings are reconnaissance handed to the client for free.
Second, a templated resource whose parameter reaches a filesystem or network call is the same untrusted-input-to-sink bug as anywhere else. The read_log example interpolates name into a path with no containment check, so a request for file://logs/../../../../etc/passwd walks out of the intended directory exactly as in Insecure File Access and Path Traversal in Python. A resource that builds a URL from its parameter is an SSRF; one that runs a query is an injection. The URI is the input, and resource templates are where it becomes dangerous.
When you assess a server, read resources/list first for disclosed structure and secrets, then test every templated resource by feeding traversal sequences and unexpected values into its URI parameters. As with tools, FastMCP provides no per-resource authorization, so reachability equals the transport (see Running a FastMCP Server). The full enumeration-and-read workflow against a no-auth server is in Unauthenticated FastMCP Servers.
Mitigation
Keep secrets out of resource bodies entirely, since a resource is readable to anyone who reaches the transport, and treat every templated URI parameter as untrusted input bound for a sink. Do not return live credentials or connection strings from a resource; pull those from the environment at call time and never expose them over the protocol. For templated resources, validate the parameter against a strict pattern and resolve the final path against a fixed base directory, rejecting anything that escapes it, exactly as in Insecure File Access and Path Traversal in Python. Put an auth provider on any network transport so resource enumeration is not free reconnaissance for unauthenticated callers.
from pathlib import Path
LOG_DIR = Path("/var/log/app").resolve()
@mcp.resource("file://logs/{name}")
def read_log(name: str) -> str:
if not name.isalnum():
raise ValueError("invalid log name")
target = (LOG_DIR / f"{name}.log").resolve()
if not target.is_relative_to(LOG_DIR): # contain traversal
raise ValueError("path escapes log directory")
return target.read_text()