Skip to content

Conversation

@cliffhall
Copy link
Member

@cliffhall cliffhall commented Sep 6, 2025

Description

Adds a mechanism to dynamically inject and update HTTP headers for ongoing proxy sessions, resolving an issue where headers introduced after the initial handshake (e.g, Mcp-Protocol-Version) were not being forwarded to the server on subsequent exchanges.

In src/index.ts:

  • Added sessionHeaderHolders, a Map to store the most recent set of headers for each active session ID.
  • Added createCustomFetch, a new helper function that creates a fetch wrapper. This wrapper merges the session headers from sessionHeaderHolders with the request-specific headers from the SDK (like Content-Type)
  • In createTransport, use createCustomFetch for both the StreamableHTTPClientTransport and SSEClientTransport. This injects the dynamic header logic into the transport layer.
  • In /mcp, /sse, /message route handlers, added logic to update the sessionHeaderHolders map with the latest headers from every incoming client request, using use updateHeadersInPlace to replace current headers with new headers, retaining the Accept header.
  • Added cleanup logic to the onsessionclosed callback and the /mcp DELETE handler to remove session data from sessionHeaderHolders.
  • In getHttpHeaders, more robust handling of string[] and undefined values from req.headers, satisfying the "strict": true TypeScript configuration.
  • Added updateHeadersInPlace helper function to solve a stale header reference issue in SSEClientTransport. It mutates the header object in-place, ensuring the transport sees all updates, while carefully preserving the original Accept header.

Motivation and Context

The core problem was that the transportToServer instance was created once with a static set of headers. This meant that dynamically added headers like mcp-protocol-version (which is negotiated during initialization) and last-event-id were being dropped in subsequent requests.

The solution implemented here is to wrap the fetch function used by the HTTP-based transports (StreamableHttp and SSE). This custom wrapper merges the latest headers from the client with the headers of each outgoing request from the proxy, ensuring all headers are preserved for the lifetime of the session.

Fixes #679, #758, #723

How Has This Been Tested?

StreamableHttp With OAuth Client Side

with-oauth.mov

Sending mcp-protocol-version during server metadata discovery

Screenshot 2025-09-06 at 5 42 31 PM

StreamableHttp Without OAuth Client Side

without-oauth.mov

StreamableHttp Server Side

mcp-session-id and mcp-protocol-version are negotiated on the first exchange and present thereafter

Screenshot 2025-09-06 at 2 48 49 PM

SSE Without OAuth Client Side

sse-without-oauth.mov

SSE Without OAuth Server Side

mcp-session-id is not a part of SSE connections

mcp-protocol-version is negotiated on the second exchange and present thereafter

sse-no-oauth

Breaking Changes

Nope.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

…ontextprotocol#679

Adds a mechanism to dynamically inject and update HTTP headers for ongoing proxy sessions, resolving an issue where headers introduced after the initial handshake (e.g, Mcp-Protocol-Version) were not being forwarded to the server.

The core problem was that the `transportToServer` instance was created once with a static set of headers. This meant that critical, dynamically added headers like `mcp-protocol-version` and `last-event-id` were being dropped in subsequent requests.

The solution implemented here is to wrap the `fetch` function used by the HTTP-based transports (`StreamableHttp` and `SSE`). This custom wrapper merges the latest headers from the client with the headers of each outgoing request from the proxy, ensuring all headers are preserved for the lifetime of the session.

Changes:

In `src/index.ts`:
  - Added `sessionHeaderHolders`, a `Map` to store the most recent set of headers for each active session ID.
  - Added `createCustomFetch`, a new helper function that creates a `fetch` wrapper. This wrapper merges the session headers from `sessionHeaderHolders` with the request-specific headers from the SDK (like `Content-Type`)
  - In `createTransport`, use `createCustomFetch` for both the `StreamableHTTPClientTransport` and `SSEClientTransport`. This injects the dynamic header logic into the transport layer.
  - In `/mcp`, `/sse`, `/message` route handlers, added logic to update the `sessionHeaderHolders` map with the latest headers from every incoming client request.
  - Added cleanup logic to the `onsessionclosed` callback and the `/mcp` DELETE handler to remove session data from `sessionHeaderHolders`.
  - In `getHttpHeaders`, more robust handling of `string[]` and `undefined` values from `req.headers`, satisfying the `"strict": true` TypeScript configuration.
…object instead of replacing it. This ensures that transports holding a static reference, like `SSEClientTransport`, always see the latest headers.

* In `src/server/index.ts`,
  - Added `updateHeadersInPlace` helper function to solve the stale header reference issue in `SSEClientTransport`. It mutates the header object in-place, ensuring the transport sees all updates, while carefully preserving the original `Accept` header required by the transport.
  - in `/mcp GET`, `/mcp POST`, and `/message POST`, use `updateHeadersInPlace` to replace current headers with new headers, retaining the Accept header.
Copy link
Member

@olaservo olaservo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this locally using Everything server with SSE and Streamable HTTP with a few different patterns of re-connecting and starting new sessions as well as trying to repro the other issues listed. (I'm using Windows and Chrome.) I can consistently see the correct headers persisting with this branch. 👍

@olaservo olaservo merged commit 9e4bbab into modelcontextprotocol:main Sep 8, 2025
4 checks passed
@cliffhall cliffhall deleted the dynamic-headers branch September 9, 2025 18:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants