modelcontextprotocol / modelcontextprotocol/typescript-sdk
SSE client: duplicate Authorization header when using eventSourceInit.fetch + requestInit.headers
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 13.4k
- Forks
- 2.2k
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 4
Description
Description
When using SSEClientTransport with both requestInit.headers and a custom eventSourceInit.fetch that wraps headers, the Authorization header sent to the server contains a comma-separated duplicate value (e.g. Bearer X, Bearer X). Servers that strictly parse the Bearer token format reject this with 401 Unauthorized.
Reproduction
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
const headers = { Authorization: "Bearer my-token" };
function buildSseEventSourceFetch(headers: Record<string, string>) {
return (url: string | URL, init?: RequestInit) => {
const sdkHeaders: Record<string, string> = {};
if (init?.headers) {
if (init.headers instanceof Headers) {
init.headers.forEach((value, key) => {
sdkHeaders[key] = value;
});
} else {
Object.assign(sdkHeaders, init.headers);
}
}
return fetch(url, {
...init,
headers: { ...sdkHeaders, ...headers },
});
};
}
const transport = new SSEClientTransport(new URL("https://mcp.example.com/sse"), {
requestInit: { headers },
eventSourceInit: { fetch: buildSseEventSourceFetch(headers) },
});
This pattern was a workaround for an older SDK version where _commonHeaders() did not include requestInit.headers. After the fix in #436 / #318 the workaround is no longer needed but is still in use in real codebases.
Expected behavior
The server receives a single Authorization: Bearer my-token header.
Actual behavior
The server receives Authorization: Bearer my-token, Bearer my-token (duplicate values comma-joined).
Verified with Node.js 22:
const headers = { authorization: 'Bearer abc', Authorization: 'Bearer abc' };
const req = new Request('https://example.com', { headers });
for (const [k, v] of req.headers.entries()) console.log(k, ':', v);
// authorization : Bearer abc, Bearer abc
Root cause
The interaction of three things:
_commonHeaders()(src/client/sse.ts) returns aHeadersinstance which lowercases all keys (authorization)._startOrAuth()overrides the user'seventSourceInit.fetchwith an internal wrapper that callsfetchImpl(url, { ...init, headers: <Headers instance> }).- The user's custom fetch (
buildSseEventSourceFetchabove) iterates theHeadersinstance into a plain object (now with the lowercase keyauthorization), then merges its own closure headers (with the original capitalizationAuthorization). The result is a plain object with bothauthorizationandAuthorizationas separate keys with the same value.
When that plain object is passed to fetch/Headers, the duplicate keys are merged into a single comma-separated value per HTTP/1.1 spec, producing Bearer X, Bearer X.
Real-world impact
Discovered while connecting to Notion's MCP server (https://mcp.notion.com/sse) which uses OAuth 2.1 with strict Bearer token parsing. The SDK appears to authenticate but every request receives 401. curl with the same token succeeds because curl doesn't produce duplicate headers.
The same pattern likely exists in other downstream projects that adopted the eventSourceInit.fetch workaround before the SDK was fixed.
Possible fixes
- Document that
eventSourceInit.fetchis no longer necessary post-#318 / #436 fix, and recommend using onlyrequestInit.headers. - Make the SDK's internal wrapper preserve the user's custom fetch behavior rather than passing a
Headersinstance that round-trips through user code. - Add a warning when both
requestInit.headersandeventSourceInit.fetchare provided.
Workaround for users
Either:
- Drop
eventSourceInit.fetchentirely and use onlyrequestInit.headers - In your custom fetch, use
new Headers(init?.headers)and.set()(which is case-insensitive) instead of plain object merging
Environment
@modelcontextprotocol/sdkv1.28.0 / v1.29.0 (same code path)- Node.js 22
eventsourcev3.0.7
Related
- #436 — earlier fix for
_commonHeadersthat addressed a different aspect of the same area - #318 — PR that fixed
_startOrAuthto honorrequestInit.headers
✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Read src/client/sse.ts, focusing on _commonHeaders() and _startOrAuth(), then run the provided Node.js 22 reproduction with both requestInit.headers and eventSourceInit.fetch. Determine how the wrapper should handle this combination so the server receives one Authorization value, while preserving the intended custom-fetch behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, networking
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100