You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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).
_commonHeaders() (src/client/sse.ts) returns a Headers instance which lowercases all keys (authorization).
_startOrAuth() overrides the user's eventSourceInit.fetch with an internal wrapper that calls fetchImpl(url, { ...init, headers: <Headers instance> }).
The user's custom fetch (buildSseEventSourceFetch above) iterates the Headers instance into a plain object (now with the lowercase key authorization), then merges its own closure headers (with the original capitalization Authorization). The result is a plain object with bothauthorization and Authorization as 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.
Description
When using
SSEClientTransportwith bothrequestInit.headersand a customeventSourceInit.fetchthat wraps headers, theAuthorizationheader 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 with401 Unauthorized.Reproduction
This pattern was a workaround for an older SDK version where
_commonHeaders()did not includerequestInit.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-tokenheader.Actual behavior
The server receives
Authorization: Bearer my-token, Bearer my-token(duplicate values comma-joined).Verified with Node.js 22:
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> }).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, producingBearer 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.curlwith the same token succeeds because curl doesn't produce duplicate headers.The same pattern likely exists in other downstream projects that adopted the
eventSourceInit.fetchworkaround before the SDK was fixed.Possible fixes
eventSourceInit.fetchis no longer necessary post-add custom headers on initial _startOrAuth call #318 / SSE client is not adding headers from requestInit? in _commonHeaders method #436 fix, and recommend using onlyrequestInit.headers.Headersinstance that round-trips through user code.requestInit.headersandeventSourceInit.fetchare provided.Workaround for users
Either:
eventSourceInit.fetchentirely and use onlyrequestInit.headersnew Headers(init?.headers)and.set()(which is case-insensitive) instead of plain object mergingEnvironment
@modelcontextprotocol/sdkv1.28.0 / v1.29.0 (same code path)eventsourcev3.0.7Related
_commonHeadersthat addressed a different aspect of the same area_startOrAuthto honorrequestInit.headers✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved)