# Compose client middleware

A **middleware** wraps the `fetch` a client transport uses, so it sees every HTTP request on the way out and every `Response` on the way back.

## Write a middleware

`createMiddleware` builds one from a function that receives the next handler plus the request. Compose it onto `fetch` with `applyMiddlewares` and hand the result to the transport's `fetch` option.

```ts
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)
});
```

Every request this transport sends now carries the header — including the requests the SDK sends that you never wrote, like `initialize`.

::: info Not the framework middleware packages
This page is about client request middleware: functions that wrap the `fetch` inside `@modelcontextprotocol/client`. The `@modelcontextprotocol/express`, `@modelcontextprotocol/hono`, and `@modelcontextprotocol/node` packages also carry the word "middleware" — those are server-side framework adapters for mounting a handler. See [Express](https://ts.sdk.modelcontextprotocol.io/v2/serving/express.md) and [Hono](https://ts.sdk.modelcontextprotocol.io/v2/serving/hono.md).
:::

## Compose several middlewares

`applyMiddlewares` takes any number of middlewares; each one in the list wraps everything before it. Stub out the network and stamp each layer's name on both sides of `next` to watch the order.

```ts
const stamp = (name: string) =>
    createMiddleware(async (next, input, init) => {
        console.log(`-> ${name}`);
        const response = await next(input, init);
        console.log(`<- ${name}`);
        return response;
    });

const base = async () => new Response('ok');
await applyMiddlewares(stamp('retry'), stamp('auth'), stamp('trace'))(base)('http://localhost:3000/mcp');
```

The last middleware you pass is outermost — it sees the request first and the response last:

```
-> trace
-> auth
-> retry
<- retry
<- auth
<- trace
```

The first middleware you pass sits closest to the network. Put a retry there so every layer above it sees one settled `Response`.

## Use the built-in logging middleware

`withLogging` ships in `@modelcontextprotocol/client`; called with no options it logs every request the wrapped `fetch` makes.

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

Connect through `loggedFetch` and call one tool. Four requests reach the wire, and you wrote one of them:

```
HTTP POST http://localhost:3000/mcp 200  (0ms)
HTTP POST http://localhost:3000/mcp 202  (0ms)
HTTP GET http://localhost:3000/mcp 405  (0ms)
HTTP POST http://localhost:3000/mcp 200  (0ms)
```

The `POST`s are `initialize`, the `notifications/initialized` notification, and your `tools/call`; the `GET` opens the server-to-client stream, which this server declines. Pass `statusLevel: 400` to log only failures, `includeRequestHeaders` / `includeResponseHeaders` to add headers to each line, and `logger` to replace the formatter entirely.

::: warning
The default logger writes to `console.log` and `console.error`. In a process whose stdout carries an MCP stdio transport, pass your own `logger` so these lines stay off that stream.
:::

## Combine middleware with an auth provider

`withOAuth(provider, serverUrl)` is the OAuth flow expressed as one middleware layer: it adds the `Authorization` header, and on a `401` it re-authenticates against `serverUrl` and retries the request once.

```ts
const serverUrl = new URL('http://localhost:3000/mcp');
const authed = new StreamableHTTPClientTransport(serverUrl, {
    fetch: applyMiddlewares(withOAuth(provider, serverUrl), withLogging({ statusLevel: 400 }))(fetch)
});
```

`provider` is the same `OAuthClientProvider` you would hand to the transport directly. With `statusLevel: 400`, `withLogging` stays silent until a request fails.

::: tip
For the common case, pass `authProvider` to the transport instead — see [OAuth](https://ts.sdk.modelcontextprotocol.io/v2/clients/oauth.md). `withOAuth` is for stacks that already own `fetch` and need auth composed with other layers.
:::

## Inspect the response

A middleware runs on both sides of `next`: read the request body before the call and the `Response` after it. Map each JSON-RPC method to the HTTP status it came back with.

```ts
const observeStatus = createMiddleware(async (next, input, init) => {
    const response = await next(input, init);
    if (typeof init?.body === 'string') {
        const { method } = JSON.parse(init.body) as { method?: string };
        console.log(`${method ?? 'response'} -> HTTP ${response.status}`);
    }
    return response;
});
```

Connecting through `observeStatus` and calling one tool prints one line per request that carried a body:

```
initialize -> HTTP 200
notifications/initialized -> HTTP 202
tools/call -> HTTP 200
```

Always return the `Response`; the transport consumes its body after you. To read the body too, read a `response.clone()`.

## Recap

- A middleware wraps the transport's `fetch`: `createMiddleware` builds one, `applyMiddlewares` composes many, and the transport's `fetch` option takes the result.
- The last middleware passed to `applyMiddlewares` is outermost; the first sits closest to the network.
- A middleware sees every HTTP request the transport sends, including the ones the SDK sends on its own.
- `withLogging` and `withOAuth` ship in `@modelcontextprotocol/client`.
- A middleware sees both directions: the request before `next`, the `Response` after it.
