A couple of months ago, I published the blog post Implementing Authentication in a Remote MCP Server with SSE Transport. That article demonstrated how to add authentication for remote MCP servers written in Go.
At the time, I also wanted to include Python examples. Unfortunately, things weren’t straightforward. The official Python MCP SDK didn’t provide a clean way to implement what I needed. There were some workarounds using Starlette middleware, but in my experience, those solutions were brittle and ultimately unsuccessful.
Later, I managed to create a working Python MCP server supporting SSE (or streaming HTTP) transport. But my solution relied on thread-level hacks to make the data thread-safe. It worked, but it felt like a fragile and inelegant design—something I wasn’t comfortable recommending or maintaining long-term.
Now, after revisiting the problem, I’ve found a much cleaner solution in Python. This time it’s not with the official Python MCP SDK, but with an alternative implementation called FastMCP. FastMCP is written in the spirit of the official SDK, offering a very similar syntax, but with additional features, clearer abstractions, and—importantly—excellent documentation.
Why Authentication Matters
My MCP server runs on a remote machine, while user authentication happens on a separate web application. When users log in to the site, they can visit their profile page and copy an “MCP Link” that looks like this:
https://my-server.com/mcp?token=some-secure-token
Each token is unique, generated per user, and stored in the database. The web application handles the token lifecycle, but I don’t currently support OAuth2. That means OAuth2 authentication flows aren’t an option for this setup—I need a simpler, token-in-URL solution.
What’s Wrong with the Official Python SDK?
The official Python SDK does have some OAuth2 support, but that didn’t solve my use case. There are two main issues:
-
Limited client support – Very few AI Agent/Chat tools currently support OAuth2 authentication when connecting to MCP servers. If I want my server to work seamlessly across a wide range of tools, I need to offer the simplest and most widely accepted approach: passing a token in the URL.
-
No OAuth2 in my system – My web application doesn’t implement OAuth2 at all. I only have token-based authentication. So I need a way for the MCP server to accept and verify tokens directly from the query string.
The official SDK doesn’t provide a good mechanism for this. That’s where FastMCP comes in.
Authentication with FastMCP Middleware
FastMCP provides a middleware layer, making it easy to insert authentication logic into your server. Instead of juggling threads or hacking around the request lifecycle, you can use clean middleware classes to validate tokens and inject user context into the request flow.
Here’s an example of implementing authentication with FastMCP:
from fastmcp import FastMCP, Context
from fastmcp.server.middleware import Middleware, MiddlewareContext
from fastmcp.exceptions import ToolError
from fastmcp.server.dependencies import get_http_request
import asyncio
class UserAuthMiddleware(Middleware):
async def on_call_tool(self, context: MiddlewareContext, call_next):
request = get_http_request()
token = request.query_params.get("token")
if not token:
# no token -> unauthorized
raise ToolError("Access denied: private tool")
user_id = await self.verify_token_and_get_user_id(token)
if not user_id:
raise ToolError("Access denied: private tool")
# Store user info in context state
context.fastmcp_context.set_state("user_id", user_id)
return await call_next(context)
async def verify_token_and_get_user_id(self, token: str) -> str | None:
# TODO: replace with real DB logic or call to auth service
await asyncio.sleep(0) # simulate async DB call
return "user_123" if token == "secrettoken" else None
# Create MCP server
mcp = FastMCP(name="DemoAuthServer")
mcp.add_middleware(UserAuthMiddleware())
@mcp.tool
def echo(message: str, ctx: Context) -> str:
"""Echo tool. Returns the input message."""
user = ctx.get_state("user_id")
return f"User {user} says: {message}"
if __name__ == "__main__":
mcp.run(transport="http", host="127.0.0.1", port=8000, path="/mcp")
How It Works
- Custom Middleware – The
UserAuthMiddleware
checks for a token in the query parameters. If none is found, or if validation fails, the request is rejected with an error. - User State Injection – When a valid token is found, the corresponding
user_id
is stored in the request’s context state. - Tool Access – Inside tools, you can retrieve the
user_id
from the context and personalize the response or enforce per-user logic.
This approach is both clean and maintainable. No need for thread hacks, no fragile workarounds. The middleware neatly encapsulates authentication logic, making it easy to swap out token verification for a real database query or external auth service later.
Auth with headers
This example can be modified to work with Authorization header instead of a query argument.
from fastmcp.server.dependencies import get_http_headers
class UserAuthMiddleware(Middleware):
async def on_call_tool(self, context: MiddlewareContext, call_next):
headers = get_http_headers()
header = headers.get("authorization")
if not header:
# no token -> unauthorized
raise ToolError("Access denied: private tool")
if not header.startswith("Bearer "):
raise ToolError("Access denied: invalid token format")
token = header.removeprefix("Bearer ").strip()
user_id = await self.verify_token_and_get_user_id(token)
if not user_id:
raise ToolError("Access denied: private tool")
# Middleware stores user info in context state
context.fastmcp_context.set_state("user_id", user_id)
return await call_next(context)
async def verify_token_and_get_user_id(self, token: str) -> str | None:
# TODO: replace with real DB logic or call to auth service
await asyncio.sleep(0) # simulate async call to DB or service
return "user_123" if token == "secrettoken" else None
Why I Like FastMCP
FastMCP has quickly become my go-to for Python-based MCP servers. Compared to the official SDK, it:
- Provides middleware support out of the box.
- Keeps the API syntax simple and familiar.
- Offers better documentation, which makes experimenting and extending much smoother.
- Encourages cleaner design patterns, such as separating auth logic into middleware.
Final Thoughts
For my use case—remote MCP servers secured with per-user tokens—FastMCP was the missing piece. It gave me exactly what I wanted: a clean, thread-safe, and extensible way to add authentication.
If you’re working on Python-based MCP servers and need more flexibility than the official SDK provides, I highly recommend giving FastMCP a try.