This morning we released FastMCP 2.9, which includes a new, MCP-native approach to middleware.
Give us a star on GitHub or check out the updated docs at gofastmcp.com.
Middleware is one of those foundational features we’ve come to expect from any serious server framework. It’s the go-to pattern for adding cross-cutting concerns like authentication, logging, or caching without rewriting your core application logic.
Until today, when developers asked how to add middleware to their MCP server, the obvious answer seemed to be wrapping their server with traditional ASGI middleware. Unfortunately, that approach has two critical flaws:
-
It only works for web-based transports like streamable-HTTP and SSE. Until very recently, most major clients only supported the local STDIO transport, making this a non-starter for many.
-
More importantly, it forces you to parse the MCP’s low-level JSON-RPC messages yourself. All the hard work FastMCP does to give you clean, high-level Tool and Resource objects is lost. You’re left trying to reconstruct meaning from a sea of protocol noise.
This is a lot of work for a very limited set of outcomes.
So, we went back to the drawing board and embraced a core FastMCP principle: focus on the developer’s intent, not the protocol’s complexity.
MCP-Native Middleware
FastMCP 2.9 introduces a powerful, intuitive middleware system. Instead of wrapping the raw protocol stream, we wrap the high-level, semantic handlers that developers interact with. This is middleware that understands tools
, resources
, and prompts
, not just JSON-RPC messages.
Creating middleware is as simple as subclassing fastmcp.server.middleware.Middleware
and overriding the hooks you need. Here’s a basic logging middleware that prints every request and response (if any):
from fastmcp import FastMCPfrom fastmcp.server.middleware import Middleware, MiddlewareContext
class LoggingMiddleware(Middleware):
async def on_message(self, context: MiddlewareContext, call_next): """Called for every MCP message."""
print(f"-> Received {context.method}") result = await call_next(context) print(f"<- Responded to {context.method}")
return result
mcp = FastMCP(name="My Server")mcp.add_middleware(LoggingMiddleware())
While on_message
is great for generic tasks, the true strength of FastMCP’s middleware lies in its semantic awareness. You can target specific protocol messages types with on_request
or on_notification
, further filtered by whether the request was initiated by the server or the client, or even target specific operations like on_call_tool
to implement more sophisticated logic.
For example, here’s a middleware that prevents access to tools tagged as "private"
:
from fastmcp import FastMCP, Contextfrom fastmcp.exceptions import ToolErrorfrom fastmcp.server.middleware import Middleware, MiddlewareContext
class PrivateMiddleware(Middleware):
async def on_call_tool(self, context: MiddlewareContext, call_next): """Called when a tool is called."""
# Fetch the FastMCP Tool object tool_name = context.message.name tool = await context.fastmcp_context.fastmcp.get_tool(tool_name)
# Check if the tool is tagged as private if "private" in tool.tags: raise ToolError(f"Access denied to private tool: {tool_name}")
# If the check passes, continue to the next handler return await call_next(context)
mcp = FastMCP(name="Private Server")
@mcp.tool(tags={"private"})def super_secret_function(): return "This is a secret!"
mcp.add_middleware(PrivateMiddleware())
This approach leverages FastMCP’s high-level understanding of your components to enable powerful, context-aware logic for authentication, authorization, caching, and more.
To get you started, in addition to the core Middleware
class, we’ve added a few starting templates for common middleware patterns:
fastmcp.server.middleware.logging
: Logs every request and notification.fastmcp.server.middleware.error_handling
: Catch and retry errors.fastmcp.server.middleware.rate_limiting
: Limits the rate of requests.fastmcp.server.middleware.timing
: Basic performance monitoring.
Check out the full middleware documentation to see what’s possible.
But Wait, There’s More
FastMCP 2.9 is a huge release, and it also includes one highly-requested feature: server-side type conversion for prompt arguments.
The MCP spec requires all prompt arguments to be strings. This has been a persistent developer pain point. Why? Because the Python function that generates those prompts often needs structured data to perform business logic, such as a list of IDs to look up, a dictionary of configuration, or some filter criteria. This has forced developers to litter their prompt logic with json.loads()
and pray that the agent provides a compatible input.
Not anymore.
With FastMCP 2.9, you can define your prompt functions with the native Python types you’d expect. FastMCP automatically handles the conversion from string to type on the server. Crucially, it also enhances the prompt’s description to show clients the expected JSON schema format, making it clear how to provide structured data. And to complete the story, FastMCP Clients
will now automatically serialize non-string arguments for you.
from fastmcp import FastMCPimport inspect
mcp = FastMCP()
@mcp.promptdef analyze_users( user_ids: list[int], # Auto-converted from JSON! analysis_type: str,) -> str: """Generate analysis prompt using loaded user data.""" users = [] for user_id in user_ids: user = db.get_user(user_id) # pseudocode users.append(f"- {user_id}: {user.name}, {user.metrics}")
user_data = "\n".join(users)
return inspect.cleandoc( f""" Analyze these users for {analysis_type} insights:
{user_data}
Provide actionable recommendations. """ )
An MCP client would call this with {"user_ids": "[1, 2, 3]", "analysis_type": "performance"}
, but the MCP server would receive a clean list
and str
. It’s a small change that removes a huge amount of friction, especially when prompts are doing more than just string interpolation.
FastMCP’s implementation of this feature is fully MCP spec-compliant, but because there is no formal way to describe the expected JSON Schema format of a prompt argument, it’s possible that some clients will choose to ignore it. As with all agentic users, performance will depend on clarity of your instructions.
From Protocol to Framework
With features like middleware and automatic type conversion, FastMCP is evolving beyond a simple high-level protocol implementation. It’s becoming a true application framework: an opinionated, high-level toolkit for building sophisticated, production-ready MCP applications. Our goal remains the same: to provide the simplest path to production.
- Upgrade:
uv add fastmcp
orpip install fastmcp --upgrade
- Explore: Dig into the new Middleware and Prompts documentation.
- Contribute: Check out the code and examples on GitHub.
Happy engineering!