Client Middleware

Purpose and Scope

Client middleware is the SDK extension point for changing or observing HTTP requests made by a client transport. In this repository, the term means a wrapper around the fetch function used by the Streamable HTTP client transport. That wrapper can see each outbound request, adjust request metadata such as headers, wait for the Response, and inspect or replace what comes back. This is useful for tracing, adding application headers, adapting logging to production environments, and composing auth-aware behavior without changing every individual client call. Sources: docs/clients/middleware.md, docs/clients/connect.md

This page is specifically about request middleware in the client package, not the server framework middleware packages. The docs distinguish it from the Express, Hono, and Node adapters, which mount MCP handlers into web frameworks on the server side. Client middleware belongs beside client connection, calling, caching, and authentication concerns because it wraps the transport layer that all those higher-level operations share. Once applied, it affects handshake traffic as well as application methods such as listing tools, reading resources, and calling tools. Sources: docs/clients/middleware.md, docs/clients/calling.md

Relevant Source Files

  • docs/clients/middleware.md - Primary guide for creating middleware with createMiddleware, composing layers with applyMiddlewares, using withLogging, and combining middleware with OAuth.
  • docs/clients/connect.md - Shows where a transport is created and connected, which is the moment the wrapped fetch becomes part of the client connection.
  • docs/clients/calling.md - Documents the client operations whose HTTP traffic can be observed by middleware after connection, including listTools, callTool, readResource, and prompt-related calls.
  • docs/clients/caching.md - Explains response caching and per-call cache modes, which matter because cache hits may avoid network requests that middleware would otherwise see.
  • docs/clients/machine-auth.md - Documents authProvider behavior for client credentials, bearer tokens, private-key JWT, and unauthorized retry behavior that can coexist with middleware.
  • docs/clients/oauth.md - Documents OAuthClientProvider, redirect-driven user authentication, UnauthorizedError, token storage, and issuer-aware client registration, all relevant to auth middleware composition.

Core Primitives

The central primitive is a middleware function created with createMiddleware. It receives a next handler plus the fetch input and init values, then decides whether to modify the request before calling next, whether to examine the Response after next resolves, or whether to return an alternative Response. applyMiddlewares turns one or more middleware layers into a fetch-compatible function, and the resulting function is passed through the fetch option of StreamableHTTPClientTransport. The Client itself remains focused on MCP protocol methods and connection state; the transport owns the HTTP boundary. Sources: docs/clients/middleware.md, docs/clients/connect.md

A typical middleware makes a small, deterministic request change. For example, the guide constructs Headers from init headers, sets an X-Request-Source value, and forwards the updated init object to next. Because initialize, initialized notifications, stream setup attempts, and user-triggered methods all use the same transport fetch, the header is present on SDK-generated traffic as well as calls the application explicitly requested. This is the main advantage over adding headers at each call site: the policy is attached once at the connection boundary. Sources: docs/clients/middleware.md

import { applyMiddlewares, createMiddleware, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
 
const tagRequests = createMiddleware(async (next, input, init) => {
    const headers = new Headers(init?.headers);
    headers.set('X-Request-Source', 'reports-cli');
    return next(input, { ...init, headers });
});
 
const transport = new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp'), {
    fetch: applyMiddlewares(tagRequests)(fetch)
});

Execution Flow

The execution flow starts before connect. Create a Client with its name and version, create a Streamable HTTP transport for the MCP endpoint, attach the wrapped fetch to that transport, and call connect. During connect, the SDK performs the initialize handshake and records negotiated server information, including protocol version, capabilities, and instructions. Since middleware wraps the transport fetch, it participates in the handshake as well. If the same client later calls listTools or callTool, those requests use the same wrapped path unless the response cache serves the result locally. Sources: docs/clients/connect.md, docs/clients/middleware.md, docs/clients/caching.md

Composition order matters when multiple policies are present. The middleware guide states that applyMiddlewares takes any number of layers and that the last middleware passed is outermost. Outermost means it sees the request first and the response last. The first middleware sits closest to the network. The docs recommend putting retry-like behavior near the network so higher layers observe one settled response rather than every transient attempt. That order also helps with logging: a trace layer outside auth can report the final request shape, while a network-near retry layer can hide retry churn from business-level observers. Sources: docs/clients/middleware.md

const loggedFetch = applyMiddlewares(tagRequests, withLogging())(fetch);

Built-in Logging and Response Inspection

withLogging is the built-in logging middleware exported by the client package. With no options, it logs each request made by the wrapped fetch. The guide shows that a single tool call over Streamable HTTP can produce more wire traffic than the application wrote directly: initialize, an initialized notification, a GET for the server-to-client stream that may be rejected, and the tool call itself. Middleware is therefore a good way to understand transport behavior, but logs should be interpreted as protocol traffic rather than a one-to-one list of application method calls. Sources: docs/clients/middleware.md, docs/clients/calling.md

Logging has one important runtime edge case. The default logger writes to console.log and console.error. That is acceptable for ordinary HTTP client programs, but dangerous for a process whose stdout is carrying an MCP stdio transport, because diagnostic text could corrupt the protocol stream. The guide explicitly recommends passing a custom logger in that situation. withLogging also supports filtering and detail options: statusLevel can restrict output to failures, includeRequestHeaders and includeResponseHeaders can add header data, and logger can replace the formatter entirely. Sources: docs/clients/middleware.md

Authentication and Cache Interactions

Authentication can be handled either by transport authProvider support or by an OAuth middleware layer. The middleware guide describes withOAuth(provider, serverUrl) as a layer that adds the Authorization header, re-authenticates on a 401 response, and retries the request once. The OAuth guide separately shows passing an OAuthClientProvider as the transport authProvider, where discovery, client registration, redirect handling, token storage, PKCE verifier state, and callback completion are driven by the SDK. When combining these patterns, keep issuer binding and token ownership clear so one layer does not accidentally send credentials meant for another authorization server. Sources: docs/clients/middleware.md, docs/clients/oauth.md

Machine authentication follows the same transport boundary. ClientCredentialsProvider, PrivateKeyJwtProvider, CrossAppAccessProvider, and simple bearer-token AuthProvider implementations all plug into the transport authProvider option. Those providers attach access tokens to requests and may refresh after an unauthorized response, with the transport retrying once when the provider supports it. Middleware can still add non-auth headers, correlation IDs, or response auditing around that behavior. Avoid duplicating Authorization handling in a generic header middleware unless it deliberately owns the same refresh and retry semantics. Sources: docs/clients/machine-auth.md, docs/clients/middleware.md

Caching changes what middleware can observe. The caching guide says every Client has a response cache, and cacheable methods such as listTools, listPrompts, listResources, listResourceTemplates, and readResource may be served locally while a server freshness hint remains valid. In that case, no HTTP request reaches the server and no fetch middleware runs for that call. Per-call cacheMode can force a refresh, bypass cache reads and writes, or use the default behavior. This distinction matters when using middleware for metrics, because network-level counts may be lower than method-level counts. Sources: docs/clients/caching.md, docs/clients/calling.md

Compact Reference

  • createMiddleware - Builds a client request middleware from a function that receives next, input, and init, then returns a Response or Promise of a Response.
  • applyMiddlewares - Composes one or more middleware layers and applies them to a base fetch implementation.
  • withLogging - Built-in middleware for request logging, with options for status filtering, header inclusion, and custom logger output.
  • withOAuth - OAuth middleware layer that adds Authorization, reacts to 401, re-authenticates against a server URL, and retries once.
  • StreamableHTTPClientTransport fetch option - The attachment point for the composed fetch used by HTTP client connections.

Practical Guidance and Next Steps

Use middleware for cross-cutting transport policies, not for MCP method semantics. A good middleware is small, order-aware, and safe for SDK-generated traffic such as initialize. For application logic, prefer the Client methods documented in the calling guide. For end-user sign-in, start with the OAuth guide; for jobs and service accounts, use the machine-auth guide; for cache behavior, read the caching guide before relying on middleware-based request counts. If your goal is to mount a server in Express, Hono, or Node HTTP, continue to the serving pages instead of this client-focused page. Sources: docs/clients/middleware.md, docs/clients/calling.md, docs/clients/oauth.md, docs/clients/machine-auth.md, docs/clients/caching.md