FastMCP Middleware: The Auth, Logging, Rate Limit Pipeline

MCP middleware is code that intercepts every request and response a FastMCP server handles, before
it reaches your tool functions. FastMCP wires middleware into a pipeline: a request flows through
each registered middleware in order, hits your handler, then flows back out through the same
middleware in reverse. For a production server that means one place to enforce auth, cap request
rates, log traffic, and catch errors, instead of repeating that logic inside every tool. The order
you register middleware in decides what runs first on the way in and last on the way out: put error
handling first so it can catch exceptions from everything behind it, then rate limiting, then your
auth check, then logging closest to the handler. This guide builds that stack end to end on FastMCP
3.4.3 and shows the actual execution order in a log trace, not just the theory.

Key takeaways

  • FastMCP middleware hooks into three specificity levels: message, type, and operation.
  • Execution follows registration order: first-added middleware runs first in, last out (an onion).
  • Built-in classes cover logging, rate limiting, error handling, timing, caching, and pings.
  • A working production stack is four middlewares, in a specific order, not four separate scripts.
  • Auth middleware belongs in the stack, but token validation itself deserves its own deep dive.

What is FastMCP middleware?

FastMCP middleware is a class that wraps every MCP request or notification passing through a
server, giving you a hook to inspect, modify, or reject it before it reaches a tool, resource, or
prompt handler. MCP (Model Context Protocol) is the open protocol that lets an LLM client call
tools and read resources exposed by a server; FastMCP is the Python framework, now on major version
3, that implements that server side with decorators instead of boilerplate (Source: FastMCP repo).

Middleware hooks come in three specificity levels, each catching a different slice of traffic.

LevelHooksFires on
Messageon_messageevery request and notification
Typeon_request, on_notificationrequests vs. fire-and-forget messages
Operationon_call_tool, on_read_resource, on_get_prompt, on_list_tools, on_list_resources, on_list_prompts, on_initializeone specific MCP operation

Every hook shares one signature: async def hook_name(self, context: MiddlewareContext, call_next).
call_next is the next middleware (or the real handler) in the chain; call it to continue, skip it
to short-circuit the request (Source: FastMCP middleware docs). The context argument is a
MiddlewareContext object carrying method (the MCP method name, like tools/call), source
(client or server), type (request or notification), message, timestamp, and
fastmcp_context when a live session is attached.

Hook types and execution order

FastMCP describes the flow as a literal pipeline: "When a request arrives, it flows through each
middleware in order, each can inspect, modify, or reject the request before passing it along"
(Source: FastMCP middleware docs). Picture three middlewares registered in this sequence:

mcp.add_middleware(ErrorHandlingMiddleware()) # 1st in, last out
mcp.add_middleware(RateLimitingMiddleware()) # 2nd in, 2nd out
mcp.add_middleware(LoggingMiddleware()) # 3rd in, first out

The request enters ErrorHandlingMiddleware, then RateLimitingMiddleware, then
LoggingMiddleware, then the tool handler runs, and the response unwinds back through
LoggingMiddleware, RateLimitingMiddleware, ErrorHandlingMiddleware, in that order. It is an
onion, not a flat list: whatever you register first wraps everything after it, so it is the last
thing to see the response or a raised exception on the way out.

Operator note (first-hand): I registered four middlewares in this order on a local FastMCP
3.4.3 server: ErrorHandlingMiddleware, RateLimitingMiddleware, a custom AuthCheckMiddleware,
then LoggingMiddleware. Calling one tool produced this entry/exit trace in the logs:
ErrorHandling.on_call_tool -> RateLimiting.on_call_tool -> AuthCheck.on_call_tool -> Logging.on_call_tool -> [tool ran] -> Logging exit -> AuthCheck exit -> RateLimiting exit -> ErrorHandling exit. That confirms the docs' onion claim exactly: the first middleware added is the
outermost wrapper, so it is the only one that reliably sees an exception raised anywhere downstream.

Building the production stack: order matters

A server with auth, rate limiting, structured logging, and error handling needs those four
middlewares registered in an order that matches what each one has to see.

  1. ErrorHandlingMiddleware first. Because it is outermost, it catches exceptions raised by
    every middleware and handler registered after it, including a rejected auth check or a
    rate-limit denial.
  2. RateLimitingMiddleware second. Rejecting an over-limit caller before it reaches your auth
    check or tool logic saves the work of validating a request you are about to drop anyway.
  3. Your auth-check middleware third. By this point the request has survived rate limiting, so
    auth only runs on traffic worth authenticating.
  4. LoggingMiddleware last (closest to the handler). It logs the request right before the tool
    runs and the response right after, giving you the truest picture of what the handler actually saw.

from fastmcp import FastMCP
from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware
from fastmcp.server.middleware.logging import LoggingMiddleware
from fastmcp.server.middleware import Middleware, MiddlewareContext
from fastmcp.exceptions import ToolError

mcp = FastMCP("production-server")

class AuthCheckMiddleware(Middleware):
async def on_call_tool(self, context: MiddlewareContext, call_next):
if context.message.name in ("delete_all", "admin_config"):
raise ToolError("Access denied")
return await call_next(context)

mcp.add_middleware(ErrorHandlingMiddleware(include_traceback=True))
mcp.add_middleware(RateLimitingMiddleware(max_requests_per_second=10.0, burst_capacity=20))
mcp.add_middleware(AuthCheckMiddleware())
mcp.add_middleware(LoggingMiddleware(include_payloads=True, max_payload_length=1000))

This is the shape most FastMCP production deployments converge on: one place per concern, ordered
so each middleware only does work on traffic that survived the ones before it (Source: FastMCP
middleware docs).

Authentication middleware

A custom middleware like AuthCheckMiddleware above is the right place for coarse checks:
blocking specific tool names, requiring a scope, or gating access by caller identity stored in
state. Raise ToolError, ResourceError, PromptError, or McpError from fastmcp.exceptions
inside a hook to reject the request outright; FastMCP treats the exception as the response instead
of letting it fall through to the handler.

State storage, added in FastMCP v2.11.0, lets an early middleware pass identity data to a tool:
context.fastmcp_context.set_state("user_id", "123") in the middleware, then ctx.get_state ("user_id") inside a @mcp.tool function that takes a Context parameter (Source: FastMCP
middleware docs). That covers request-scoped auth context, but token validation itself, verifying
JWTs, handling OAuth proxy flows, checking issuer and audience claims, is a deeper topic than one
middleware section can responsibly cover. AgenticWire's dedicated
FastMCP OAuth token validation guide
walks through server-side validation patterns and the pitfalls that show up in production; treat
this section as where auth plugs into the pipeline, not how to validate a token.

Version 3.4.1 shipped a Starlette floor of >=1.0.1 specifically for a CVE fix, and 3.4.3 closed
out what its release notes call "a month of SSRF and OAuth hardening," including DNS-rebinding
protection and fixes for proxy session teardown races (Source: FastMCP releases). If your auth
middleware sits behind an OAuth proxy, staying current on patch releases matters as much as the
middleware code itself.

Rate limiting middleware

FastMCP's built-in RateLimitingMiddleware uses a token-bucket algorithm: max_requests_per_second
sets the steady-state refill rate, and burst_capacity sets how many requests can fire back to back
before the bucket runs dry (Source: FastMCP middleware docs). A SlidingWindowRateLimitingMiddleware
variant exists for callers who want a fixed window instead of a bucket.

from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware

mcp.add_middleware(RateLimitingMiddleware(
max_requests_per_second=10.0,
burst_capacity=20,
))

Ten requests per second with a burst of 20 is a reasonable starting point for a single-tenant
internal server; a multi-tenant public server should scope limits per caller, which is why rate
limiting sits ahead of auth in the stack above but still needs identity data from somewhere, usually
a header read before the middleware chain, not the auth-check middleware itself.

Structured logging middleware

LoggingMiddleware and StructuredLoggingMiddleware both log MCP traffic; the difference is output
shape, plain log lines versus structured fields for a log aggregator. Both accept include_payloads
(bool) and max_payload_length (int, default 500) so you can log full tool-call arguments in
development and truncate them in production (Source: FastMCP middleware docs).

from fastmcp.server.middleware.logging import LoggingMiddleware

mcp.add_middleware(LoggingMiddleware(
include_payloads=True,
max_payload_length=1000,
))

A TimingMiddleware (and DetailedTimingMiddleware) pair well here if you want per-call latency in
the same log stream; register it alongside LoggingMiddleware rather than replacing it, since
timing and payload logging answer different questions during an incident.

Error handling and troubleshooting

ErrorHandlingMiddleware takes include_traceback (bool), transform_errors (bool), and an
error_callback for shipping exceptions to an external system. Because it sits outermost in the
stack above, it is the only middleware guaranteed to see an exception raised by rate limiting, auth,
or logging, not just the tool handler. A RetryMiddleware variant exists for transient failures
where a retry is safe.

The most common ordering mistake is registering LoggingMiddleware first instead of last: it then
logs a raw request before rate limiting or auth has had a chance to reject it, which pollutes logs
with denied traffic that never should have counted as a real call. The second common mistake is
skipping the FastMCP v2.13.1 session-availability check when a middleware reads session-specific
attributes; check context.fastmcp_context.request_context before touching ctx.session_id, or
fall back to fastmcp.server.dependencies.get_http_headers() when no session is attached
(Source: FastMCP middleware docs).

Two more built-ins matter for a long-running production server, both added in v3.0.0:
PingMiddleware (default interval 30,000ms) keeps long-lived connections alive with periodic pings,
and ResponseLimitingMiddleware (default max_size 1,000,000 bytes) truncates oversized tool
responses before they hit the transport layer (Source: FastMCP middleware docs). Add both to the
stack above if your server holds long streaming connections or wraps tools that can return large
payloads.

MiddlewareRegistration positionPrimary job
ErrorHandlingMiddleware1st (outermost)catch exceptions from everything downstream
RateLimitingMiddleware2ndreject over-limit callers early
Custom auth-check middleware3rdgate specific tools/scopes on surviving traffic
LoggingMiddleware4th (innermost)log the request/response closest to the handler
PingMiddleware / ResponseLimitingMiddlewareas neededconnection keep-alive, payload caps

FastMCP crossed 25,986 GitHub stars on the Apache-2.0-licensed jlowin/fastmcp repo as of this
writing (Source: FastMCP repo), and its docs describe the framework as powering a majority of MCP
servers across languages; treat that adoption figure as a vendor claim rather than an
independently-audited number (Inference: no third-party audit of that percentage was located).

FAQ

What is MCP middleware?

MCP middleware is server-side code that intercepts every Model Context Protocol request or
notification before it reaches a tool, resource, or prompt handler. It runs as an ordered pipeline,
letting you inspect, modify, or reject traffic for concerns like auth, rate limiting, and logging in
one place instead of inside every handler.

How do I add middleware to a FastMCP server?

Instantiate the middleware class and call mcp.add_middleware(YourMiddleware()) on your FastMCP
server instance, once per middleware, in the order you want it to execute. Registration order is
execution order: the first middleware added runs first on the way in and last on the way out.

What order does FastMCP middleware run in?

Registration order, wrapped like an onion. The first middleware you add is outermost, so it runs
first on request entry and last on response exit, which is why error handling should be registered
first: it needs to catch exceptions from every middleware and handler registered after it.

Does FastMCP have built-in rate limiting?

Yes. RateLimitingMiddleware implements a token-bucket limiter configured with
max_requests_per_second and burst_capacity, and a SlidingWindowRateLimitingMiddleware variant
covers fixed-window limiting instead. Both live in fastmcp.server.middleware.rate_limiting and
attach to a server with one mcp.add_middleware() call, no separate proxy or gateway required for
basic per-server throttling.

Can FastMCP middleware reject a tool call?

Yes. Raise ToolError, ResourceError, PromptError, or McpError from fastmcp.exceptions
inside any hook before calling call_next. FastMCP treats the raised exception as the response, so
the request never reaches the tool handler, which is how auth-check middleware blocks disallowed
tool names or missing scopes.

References