Middleware
Middleware provides a powerful way to intercept and augment the execution of tool handlers, resource requests, and prompt retrievals. By using middleware, you can implement cross-cutting concerns like logging, authentication, rate limiting, and request validation without cluttering your core business logic.
Overview
Middleware functions sit between the server’s request dispatcher and the final handler. They can:
Inspect and modify incoming request parameters.
Perform operations before and after the handler executes.
Short-circuit the request by returning a result, or refuse it by throwing, before the handler is even called.
Catch exceptions thrown by inner middleware and by resource and prompt handlers.
Inject additional context or metadata.
Registering Middleware
Middleware is registered using the server.use() method. A middleware function must match the mcp::Middleware signature:
using Middleware = std::function<Task<nlohmann::json>(
Context& ctx,
const nlohmann::json& params,
TypeErasedHandler next
)>;
Where:
ctx: The request context (provides logging, progress, etc.).params: The raw JSON parameters of the request.next: A functional object representing the next step in the chain (either another middleware or the final handler).
The following example shows how to register multiple middleware layers:
// Middleware A: Outer middleware (registered first)
// Logs before and after the next middleware/handler
server.use([](Context& ctx, const nlohmann::json& params,
TypeErasedHandler next) -> Task<nlohmann::json> {
std::cout << "[Middleware A] Before handler\n";
co_await ctx.log_info("Middleware A: before");
auto result = co_await next(ctx, params);
std::cout << "[Middleware A] After handler\n";
co_await ctx.log_info("Middleware A: after");
co_return result;
});
// Middleware B: Middle middleware (registered second)
// Logs before and after, and can inspect params
server.use([](Context& ctx, const nlohmann::json& params,
TypeErasedHandler next) -> Task<nlohmann::json> {
std::cout << "[Middleware B] Before handler\n";
co_await ctx.log_info("Middleware B: before");
auto result = co_await next(ctx, params);
std::cout << "[Middleware B] After handler\n";
co_await ctx.log_info("Middleware B: after");
co_return result;
});
// Middleware C: Inner middleware (registered third)
// This middleware can short-circuit by checking params
server.use([](Context& ctx, const nlohmann::json& params,
TypeErasedHandler next) -> Task<nlohmann::json> {
std::cout << "[Middleware C] Before handler\n";
co_await ctx.log_info("Middleware C: before");
// Check if params contain a "block" flag to demonstrate short-circuiting
if (params.contains("arguments") && params.at("arguments").contains("block") &&
params.at("arguments").at("block").is_boolean() &&
params.at("arguments").at("block").get<bool>()) {
std::cout << "[Middleware C] Short-circuiting: block flag detected\n";
co_await ctx.log_info("Middleware C: short-circuit");
// A throw from middleware is answered as a JSON-RPC error (-32603), which
// call_tool() raises as McpError.
throw std::runtime_error("Request blocked by middleware C");
}
auto result = co_await next(ctx, params);
std::cout << "[Middleware C] After handler\n";
co_await ctx.log_info("Middleware C: after");
co_return result;
});
Execution Order
Middleware follows an “onion” model. The first middleware registered with server.use() becomes the outermost layer. It is the first to execute on the way “in” and the last to execute on the way “out”.
For example, if you register Middleware A then Middleware B:
Astarts.Acallsnext(), which triggersB.Bstarts.Bcallsnext(), which triggers the actual Handler.Handler executes and returns a result to
B.Bfinishes and returns a result toA.Afinishes and returns the final result to the client.
Short-circuiting
A middleware can choose not to call next(). This is known as short-circuiting. It is useful for security checks, caching, or validation where you want to return an error or a cached response without involving the final handler.
When a middleware short-circuits, the execution chain is halted. Control immediately passes back to the previous middleware in the chain (if any) or back to the server’s dispatcher.
In the example below, Middleware C checks for a “block” flag and throws if it’s present, so the client receives a JSON-RPC Internal error (-32603) carrying the exception’s message:
// This middleware can short-circuit by checking params
server.use([](Context& ctx, const nlohmann::json& params,
TypeErasedHandler next) -> Task<nlohmann::json> {
std::cout << "[Middleware C] Before handler\n";
co_await ctx.log_info("Middleware C: before");
// Check if params contain a "block" flag to demonstrate short-circuiting
if (params.contains("arguments") && params.at("arguments").contains("block") &&
params.at("arguments").at("block").is_boolean() &&
params.at("arguments").at("block").get<bool>()) {
std::cout << "[Middleware C] Short-circuiting: block flag detected\n";
co_await ctx.log_info("Middleware C: short-circuit");
// A throw from middleware is answered as a JSON-RPC error (-32603), which
// call_tool() raises as McpError.
throw std::runtime_error("Request blocked by middleware C");
}
auto result = co_await next(ctx, params);
std::cout << "[Middleware C] After handler\n";
co_await ctx.log_info("Middleware C: after");
co_return result;
});
Composition Patterns
Middleware can be composed to build complex request processing pipelines. By combining several small, single-purpose middleware functions, you can create highly specialized server behaviors while keeping the code maintainable.
Common patterns include:
Logging & Metrics: An outer middleware that logs the start and end of every request, measuring execution time. This provides visibility into server performance and usage patterns.
Authentication: A middleware that validates API keys, session tokens, or OAuth tokens before allowing access to sensitive tools. This ensures that only authorized clients can trigger certain actions.
Rate Limiting: A middleware that tracks request frequency from clients and returns a “Too Many Requests” error if they exceed their quota.
Validation: A middleware that ensures request parameters conform to additional business rules beyond what JSON Schema can express (e.g., checking if a file exists or if a value is within a dynamic range).
Error Mapping: A middleware that catches exceptions from resource and prompt handlers, or from inner middleware, and rethrows them with a clearer message. A tool handler’s exception never reaches middleware; it arrives as an
isErrorresult.Tracing: A middleware that injects tracing headers or spans into the context to enable distributed tracing across multiple services.
Common Examples
Authentication
You can create middleware that checks a specific parameter. For the current OAuth and HTTP bearer-authentication building blocks, limits, and recommended security boundary, see OAuth.
server.use([](Context& ctx, const nlohmann::json& params, TypeErasedHandler next) -> Task<nlohmann::json> {
if (!is_authorized(params)) {
throw std::runtime_error("Unauthorized access");
}
co_return co_await next(ctx, params);
});
An exception thrown by middleware is answered as a JSON-RPC
Internal error (-32603) for every request kind, tools/call included:
middleware decides whether a call may proceed at all, which is a protocol-level
answer rather than a tool outcome.
Throw to refuse a request. Middleware registered with server.use() wraps
tool calls, resource reads and prompt requests alike, and Context does not
say which kind it is handling. A CallToolResult returned without calling
next(), such as make_tool_error_result(...), is passed through
unchanged, but only tools/call expects that shape: a resources/read or
prompts/get would send it to the client as a successful result. The bundled
make_auth_middleware() answers a refused tools/call with a tool error
result and a refused resources/read or prompts/get with -32603; it
tells the two apart through a type internal to the SDK, which custom middleware
cannot use.
Middleware runs outside the tool handler’s exception guard, so a tool handler’s
exception never reaches it: by the time next() returns, the exception has
already become a CallToolResult with isError: true. Middleware that
counts or logs tool failures should inspect that flag rather than catch.
Logging
Middleware can use the Context to send logs back to the client during request execution.
server.use([](Context& ctx, const nlohmann::json& params, TypeErasedHandler next) -> Task<nlohmann::json> {
co_await ctx.log_info("Request started");
auto result = co_await next(ctx, params);
co_await ctx.log_info("Request finished");
co_return result;
});
Best Practices
Keep it focused: Each middleware should handle exactly one concern. This makes your code easier to test, debug, and reuse across different servers.
Always co_await next: Unless you are explicitly short-circuiting, ensure you
co_await next(ctx, params)to maintain the execution chain. Forgetting to call or awaitnextwill cause the request to hang or terminate prematurely.Handle exceptions: If a middleware performs risky operations (like database queries or network calls), catch their failures inside the middleware. An exception that leaves a middleware is answered as
-32603. Wrappingnext()in a try-catch sees exceptions from inner middleware, including a refusal frommake_auth_middleware(), and from resource and prompt handlers, but not tool handler exceptions, which arrive asisErrorresults.Mind the order: Register global concerns (like logging or tracing) first, and more specific concerns (like tool-specific validation or authentication) later. Remember that the first registered middleware is the outermost.
Use the Context: Leverage the
ctxobject for logging information relevant to the middleware’s task. This helps in auditing and troubleshooting request flows in production environments.Avoid Heavy lifting: Middleware should be relatively lightweight. If a middleware needs to perform a very expensive operation, consider if it should be part of the tool handler itself or if it can be optimized with caching.
Thread Safety: Ensure your middleware logic is thread-safe, as it may be called from multiple concurrent request handlers within the Boost.Asio io_context. Avoid sharing non-thread-safe state unless protected by mutexes or strands.
See Also
Tools for details on tool handlers.
Server Context for information on using the request context.
OAuth for an advanced use case involving authentication middleware.