PyFu

FastMCP Authentication

Python Web Development Frameworks

By default a FastMCP server has no authentication. Over the stdio transport that is reasonable, because the only caller is the local process that launched it. Over a network transport it means every tool, resource, and prompt is open to anyone who can reach the port. Authentication in FastMCP is something you add, and it is added by passing an auth provider when you construct the server.

from fastmcp import FastMCP
from fastmcp.server.auth.providers.bearer import BearerAuthProvider

# Verify a bearer token (e.g. a JWT) on every request.
auth = BearerAuthProvider(jwks_uri="https://issuer.example/.well-known/jwks.json",
                          issuer="https://issuer.example", audience="internal-tools")

mcp = FastMCP("internal-tools", auth=auth)

if __name__ == "__main__":
    mcp.run(transport="http", host="0.0.0.0", port=8000)

FastMCP ships bearer-token and OAuth providers. With a provider configured, requests that arrive without a valid credential are rejected before any tool runs; without one, the server answers tools/list, tools/call, resources/read, and prompts/get for every caller.

Why authentication matters from an offensive security perspective

The single most important fact about MCP security is that authentication is opt-in, and the configuration that “just works” in tutorials omits it. A bare mcp.run(transport="http", host="0.0.0.0", port=8000) is a complete, unauthenticated RPC server, and that exact line gets copied into deployments. So the first thing to determine about any network-reachable MCP server is whether an auth provider is wired in at all; if it is not, you are looking at the headline finding and everything in FastMCP Tools, FastMCP Resources, and FastMCP Prompts is directly reachable. The exploitation of that state is in Unauthenticated FastMCP Servers.

When a provider is present, the checks shift to the usual token weaknesses, because FastMCP’s bearer provider validates JWTs and inherits the entire JWT attack surface. Confirm the signature is actually verified and the algorithm is pinned, since an accepted none algorithm or an RS256-to-HS256 confusion forges a valid token; the mechanics are in Forging JWTs with the none Algorithm and JWT Algorithm Confusion (RS256 to HS256). Check that the issuer and audience are enforced and that a weak or leaked signing key cannot be recovered, as in Cracking Weak JWT Signing Keys.

One limitation persists even with authentication configured: it is a gate at the door, not per-tool authorization. FastMCP authenticates the caller but does not, on its own, decide which authenticated caller may invoke which tool. A server that authenticates everyone and then exposes a run_command tool to all of them has only moved the boundary, not closed it. Treat authentication as necessary but not sufficient, and judge each tool by what it does once a caller is past the door.

Mitigation

Wire an auth provider into every network-transport server and let FastMCP reject unauthenticated requests before any tool runs, then pin the token validation so the gate cannot be forged. Configure the bearer provider with an explicit issuer and audience, source verification keys from a JWKS endpoint or a known public key so the algorithm is fixed to an asymmetric one, and never accept tokens whose claims you do not check. Because the provider only authenticates and does not authorize, add per-tool checks inside sensitive tools using the authenticated identity from the request context, so a valid token is not by itself a license to invoke everything.

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",   # enforced
    audience="internal-tools",         # enforced
)

mcp = FastMCP("internal-tools", auth=auth)

@mcp.tool
def run_command(cmd: str) -> str:
    # Authenticate at the door, then authorize per tool.
    if not caller_is_admin():
        raise PermissionError("not authorized")
    ...

if __name__ == "__main__":
    mcp.run(transport="http", host="127.0.0.1", port=8000)