Middleware adds behavior that applies across multiple operations—authentication, logging, rate limiting, or request transformation—without modifying individual tools or resources.
Overview
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.
This bidirectional flow means middleware can:
- Pre-process: Validate authentication, log incoming requests, check rate limits
- Post-process: Transform responses, record timing metrics, handle errors consistently
The key decision point is call_next(context). Calling it continues the chain; not calling it stops processing entirely.
Execution Order
Middleware executes in the order added to the server. The first middleware runs first on the way in and last on the way 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.
Server Composition
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
Requests to child_tool flow through the parent’s AuthMiddleware first, then through the child’s LoggingMiddleware.
Middleware-stored state does not automatically cross mount boundaries. If AuthMiddleware on the parent calls ctx.set_state("user_id", ...), a tool on the child server calling ctx.get_state("user_id") will get None — each FastMCP instance owns its own session state store. To share state across the mount, either pass the same session_state_store to both servers or use serializable=False for request-scoped values. See Session State for details.
Hooks
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.
What middleware sees
Dispatch begins in the SDK’s middleware layer — the single point every inbound message passes through. As a result, on_message, on_request, and on_notification observe every message a client sends, including the ones that never reach a tool, resource, or prompt handler:
- Notifications such as
notifications/cancelled,notifications/initialized, andnotifications/progressreachon_messageandon_notification. - Cancellations are observed as a
notifications/cancelledmessage. The connection applies the cancellation itself and then hands the notification to your middleware. - Malformed or unroutable requests—an unknown method, or a
tools/callwhose params fail validation before the tool runs—reachon_messageandon_requestas a raised error propagating throughcall_next, so logging and error-handling middleware record them.
The operation hooks (on_call_tool, on_list_tools, and the rest) fire exactly once per request, and their call_next still returns the typed component result—a ToolResult, a list[Tool], and so on—so a tool exception propagates through on_call_tool, on_request, and on_message exactly where error, logging, and timing middleware expect it.
Multi-round tool calls
A guard tool asks the client for input by returning an InputRequiredResult (see Elicitation on the modern protocol). Each round of a multi-round call is a complete request→response cycle that runs the full middleware chain: on_call_tool fires once per round, and on an asking round call_next returns the ask as that round’s ordinary result value—an InputRequiredToolResult, a ToolResult subclass. Nothing is raised and nothing is held open, so default middleware completes normally on every round (logging logs the ask, timing times it, error handling does not fire—an ask is a legitimate result, not an error). Middleware that needs to treat an ask differently identifies it with an isinstance(result, InputRequiredToolResult) check; see Middleware and multi-round calls for a worked example.
Hook Signature
Every hook follows the same pattern:
Parameters:
context—MiddlewareContextcontaining request informationcall_next— Async function to continue the middleware chain
Returns: The appropriate result type for the hook (varies by operation).
MiddlewareContext
The context parameter provides access to request details:
| Attribute | Type | Description |
|---|---|---|
method | str | MCP method name (e.g., "tools/call") |
source | str | Origin: "client" or "server" |
type | str | Message type: "request" or "notification" |
message | object | The MCP message data |
timestamp | datetime | When the request was received |
fastmcp_context | Context | FastMCP context object (if available) |
Message Hooks
on_message
Called for every MCP message—both requests and notifications.
Use for: Logging, metrics, or any cross-cutting concern that applies to all traffic.
on_request
Called for MCP requests that expect a response.
Use for: Authentication, authorization, request validation.
on_notification
Called for fire-and-forget MCP notifications.
Use for: Event logging, async side effects.
Operation Hooks
on_call_tool
Called when a tool is executed. The context.message contains name (tool name) and arguments (dict).
Returns: Tool execution result or raises ToolError.
on_read_resource
Called when a resource is read. The context.message contains uri (resource URI).
Returns: Resource content.
on_get_prompt
Called when a prompt is retrieved. The context.message contains name (prompt name) and arguments (dict).
Returns: Prompt messages.
on_list_tools
Called when listing available tools. Returns a list of FastMCP Tool objects before MCP conversion.
Returns: list[Tool] — Can be filtered before returning to client.
on_list_resources
Called when listing available resources. Returns FastMCP Resource objects.
Returns: list[Resource]
on_list_resource_templates
Called when listing resource templates.
Returns: list[ResourceTemplate]
on_list_prompts
Called when listing available prompts.
Returns: list[Prompt]
on_initialize
Called when a client connects and initializes the session. Middleware can reject the client before call_next() raises an error response, or inspect and modify the InitializeResult after call_next() returns.
The request params carry the identity the client declared for itself on client_info, which makes this the natural place to gate access by client. Note that these fields are snake_case: the MCP wire format spells it clientInfo, but the Python model exposes client_info and treats the camelCase form as a serialization alias only.
Returns: InitializeResult | None — The value you return is what gets serialized to the client, so modifying the result from call_next() changes what the client receives, including fields like instructions and server_info.
on_discover
Called when a modern client negotiates through server/discover. Core discovery responses are returned as DiscoverResult; extension-owned result types are returned as dictionaries and should be passed through unless the middleware handles that extension.
Fields such as supported_versions, capabilities, and cache policy should only be changed when the server’s public behavior also changes.
Raw Handler
For complete control over all messages, override __call__ instead of individual hooks:
This bypasses the hook dispatch system entirely. Use when you need uniform handling regardless of message type.
Session Availability
The MCP session may not be available during certain phases like initialization. Check before accessing session-specific attributes:
For HTTP-specific data (headers, client IP) when using HTTP transports, see HTTP Request.
Built-in Middleware
FastMCP includes production-ready middleware for common server concerns.
Logging
LoggingMiddleware provides human-readable request and response logging. StructuredLoggingMiddleware outputs JSON-formatted logs for aggregation tools like Datadog or Splunk.
| Parameter | Type | Default | Description |
|---|---|---|---|
include_payloads | bool | False | Log request/response content |
max_payload_length | int | 1000 | Truncate payloads beyond this length |
logger | Logger | module logger | Custom logger instance |
Timing
TimingMiddleware logs execution duration for all requests. DetailedTimingMiddleware provides per-operation timing with separate tracking for tools, resources, and prompts.
Caching
Caches tool calls, resource reads, and list operations with TTL-based expiration.
Each operation type can be configured independently using settings classes:
| Settings Class | Configures |
|---|---|
ListToolsSettings | on_list_tools caching |
CallToolSettings | on_call_tool caching |
ListResourcesSettings | on_list_resources caching |
ReadResourceSettings | on_read_resource caching |
ListPromptsSettings | on_list_prompts caching |
GetPromptSettings | on_get_prompt caching |
Each settings class accepts:
enabled— Enable/disable caching for this operationttl— Time-to-live in secondsincluded_*/excluded_*— Whitelist or blacklist specific items
For persistence or distributed deployments, configure a different storage backend:
See Storage Backends for complete options.
Rate Limiting
RateLimitingMiddleware uses a token bucket algorithm allowing controlled bursts. SlidingWindowRateLimitingMiddleware provides precise time-window rate limiting without burst allowance.
| Parameter | Type | Default | Description |
|---|---|---|---|
max_requests_per_second | float | 10.0 | Sustained request rate |
burst_capacity | int | 20 | Maximum burst size |
get_client_id | Callable | None | Custom client identification |
For sliding window rate limiting:
Error Handling
ErrorHandlingMiddleware provides centralized error logging and transformation. RetryMiddleware automatically retries with exponential backoff for transient failures.
| Parameter | Type | Default | Description |
|---|---|---|---|
include_traceback | bool | False | Include stack traces in logs |
transform_errors | bool | True | Convert exceptions to MCP errors |
error_callback | Callable | None | Custom callback on errors |
For automatic retries:
Ping
Keeps long-lived connections alive by sending periodic pings.
| 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.
Response Limiting
Large tool responses can overwhelm LLM context windows or cause memory issues. You can add response-limiting middleware to enforce size constraints on tool outputs.
When a response exceeds the limit, the middleware extracts all text content, joins it together, truncates to fit within the limit, and returns a single TextContent block. For non-text responses, the serialized JSON is used as the text source.
| Parameter | Type | Default | Description |
|---|---|---|---|
max_size | int | 1_000_000 | Maximum response size in bytes (1MB default) |
truncation_suffix | str | "\n\n[Response truncated due to size limit]" | Suffix appended to truncated responses |
tools | list[str] | None | None | Limit only these tools (None = all tools) |
Combining Middleware
Order matters. Place middleware that should run first (on the way in) earliest:
Custom Middleware
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.
Override only the hooks relevant to your use case. Unoverridden hooks pass through automatically.
Denying Requests
Raise the appropriate error type to stop processing and return an error to the client.
| Operation | Error Type |
|---|---|
| Tool calls | ToolError |
| Resource reads | ResourceError |
| Prompt retrieval | PromptError |
| General requests | McpError |
Do not return error values or skip call_next() to indicate errors—raise exceptions for proper error propagation.
Modifying Requests
Change the message before passing it down the chain.
Modifying Responses
Transform results after the handler executes.
For more complex tool transformations, consider Transforms instead.
Filtering Lists
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:
Accessing Component Metadata
During execution hooks, component metadata (like tags) isn’t directly available. Look up the component through the server:
The same pattern works for resources and prompts:
Storing State
Middleware can store state that tools access later through the FastMCP context.
Tools retrieve the state:
See Request State for details.
Constructor Parameters
Initialize middleware with configuration:
Error Handling in Custom Middleware
Wrap call_next() to handle errors from downstream middleware and handlers.
Catching and not re-raising suppresses the error entirely. Usually you want to log and re-raise.
Audit and Event Records
A common need is to emit one structured record per tool call — for audit logs, policy decisions, or offline analysis — without wrapping individual tools or storing raw payloads. on_call_tool is the right place: it sees the call start, the resolved ToolResult (so it can detect empty or error results), the duration, and can deny the call before it runs.
Use OpenTelemetry when the goal is to export spans to an observability backend. Reach for a record like this when you want a self-contained, redacted audit trail — or to drive runtime decisions from the result.
Each record carries the fields downstream tools tend to need — tool name, call id, input schema hash, redacted arguments, result class (completed / empty / error / failed), and duration — while raw inputs and outputs stay out by default.
To make this a policy layer, deny inside the same hook before calling call_next:
Complete Example
Authentication middleware checking API keys for specific tools: