Add cross-cutting functionality to your MCP server with middleware that intercepts and modifies requests and responses.
New in version 2.9.0Middleware adds behavior that applies across multiple operations—authentication, logging, rate limiting, or request transformation—without modifying individual tools or resources.
MCP middleware is a FastMCP-specific concept and is not part of the official MCP protocol specification.
MCP middleware forms a pipeline around your server’s operations. When a request arrives, it flows through each middleware in order—each can inspect, modify, or reject the request before passing it along. After the operation completes, the response flows back through the same middleware in reverse order.
Copy
Request → Middleware A → Middleware B → Handler → Middleware B → Middleware A → Response
Middleware executes in the order added to the server. The first middleware runs first on the way in and last on the way out:
Copy
from fastmcp import FastMCPfrom fastmcp.server.middleware.error_handling import ErrorHandlingMiddlewarefrom fastmcp.server.middleware.rate_limiting import RateLimitingMiddlewarefrom fastmcp.server.middleware.logging import LoggingMiddlewaremcp = FastMCP("MyServer")mcp.add_middleware(ErrorHandlingMiddleware()) # 1st in, last outmcp.add_middleware(RateLimitingMiddleware()) # 2nd in, 2nd outmcp.add_middleware(LoggingMiddleware()) # 3rd in, first out
This ordering matters. Place error handling early so it catches exceptions from all subsequent middleware. Place logging late so it records the actual execution after other middleware has processed the request.
When using mounted servers, middleware behavior follows a clear hierarchy:
Parent middleware runs for all requests, including those routed to mounted servers
Mounted server middleware only runs for requests handled by that specific server
Copy
from fastmcp import FastMCPfrom fastmcp.server.middleware.logging import LoggingMiddlewareparent = FastMCP("Parent")parent.add_middleware(AuthMiddleware()) # Runs for ALL requestschild = FastMCP("Child")child.add_middleware(LoggingMiddleware()) # Only runs for child's toolsparent.mount(child, namespace="child")
Requests to child_tool flow through the parent’s AuthMiddleware first, then through the child’s LoggingMiddleware.
Rather than processing every message identically, FastMCP provides specialized hooks at different levels of specificity. Multiple hooks fire for a single request, going from general to specific:
Level
Hooks
Purpose
Message
on_message
All MCP traffic (requests and notifications)
Type
on_request, on_notification
Requests expecting responses vs fire-and-forget
Operation
on_call_tool, on_read_resource, on_get_prompt, etc.
Specific MCP operations
When a client calls a tool, the middleware chain processes on_message first, then on_request, then on_call_tool. This hierarchy lets you target exactly the right scope—use on_message for logging everything, on_request for authentication, and on_call_tool for tool-specific behavior.
New in version 2.13.0Called when a client connects and initializes the session. This hook cannot modify the initialization response.
Copy
from mcp import McpErrorfrom mcp.types import ErrorDataasync def on_initialize(self, context: MiddlewareContext, call_next): client_info = context.message.params.get("clientInfo", {}) client_name = client_info.get("name", "unknown") # Reject before call_next to send error to client if client_name == "blocked-client": raise McpError(ErrorData(code=-32000, message="Client not supported")) await call_next(context) print(f"Client {client_name} initialized")
Returns:None — The initialization response is handled internally by the MCP protocol.
Raising McpError after call_next() will only log the error, not send it to the client. The response has already been sent. Always reject beforecall_next().
New in version 2.13.1The MCP session may not be available during certain phases like initialization. Check before accessing session-specific attributes:
Copy
async def on_request(self, context: MiddlewareContext, call_next): ctx = context.fastmcp_context if ctx.request_context: # MCP session available session_id = ctx.session_id request_id = ctx.request_id else: # Session not yet established (e.g., during initialization) # Use HTTP helpers if needed from fastmcp.server.dependencies import get_http_headers headers = get_http_headers() return await call_next(context)
For HTTP-specific data (headers, client IP) when using HTTP transports, see HTTP Requests.
from fastmcp.server.middleware.logging import LoggingMiddleware, StructuredLoggingMiddleware
LoggingMiddleware provides human-readable request and response logging. StructuredLoggingMiddleware outputs JSON-formatted logs for aggregation tools like Datadog or Splunk.
from fastmcp.server.middleware.timing import TimingMiddleware, DetailedTimingMiddleware
TimingMiddleware logs execution duration for all requests. DetailedTimingMiddleware provides per-operation timing with separate tracking for tools, resources, and prompts.
Copy
from fastmcp import FastMCPfrom fastmcp.server.middleware.timing import TimingMiddlewaremcp = FastMCP("MyServer")mcp.add_middleware(TimingMiddleware())
from fastmcp.server.middleware import PingMiddleware
Keeps long-lived connections alive by sending periodic pings.
Copy
from fastmcp import FastMCPfrom fastmcp.server.middleware import PingMiddlewaremcp = FastMCP("MyServer")mcp.add_middleware(PingMiddleware(interval_ms=5000))
Parameter
Type
Default
Description
interval_ms
int
30000
Ping interval in milliseconds
The ping task starts on the first message and stops automatically when the session ends. Most useful for stateful HTTP connections; has no effect on stateless connections.
from fastmcp.server.middleware.tool_injection import ( ToolInjectionMiddleware, PromptToolMiddleware, ResourceToolMiddleware)
ToolInjectionMiddleware dynamically injects tools during request processing. PromptToolMiddleware and ResourceToolMiddleware provide compatibility layers for clients that cannot list or access prompts and resources directly—they expose those capabilities as tools.
When the built-in middleware doesn’t fit your needs—custom authentication schemes, domain-specific logging, or request transformation—subclass Middleware and override the hooks you need.
List operations return FastMCP objects that you can filter before they reach the client. When filtering list results, also block execution in the corresponding operation hook to maintain consistency:
Copy
from fastmcp.server.middleware import Middleware, MiddlewareContextfrom fastmcp.exceptions import ToolErrorclass PrivateToolFilter(Middleware): async def on_list_tools(self, context: MiddlewareContext, call_next): tools = await call_next(context) return [tool for tool in tools if "private" not in tool.tags] async def on_call_tool(self, context: MiddlewareContext, call_next): if context.fastmcp_context: tool = await context.fastmcp_context.fastmcp.get_tool(context.message.name) if "private" in tool.tags: raise ToolError("Tool not found") return await call_next(context)
New in version 2.11.0Middleware can store state that tools access later through the FastMCP context.
Copy
from fastmcp.server.middleware import Middleware, MiddlewareContextclass UserMiddleware(Middleware): async def on_request(self, context: MiddlewareContext, call_next): # Extract user from headers (HTTP transport) from fastmcp.server.dependencies import get_http_headers headers = get_http_headers() or {} user_id = headers.get("x-user-id", "anonymous") # Store for tools to access if context.fastmcp_context: context.fastmcp_context.set_state("user_id", user_id) return await call_next(context)
Tools retrieve the state:
Copy
from fastmcp import FastMCP, Contextmcp = FastMCP("MyServer")@mcp.tooldef get_user_data(ctx: Context) -> str: user_id = ctx.get_state("user_id") return f"Data for user: {user_id}"