diff --git a/.changeset/fix-json-response-accept.md b/.changeset/fix-json-response-accept.md new file mode 100644 index 000000000..90310d080 --- /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 1f528427c..9b72aa8a0 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 4a4f7d824..a6b564fc0 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' },