Introduction to FastMCP Security Testing
FastMCP is the de facto Python framework for building Model Context Protocol (MCP) servers, the components that expose tools, resources, and prompts to LLM clients like Claude Desktop, IDEs, and agent frameworks. It is the basis of the official MCP Python SDK, and a few decorators turn ordinary Python functions into capabilities an LLM can invoke. That convenience is exactly why it deserves security scrutiny: an MCP “tool” is server-side Python that runs with the server’s privileges, and the framework does almost nothing to control who is allowed to invoke it.
From an offensive perspective, an MCP server is a remote procedure call surface that was designed assuming a single trusted local client. The moment that assumption breaks, when the server is reachable over the network, when it is shared, when an attacker can reach the transport, every tool becomes a callable endpoint and every resource becomes a readable one, with no user, no session, and frequently no authentication in between.
How FastMCP works
An MCP server exposes three primitive types over JSON-RPC:
- Tools: functions the model (or anyone speaking the protocol) can call, with arguments. These are the dangerous ones: a tool that runs a command, queries a database, or reads a file is an RCE/SSRF/file-read primitive the instant it is reachable.
- Resources: data the client can read by URI (files, config, query results).
- Prompts: reusable prompt templates the client can list and fetch.
In FastMCP these are just decorated functions:
from fastmcp import FastMCP
mcp = FastMCP("demo")
@mcp.tool
def add(a: int, b: int) -> int:
return a + b
@mcp.resource("config://app")
def app_config() -> str:
return "internal config..."
@mcp.prompt
def review(code: str) -> str:
return f"Review this code:\n{code}"
Transports and the trust boundary
The trust boundary is the transport. FastMCP supports several:
- stdio: the server runs as a subprocess of one local client over stdin/stdout. This is the safe default for a desktop tool: there is no network listener, so reachability equals “already on the box as that user”.
- HTTP / SSE / streamable-http: the server binds a TCP port and speaks MCP over HTTP. This is what makes a server shareable, and what turns it into a network service that anyone who can reach the port can drive.
The dangerous configuration is an HTTP-transport server bound to 0.0.0.0 with no authentication, which is also the configuration that “just works” in tutorials and gets copied into deployments. FastMCP supports authentication (bearer tokens, OAuth), but it is opt-in; a bare mcp.run(transport="http", host="0.0.0.0", port=8000) is wide open.
What to test
- Is there a network transport at all, and is it authenticated? An unauthenticated HTTP/SSE MCP server is the headline finding, see Unauthenticated FastMCP Servers.
- What do the exposed tools do? Enumerate
tools/listand read each tool’s implementation. Tools that wrap shells, file access, or outbound requests reproduce Python Command Injection, Insecure File Access and Path Traversal in Python, and Server Side Request Forgery (SSRF) in Flask Applications, now invokable with no auth. - What leaks through resources and prompts?
resources/listandprompts/listdisclose internal structure, file paths, and sometimes secrets, before any tool is called. - Where is it bound? A server on
0.0.0.0(or behind a careless reverse proxy) is exposed far beyond its intended single client.
FastMCP concepts
The building blocks, each documented with its offensive angle:
- Running a FastMCP Server: transports and why the transport is the trust boundary.
- FastMCP Tools: functions the client calls, and why each is the sink it wraps.
- FastMCP Resources: data read by URI, enumeration, and templated-resource traversal.
- FastMCP Prompts: server-side prompt templates, disclosure, and template injection.
- FastMCP Authentication: the opt-in auth providers and the JWT surface behind them.
What this section covers
- Unauthenticated FastMCP Servers: enumerating and calling tools, prompts, and resources on a no-auth server, and escalating a tool call to code execution.
The takeaway: MCP collapses “expose a function to an LLM” into “expose a function”, and FastMCP makes that one decorator away. Treat every network-reachable MCP server as an unauthenticated RPC endpoint until you have confirmed an auth provider is configured, and treat each tool as the vulnerability class it wraps. The runnable lab is mcp-fu/fastmcp-no-auth.
Why FastMCP security testing matters from an offensive security perspective
I treat an MCP server as one of the densest targets in a Python estate, because a single misconfiguration exposes a whole catalog of capabilities at once rather than a single endpoint. The framework was built around a trusted local client, so the security-relevant decisions, whether to put the server on the network and whether to authenticate it, are left entirely to the developer, and the tutorial-default configuration gets both wrong. When the server is reachable and unauthenticated, every decorated function is a callable method and every resource is a readable one, with no user and no session in between, which means the framework hands me enumeration for free and tool invocation as code execution.
When auditing a deployment, these are the tells I look for first:
- An HTTP, SSE, or streamable-http transport. Anything other than
stdiois a network listener, and the trust boundary moves from the host to the port. - A bind on
0.0.0.0or no auth provider passed toFastMCP(...). Either alone is a critical finding, see Unauthenticated FastMCP Servers. - Tool bodies that wrap shells, filesystem paths, or outbound requests. Each reproduces its own vulnerability class, now reachable over the protocol, see FastMCP Tools.
- Resources and prompts that disclose internal structure or secrets.
resources/listandprompts/listare reconnaissance before any tool runs, see FastMCP Resources and FastMCP Prompts.
The defender takeaway: the most important property of an MCP server is not what its tools do but whether an unauthenticated caller can reach them at all, so confirm the transport and the auth provider before anything else.
Mitigation
Secure an MCP deployment at the boundary first, then at each capability. Default to the stdio transport for single-client tools so there is no listener to attack, and only move to a network transport when sharing is a real requirement. When you do network it, always pass an auth provider to FastMCP(...) so the handshake rejects unauthenticated callers, bind to 127.0.0.1 rather than 0.0.0.0 unless remote access is genuinely needed, and front it with an authenticating reverse proxy when it is. With the door closed, harden each capability on its own terms: scope tools narrowly and validate their arguments as you would any API input, keep secrets out of resource bodies and constrain templated paths, and pass prompt arguments as rendering data rather than into template source.
from fastmcp import FastMCP
from fastmcp.server.auth.providers.bearer import BearerAuthProvider
auth = BearerAuthProvider(jwks_uri="https://issuer.example/.well-known/jwks.json",
issuer="https://issuer.example", audience="internal-tools")
# Authenticated, loopback-bound. Reachability no longer equals invocation.
mcp = FastMCP("internal-tools", auth=auth)
if __name__ == "__main__":
mcp.run(transport="http", host="127.0.0.1", port=8000)