Implement dynamic header injection for proxy transports. #787
Merged
olaservo merged 2 commits intomodelcontextprotocol:mainfrom Sep 8, 2025
Merged
Implement dynamic header injection for proxy transports. #787olaservo merged 2 commits intomodelcontextprotocol:mainfrom
olaservo merged 2 commits intomodelcontextprotocol:mainfrom
Conversation
…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.
This was referenced Sep 6, 2025
This was
linked to
issues
Sep 6, 2025
olaservo
approved these changes
Sep 8, 2025
Member
olaservo
left a comment
There was a problem hiding this comment.
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. 👍
This was referenced Sep 15, 2025
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:sessionHeaderHolders, aMapto store the most recent set of headers for each active session ID.createCustomFetch, a new helper function that creates afetchwrapper. This wrapper merges the session headers fromsessionHeaderHolderswith the request-specific headers from the SDK (likeContent-Type)createTransport, usecreateCustomFetchfor both theStreamableHTTPClientTransportandSSEClientTransport. This injects the dynamic header logic into the transport layer./mcp,/sse,/messageroute handlers, added logic to update thesessionHeaderHoldersmap with the latest headers from every incoming client request, using useupdateHeadersInPlaceto replace current headers with new headers, retaining the Accept header.onsessionclosedcallback and the/mcpDELETE handler to remove session data fromsessionHeaderHolders.getHttpHeaders, more robust handling ofstring[]andundefinedvalues fromreq.headers, satisfying the"strict": trueTypeScript configuration.updateHeadersInPlacehelper function to solve a stale header reference issue inSSEClientTransport. It mutates the header object in-place, ensuring the transport sees all updates, while carefully preserving the originalAcceptheader.Motivation and Context
The core problem was that the
transportToServerinstance was created once with a static set of headers. This meant that dynamically added headers likemcp-protocol-version(which is negotiated during initialization) andlast-event-idwere being dropped in subsequent requests.The solution implemented here is to wrap the
fetchfunction used by the HTTP-based transports (StreamableHttpandSSE). 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-versionduring server metadata discoveryStreamableHttp Without OAuth Client Side
without-oauth.mov
StreamableHttp Server Side
mcp-session-idandmcp-protocol-versionare negotiated on the first exchange and present thereafterSSE Without OAuth Client Side
sse-without-oauth.mov
SSE Without OAuth Server Side
mcp-session-idis not a part of SSE connectionsmcp-protocol-versionis negotiated on the second exchange and present thereafterBreaking Changes
Nope.
Types of changes
Checklist
Additional context