diff --git a/.changeset/fix-node-set-cookie.md b/.changeset/fix-node-set-cookie.md new file mode 100644 index 0000000000..a090aec10f --- /dev/null +++ b/.changeset/fix-node-set-cookie.md @@ -0,0 +1,5 @@ +--- +"@modelcontextprotocol/node": patch +--- + +Preserve every Set-Cookie field when adapting a web response to Node. diff --git a/packages/middleware/node/src/toNodeHandler.ts b/packages/middleware/node/src/toNodeHandler.ts index 5f26b1ad26..965c94cbad 100644 --- a/packages/middleware/node/src/toNodeHandler.ts +++ b/packages/middleware/node/src/toNodeHandler.ts @@ -66,7 +66,7 @@ export interface NodeIncomingMessageLike extends AsyncIterable { /** Minimal duck-typed shape of a Node.js `ServerResponse` accepted by {@linkcode toNodeHandler}. */ export interface NodeServerResponseLike { - writeHead(statusCode: number, headers?: Record): unknown; + writeHead(statusCode: number, headers?: Record): unknown; write(chunk: string | Uint8Array): unknown; end(chunk?: string | Uint8Array): unknown; on(event: string, listener: (...args: unknown[]) => void): unknown; @@ -172,10 +172,12 @@ export function toNodeHandler(handler: FetchLikeMcpHandler, opts?: ToNodeHandler } } - const headers: Record = {}; + const headers: Record = {}; for (const [name, value] of response.headers) { headers[name] = value; } + const cookies = response.headers.getSetCookie(); + if (cookies.length > 0) headers['set-cookie'] = cookies; res.writeHead(response.status, headers); if (response.body === null) { finished = true; diff --git a/packages/middleware/node/test/toNodeHandler.test.ts b/packages/middleware/node/test/toNodeHandler.test.ts index b879365f4a..b0bfb48ab0 100644 --- a/packages/middleware/node/test/toNodeHandler.test.ts +++ b/packages/middleware/node/test/toNodeHandler.test.ts @@ -421,7 +421,7 @@ describe('toNodeHandler', () => { interface FakeNodeResponse extends NodeServerResponseLike { statusCode: number; - headers: Record | undefined; + headers: Record | undefined; } function nodeRequestResponse(body: unknown): { @@ -454,7 +454,7 @@ function nodeRequestResponse(body: unknown): { const res: FakeNodeResponse = { statusCode: 0, headers: undefined, - writeHead(statusCode: number, headers?: Record) { + writeHead(statusCode: number, headers?: Record) { this.statusCode = statusCode; this.headers = headers; return this; @@ -484,3 +484,15 @@ function nodeRequestResponse(body: unknown): { } }; } + +describe('response cookies', () => { + it('passes each Set-Cookie field separately to Node', async () => { + const cookies = ['first=one; Path=/; HttpOnly', 'second=two; Path=/; HttpOnly']; + const response = new Response(null, { headers: cookies.map(cookie => ['set-cookie', cookie] as [string, string]) }); + const writeHead = vi.fn(); + const res: NodeServerResponseLike = { writeHead, on: vi.fn(), write: vi.fn(), end: vi.fn() }; + const req = Object.assign(Readable.from([]), { method: 'GET', url: '/', headers: { host: 'localhost' } }); + await toNodeHandler({ fetch: async () => response })(req, res); + expect(writeHead).toHaveBeenCalledWith(200, { 'set-cookie': cookies }); + }); +});