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: .. code-block:: cpp using Middleware = std::function( 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: .. literalinclude:: ../../examples/features/middleware.cpp :language: cpp :start-after: // ========== MIDDLEWARE REGISTRATION ========== :end-before: // ========== TOOL REGISTRATION ========== 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``: 1. ``A`` starts. 2. ``A`` calls ``next()``, which triggers ``B``. 3. ``B`` starts. 4. ``B`` calls ``next()``, which triggers the actual **Handler**. 5. **Handler** executes and returns a result to ``B``. 6. ``B`` finishes and returns a result to ``A``. 7. ``A`` finishes 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: .. literalinclude:: ../../examples/features/middleware.cpp :language: cpp :start-after: // Middleware C: Inner middleware (registered third) :end-before: // ========== TOOL REGISTRATION ========== 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 ``isError`` result. - **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 :doc:`oauth`. .. code-block:: cpp server.use([](Context& ctx, const nlohmann::json& params, TypeErasedHandler next) -> Task { 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. .. code-block:: cpp server.use([](Context& ctx, const nlohmann::json& params, TypeErasedHandler next) -> Task { 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 await ``next`` will 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``. Wrapping ``next()`` in a try-catch sees exceptions from inner middleware, including a refusal from ``make_auth_middleware()``, and from resource and prompt handlers, but not tool handler exceptions, which arrive as ``isError`` results. - **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 ``ctx`` object 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 -------- - :doc:`tools` for details on tool handlers. - :doc:`context` for information on using the request context. - :doc:`oauth` for an advanced use case involving authentication middleware.