From 4a29aa3434b0bf341a1b1a7efbb4f684ec7b7fb2 Mon Sep 17 00:00:00 2001 From: Raashish Aggarwal <94279692+raashish1601@users.noreply.github.com> Date: Mon, 11 May 2026 20:51:55 +0530 Subject: [PATCH] fix json response accept validation --- .changeset/fix-json-response-accept.md | 5 +++++ src/server/webStandardStreamableHttp.ts | 17 +++++++++-------- test/server/streamableHttp.test.ts | 23 +++++++++++++++++++++++ 3 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 .changeset/fix-json-response-accept.md diff --git a/.changeset/fix-json-response-accept.md b/.changeset/fix-json-response-accept.md new file mode 100644 index 0000000000..90310d0802 --- /dev/null +++ b/.changeset/fix-json-response-accept.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/sdk': patch +--- + +Allow streamable HTTP JSON response mode to accept POST requests whose `Accept` header only lists `application/json`. diff --git a/src/server/webStandardStreamableHttp.ts b/src/server/webStandardStreamableHttp.ts index 1f528427c8..9b72aa8a00 100644 --- a/src/server/webStandardStreamableHttp.ts +++ b/src/server/webStandardStreamableHttp.ts @@ -598,14 +598,15 @@ export class WebStandardStreamableHTTPServerTransport implements Transport { try { // Validate the Accept header const acceptHeader = req.headers.get('accept'); - // The client MUST include an Accept header, listing both application/json and text/event-stream as supported content types. - if (!acceptHeader?.includes('application/json') || !acceptHeader.includes('text/event-stream')) { - this.onerror?.(new Error('Not Acceptable: Client must accept both application/json and text/event-stream')); - return this.createJsonErrorResponse( - 406, - -32000, - 'Not Acceptable: Client must accept both application/json and text/event-stream' - ); + const acceptsJson = acceptHeader?.includes('application/json') ?? false; + const acceptsEventStream = acceptHeader?.includes('text/event-stream') ?? false; + + if (!acceptsJson || (!this._enableJsonResponse && !acceptsEventStream)) { + const message = this._enableJsonResponse + ? 'Not Acceptable: Client must accept application/json' + : 'Not Acceptable: Client must accept both application/json and text/event-stream'; + this.onerror?.(new Error(message)); + return this.createJsonErrorResponse(406, -32000, message); } const ct = req.headers.get('content-type'); diff --git a/test/server/streamableHttp.test.ts b/test/server/streamableHttp.test.ts index 4a4f7d8248..a6b564fc0b 100644 --- a/test/server/streamableHttp.test.ts +++ b/test/server/streamableHttp.test.ts @@ -1115,6 +1115,29 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { }); }); + it('should accept JSON-only Accept header in JSON response mode', async () => { + const toolsListMessage: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'tools/list', + params: {}, + id: 'json-req-1' + }; + + const response = await sendPostRequest(baseUrl, toolsListMessage, sessionId, { Accept: 'application/json' }); + + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('application/json'); + + const result = await response.json(); + expect(result).toMatchObject({ + jsonrpc: '2.0', + result: expect.objectContaining({ + tools: expect.arrayContaining([expect.objectContaining({ name: 'greet' })]) + }), + id: 'json-req-1' + }); + }); + it('should return JSON response for batch requests', async () => { const batchMessages: JSONRPCMessage[] = [ { jsonrpc: '2.0', method: 'tools/list', params: {}, id: 'batch-1' },