Summary
When auth=JWTVerifier(...) is passed to FastMCP, the built-in RequireAuthMiddleware only protects /mcp and /sse. Endpoints registered via @mcp.custom_route() share the same token-parsing middleware (so request.scope["user"] is populated when a valid token is present) but are never blocked on missing/invalid tokens.
Reproduction
mcp = FastMCP("my-server", auth=JWTVerifier(...))
@mcp.custom_route("/api/v1/tools", methods=["GET"])
async def list_tools(request) -> JSONResponse:
# reachable without a token even when auth is enabled
...
Unauthenticated requests to /api/v1/tools return 200 — no 401.
Current workaround
A BaseHTTPMiddleware that manually inspects request.scope["user"] for the relevant path prefix:
class _RestAuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
if AUTH_ENABLED and request.url.path.startswith("/api/v1/"):
from starlette.authentication import UnauthenticatedUser
user = request.scope.get("user")
if user is None or isinstance(user, UnauthenticatedUser):
return JSONResponse({"error": "unauthorized"}, status_code=401)
return await call_next(request)
This works but is a manual gap-fill rather than a first-class feature.
Proposed solution
A require_auth parameter on custom_route:
@mcp.custom_route("/api/v1/tools", methods=["GET"], require_auth=True)
async def list_tools(request) -> JSONResponse:
...
When require_auth=True and no authenticated user is present in the request scope, FastMCP would return HTTP 401 before the handler is called — consistent with how /mcp and /sse are protected today.
Environment
- fastmcp version: 3.1.1
- Python: 3.14
Summary
When
auth=JWTVerifier(...)is passed toFastMCP, the built-inRequireAuthMiddlewareonly protects/mcpand/sse. Endpoints registered via@mcp.custom_route()share the same token-parsing middleware (sorequest.scope["user"]is populated when a valid token is present) but are never blocked on missing/invalid tokens.Reproduction
Unauthenticated requests to
/api/v1/toolsreturn 200 — no 401.Current workaround
A
BaseHTTPMiddlewarethat manually inspectsrequest.scope["user"]for the relevant path prefix:This works but is a manual gap-fill rather than a first-class feature.
Proposed solution
A
require_authparameter oncustom_route:When
require_auth=Trueand no authenticated user is present in the request scope, FastMCP would return HTTP 401 before the handler is called — consistent with how/mcpand/sseare protected today.Environment