From 7bc57da02f7ad65d781fd808255fecea1a3e16c7 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Sat, 20 Dec 2025 14:16:37 +0200 Subject: [PATCH 1/3] move to Headers object --- packages/core/src/types/types.ts | 7 +--- packages/server/src/server/helper/headers.ts | 33 +++++++++++++++++++ packages/server/src/server/server.ts | 2 +- packages/server/src/server/sse.ts | 4 ++- .../src/server/webStandardStreamableHttp.ts | 2 +- packages/server/test/server/sse.test.ts | 8 ++--- .../server/test/server/streamableHttp.test.ts | 4 +-- .../integration/test/taskResumability.test.ts | 2 +- 8 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 packages/server/src/server/helper/headers.ts diff --git a/packages/core/src/types/types.ts b/packages/core/src/types/types.ts index 35b04745d..97b7dc5cb 100644 --- a/packages/core/src/types/types.ts +++ b/packages/core/src/types/types.ts @@ -2384,11 +2384,6 @@ type Flatten = T extends Primitive type Infer = Flatten>; -/** - * Headers that are compatible with both Node.js and the browser. - */ -export type IsomorphicHeaders = Record; - /** * Information about the incoming request. */ @@ -2396,7 +2391,7 @@ export interface RequestInfo { /** * The headers of the request. */ - headers: IsomorphicHeaders; + headers: Headers; } /** diff --git a/packages/server/src/server/helper/headers.ts b/packages/server/src/server/helper/headers.ts new file mode 100644 index 000000000..4ff9796dd --- /dev/null +++ b/packages/server/src/server/helper/headers.ts @@ -0,0 +1,33 @@ +import type { IncomingHttpHeaders } from 'node:http'; + +/** + * Converts a Node.js IncomingHttpHeaders object to a Web Headers object. + * @param h - The Node.js IncomingHttpHeaders object. + * @returns The Web Headers object. + */ +export function nodeHeadersToWebHeaders(h: IncomingHttpHeaders): Headers { + const out = new Headers(); + + for (const [name, value] of Object.entries(h)) { + if (value === undefined) continue; + + // Node may surface set-cookie as string[] + if (name.toLowerCase() === 'set-cookie') { + if (Array.isArray(value)) { + for (const v of value) out.append('set-cookie', v); + } else { + out.append('set-cookie', value); + } + continue; + } + + if (Array.isArray(value)) { + // Most headers can be joined; append preserves multiple values too. + for (const v of value) out.append(name, v); + } else { + out.set(name, value); + } + } + + return out; +} diff --git a/packages/server/src/server/server.ts b/packages/server/src/server/server.ts index 8132e342b..c0e0541e8 100644 --- a/packages/server/src/server/server.ts +++ b/packages/server/src/server/server.ts @@ -168,7 +168,7 @@ export class Server< if (this._capabilities.logging) { this.setRequestHandler(SetLevelRequestSchema, async (request, extra) => { const transportSessionId: string | undefined = - extra.sessionId || (extra.requestInfo?.headers['mcp-session-id'] as string) || undefined; + extra.sessionId || (extra.requestInfo?.headers.get('mcp-session-id') as string) || undefined; const { level } = request.params; const parseResult = LoggingLevelSchema.safeParse(level); if (parseResult.success) { diff --git a/packages/server/src/server/sse.ts b/packages/server/src/server/sse.ts index 4fd0fa1d6..88597e0b1 100644 --- a/packages/server/src/server/sse.ts +++ b/packages/server/src/server/sse.ts @@ -7,6 +7,8 @@ import { JSONRPCMessageSchema } from '@modelcontextprotocol/core'; import contentType from 'content-type'; import getRawBody from 'raw-body'; +import { nodeHeadersToWebHeaders } from './helper/headers.js'; + const MAXIMUM_MESSAGE_SIZE = '4mb'; /** @@ -149,7 +151,7 @@ export class SSEServerTransport implements Transport { } const authInfo: AuthInfo | undefined = req.auth; - const requestInfo: RequestInfo = { headers: req.headers }; + const requestInfo: RequestInfo = { headers: nodeHeadersToWebHeaders(req.headers) }; let body: string | unknown; try { diff --git a/packages/server/src/server/webStandardStreamableHttp.ts b/packages/server/src/server/webStandardStreamableHttp.ts index 082c904e1..78aadfc62 100644 --- a/packages/server/src/server/webStandardStreamableHttp.ts +++ b/packages/server/src/server/webStandardStreamableHttp.ts @@ -596,7 +596,7 @@ export class WebStandardStreamableHTTPServerTransport implements Transport { // Build request info from headers const requestInfo: RequestInfo = { - headers: Object.fromEntries(req.headers.entries()) + headers: req.headers }; let rawMessage; diff --git a/packages/server/test/server/sse.test.ts b/packages/server/test/server/sse.test.ts index 0fc9eebc8..cfe1d976f 100644 --- a/packages/server/test/server/sse.test.ts +++ b/packages/server/test/server/sse.test.ts @@ -303,7 +303,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { { type: 'text', text: JSON.stringify({ - headers: { + headers: new Headers({ host: `127.0.0.1:${serverPort}`, connection: 'keep-alive', 'content-type': 'application/json', @@ -313,7 +313,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { 'user-agent': 'node', 'accept-encoding': 'gzip, deflate', 'content-length': '124' - } + }) }) } ] @@ -416,9 +416,9 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { token: 'test-token' }, requestInfo: { - headers: { + headers: new Headers({ 'content-type': 'application/json' - } + }) } } ); diff --git a/packages/server/test/server/streamableHttp.test.ts b/packages/server/test/server/streamableHttp.test.ts index d8c6388e4..28022b8c2 100644 --- a/packages/server/test/server/streamableHttp.test.ts +++ b/packages/server/test/server/streamableHttp.test.ts @@ -445,7 +445,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { const requestInfo = JSON.parse(eventData.result.content[1].text); expect(requestInfo).toMatchObject({ - headers: { + headers: new Headers({ 'content-type': 'application/json', accept: 'application/json, text/event-stream', connection: 'keep-alive', @@ -454,7 +454,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { 'user-agent': expect.any(String), 'accept-encoding': expect.any(String), 'content-length': expect.any(String) - } + }) }); }); diff --git a/test/integration/test/taskResumability.test.ts b/test/integration/test/taskResumability.test.ts index 1e4d8a0fd..178a95202 100644 --- a/test/integration/test/taskResumability.test.ts +++ b/test/integration/test/taskResumability.test.ts @@ -2,13 +2,13 @@ import { randomUUID } from 'node:crypto'; import { createServer, type Server } from 'node:http'; import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; +import type { EventStore, JSONRPCMessage } from '@modelcontextprotocol/server'; import { CallToolResultSchema, LoggingMessageNotificationSchema, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server'; -import type { EventStore, JSONRPCMessage } from '@modelcontextprotocol/server'; import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; import { listenOnRandomPort, zodTestMatrix } from '@modelcontextprotocol/test-helpers'; From 03636a98e59afdef261b8061e0b253c70ef5ea2a Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Fri, 16 Jan 2026 12:18:36 +0200 Subject: [PATCH 2/3] clean up --- packages/server/src/server/helper/headers.ts | 33 -------------------- 1 file changed, 33 deletions(-) delete mode 100644 packages/server/src/server/helper/headers.ts diff --git a/packages/server/src/server/helper/headers.ts b/packages/server/src/server/helper/headers.ts deleted file mode 100644 index 4ff9796dd..000000000 --- a/packages/server/src/server/helper/headers.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { IncomingHttpHeaders } from 'node:http'; - -/** - * Converts a Node.js IncomingHttpHeaders object to a Web Headers object. - * @param h - The Node.js IncomingHttpHeaders object. - * @returns The Web Headers object. - */ -export function nodeHeadersToWebHeaders(h: IncomingHttpHeaders): Headers { - const out = new Headers(); - - for (const [name, value] of Object.entries(h)) { - if (value === undefined) continue; - - // Node may surface set-cookie as string[] - if (name.toLowerCase() === 'set-cookie') { - if (Array.isArray(value)) { - for (const v of value) out.append('set-cookie', v); - } else { - out.append('set-cookie', value); - } - continue; - } - - if (Array.isArray(value)) { - // Most headers can be joined; append preserves multiple values too. - for (const v of value) out.append(name, v); - } else { - out.set(name, value); - } - } - - return out; -} From 1ddb193bfb18a899bc0a771baa3e06f3bb0d14c4 Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Fri, 16 Jan 2026 12:23:08 +0200 Subject: [PATCH 3/3] test fix --- .../middleware/node/test/streamableHttp.test.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/middleware/node/test/streamableHttp.test.ts b/packages/middleware/node/test/streamableHttp.test.ts index ea6c09333..db1fb895c 100644 --- a/packages/middleware/node/test/streamableHttp.test.ts +++ b/packages/middleware/node/test/streamableHttp.test.ts @@ -12,13 +12,13 @@ import type { JSONRPCResultResponse, RequestId } from '@modelcontextprotocol/core'; +import type { EventId, EventStore, StreamId } from '@modelcontextprotocol/server'; +import { McpServer } from '@modelcontextprotocol/server'; import type { ZodMatrixEntry } from '@modelcontextprotocol/test-helpers'; import { listenOnRandomPort, zodTestMatrix } from '@modelcontextprotocol/test-helpers'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { NodeStreamableHTTPServerTransport } from '../src/streamableHttp.js'; -import { McpServer } from '@modelcontextprotocol/server'; -import type { EventId, EventStore, StreamId } from '@modelcontextprotocol/server'; -import { describe, expect, beforeEach, afterEach, it } from 'vitest'; async function getFreePort() { return new Promise(res => { @@ -402,10 +402,15 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { 'A simple test tool with request info', { name: z.string().describe('Name to greet') }, async ({ name }, { requestInfo }): Promise => { + // Convert Headers object to plain object for JSON serialization + // Headers is a Web API class that doesn't serialize with JSON.stringify + const serializedRequestInfo = { + headers: Object.fromEntries(requestInfo?.headers ?? new Headers()) + }; return { content: [ { type: 'text', text: `Hello, ${name}!` }, - { type: 'text', text: `${JSON.stringify(requestInfo)}` } + { type: 'text', text: `${JSON.stringify(serializedRequestInfo)}` } ] }; }