Skip to main content
Tools, resources, and prompts each describe one capability. Middleware describes a concern that cuts across all of them — logging every call, caching expensive responses, rejecting traffic that arrives too fast, cancelling work the client no longer wants. Rather than thread that logic through every handler, you register it once and FastMCP runs it around each request. Middleware never changes what a primitive is; a tool wrapped by three middleware is still the same tool, registered the same way. The chain wraps the dispatch to your handlers, so everything you register — including mounted, proxied, or transformed components — passes through it. You register middleware fluently with server.use(mw), or all at once through FastMCPOptions.middleware. The order you register in is the order requests flow through.

Hook levels

A middleware can intercept requests at three levels of granularity, and you implement only the ones you need. Each is an optional method on the Middleware interface. setup(server) runs once per Server instance, before any request arrives. This is where you register notification handlers — things that respond to client-initiated messages rather than wrapping a request. Calling server.use(mw) invokes setup immediately on the primary server so your handlers are live before the first connection, and each new HTTP session runs it again when its server is built. onRequest(ctx, next) is the coarse hook. It fires for every request that has no more-specific hook on that same middleware instance. Call next() to continue down the chain to the handler, and do your work before, after, or around that call. This is the right level for concerns that genuinely apply to everything — timing, global error handling, response-size limits. Per-method hooks — onCallTool, onListTools, onReadResource, onListResources, onListResourceTemplates, onGetPrompt, onListPrompts — target a single MCP method. When a middleware defines one, it takes precedence over that instance’s onRequest for the matching method. Reach for these when a concern only makes sense for one kind of request: rate-limiting tool calls but not list operations, or caching reads while leaving writes untouched. Not every method has a dedicated hook. resources/subscribe, resources/unsubscribe, and completion/complete have none, so they always reach onRequest — a middleware sees them at the coarse level or not at all.

Built-in middleware

The built-ins cover the concerns most servers need, so you compose them rather than write them. Each is a class you instantiate and pass to use(), and you choose which to layer on based on what the deployment requires. LoggingMiddleware records every request’s method, outcome, and elapsed time through a configurable emit function — the first thing to reach for when you want visibility into what a server is doing. CachingMiddleware holds responses in a TTL cache so repeated identical calls skip the handler entirely; its default cache key already partitions by the caller’s auth identity, so a cached result never crosses identities — caching covers the full contract. RateLimitingMiddleware enforces a fixed-window request budget and throws when a caller exceeds it, protecting expensive handlers from abuse. SizeLimitingMiddleware guards the other direction, throwing when a serialized response grows past a byte ceiling. ErrorNormalizationMiddleware gives uncaught tool errors a consistent shape, and CancellationMiddleware lets a client abort in-flight work it no longer needs. CachingMiddleware is safe under per-component auth with no tuning. The default key is method, plus an auth partition, plus the serialized params, so a result computed for one caller is never served to another. An anonymous request uses a single anon partition; an authenticated request is partitioned by the SHA-256 hash of its bearer token. Auth-filtered results such as a per-caller tools/list therefore stay correct out of the box. A custom CacheKeyFn replaces the default key entirely — the auth partition is not merged back in. So when your cached values depend on the caller and you supply a key function, fold identity in yourself, using the hashed bearer token exactly as the default does. Read the token from ctx.mcpContext.auth, and never place a raw token in a key.
Two kinds of request the cache never touches, whatever your key. It never serves or stores an input-required round — a retry re-sends byte-identical params, so a cache would replay the first round’s result forever and the flow could never finish. And it never caches the resources/subscribe or resources/unsubscribe control RPCs, because serving a cached acknowledgement would skip the real subscription change and later updates would never arrive. Caching covers the full key contract and these exclusions.

Error semantics

Two kinds of failure travel through the chain, and FastMCP keeps them distinct on purpose. A tool that fails its own work — a lookup miss, a downstream timeout — represents a tool error: the request was valid, but the operation didn’t succeed. The client should see that as a result with isError: true and decide what to do. A failure in the request machinery itself — a rate limit tripped, a malformed request, an exhausted resource — represents a protocol error, and the client should see a JSON-RPC error. The mechanism that enforces this is deliberate: the try/catch that converts a thrown error into an isError result sits outside the middleware chain, and it only catches errors that are not ProtocolError. So when any middleware throws a ProtocolError — rate limiting throwing InvalidRequest, size limiting throwing InternalError — it sails past that catch and reaches the client as a protocol error, exactly as intended. A plain Error thrown from a tool handler is caught and becomes an isError tool result. ErrorNormalizationMiddleware follows the same rule from the inside: its onCallTool converts ordinary errors to isError but explicitly re-throws ProtocolError, so wrapping it never downgrades a protocol error into a tool result. The practical consequence: throw ProtocolError when something is wrong with the request or the server, and throw or return a plain error when a tool’s work fails. The chain routes each to the right place automatically.

Writing middleware

A custom middleware is any object implementing the Middleware interface. Implement the narrowest hook that fits the concern — a per-method hook when it applies to one method, onRequest when it applies to all — and call next() to pass control down the chain. Code before next() runs on the way in; code after it runs on the way out.
timing.ts
Because middleware composes on top of the registration system, it works the same way regardless of where a component came from. A middleware you write sees mounted and proxied components and transformed ones identically to locally registered ones — they are all just requests flowing through the same chain. When a middleware needs to participate in client-initiated traffic rather than wrap a request, register a notification handler in setup instead of using a request hook; CancellationMiddleware works exactly this way, registering its cancellation handler in setup and aborting matching in-flight requests. Cancellation carries one era boundary worth knowing. CancellationMiddleware observes the legacy notifications/cancelled message, which covers stdio and legacy HTTP connections. A modern Streamable HTTP client signals cancellation by closing the request’s response stream instead of sending that notification, so this middleware never observes it — the client is correctly told the call was cancelled, but the handler keeps running to completion server-side. Unifying the middleware with the modern stream-close signal is tracked work, not yet implemented, so on the modern era treat a long-running handler as uncancellable by this middleware.