From 605cff7fef24013994cafc0c85f5fea0131f4cb2 Mon Sep 17 00:00:00 2001 From: Charlie Tonneslan Date: Tue, 17 Mar 2026 09:51:02 -0400 Subject: [PATCH 1/2] fix(mcp): relax Accept header validation for broader client compatibility The strict Accept header check in handlePostRequest rejects requests from widely-used MCP clients (Gemini CLI, Java MCP SDK, Open WebUI, curl) that don't send both application/json and text/event-stream. Apply Postel's Law: accept requests that include either application/json OR text/event-stream OR */*. Only reject when the client explicitly accepts neither. Default to */* when no Accept header is present. Fixes #1773 --- packages/mcp/src/streamable-http.test.ts | 70 ++++++++++++++++++++++-- packages/mcp/src/streamable-http.ts | 15 ++--- 2 files changed, 74 insertions(+), 11 deletions(-) diff --git a/packages/mcp/src/streamable-http.test.ts b/packages/mcp/src/streamable-http.test.ts index 30891f319..2aad864c8 100644 --- a/packages/mcp/src/streamable-http.test.ts +++ b/packages/mcp/src/streamable-http.test.ts @@ -608,15 +608,15 @@ describe('MCP helper', () => { expectErrorResponse(errorData, -32000, /Client must accept text\/event-stream/) }) - it('should reject POST requests without proper Accept header', async () => { + it('should reject POST requests that explicitly accept neither json nor sse', async () => { sessionId = await initializeServer() - // Try POST without Accept: text/event-stream + // Try POST with Accept header that includes neither json nor sse const response = await server.request('/', { method: 'POST', headers: { 'Content-Type': 'application/json', - Accept: 'application/json', // Missing text/event-stream + Accept: 'text/plain', // Neither application/json nor text/event-stream 'mcp-session-id': sessionId, 'mcp-protocol-version': '2025-03-26', }, @@ -628,10 +628,72 @@ describe('MCP helper', () => { expectErrorResponse( errorData, -32000, - /Client must accept both application\/json and text\/event-stream/ + /Client must accept application\/json or text\/event-stream/ ) }) + it('should accept POST requests with only application/json Accept header', async () => { + sessionId = await initializeServer() + + const response = await sendPostRequest(server, TEST_MESSAGES.toolsList, { + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-03-26', + Accept: 'application/json', + }) + + expect(response.status).not.toBe(406) + }) + + it('should accept POST requests with only text/event-stream Accept header', async () => { + sessionId = await initializeServer() + + const response = await server.request('/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-03-26', + }, + body: JSON.stringify(TEST_MESSAGES.toolsList), + }) + + expect(response.status).not.toBe(406) + }) + + it('should accept POST requests with wildcard Accept header', async () => { + sessionId = await initializeServer() + + const response = await server.request('/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: '*/*', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-03-26', + }, + body: JSON.stringify(TEST_MESSAGES.toolsList), + }) + + expect(response.status).not.toBe(406) + }) + + it('should accept POST requests with no Accept header', async () => { + sessionId = await initializeServer() + + const response = await server.request('/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'mcp-session-id': sessionId, + 'mcp-protocol-version': '2025-03-26', + }, + body: JSON.stringify(TEST_MESSAGES.toolsList), + }) + + expect(response.status).not.toBe(406) + }) + it('should reject unsupported Content-Type', async () => { sessionId = await initializeServer() diff --git a/packages/mcp/src/streamable-http.ts b/packages/mcp/src/streamable-http.ts index b9992f73d..db237f135 100644 --- a/packages/mcp/src/streamable-http.ts +++ b/packages/mcp/src/streamable-http.ts @@ -275,19 +275,20 @@ export class StreamableHTTPTransport implements Transport { private async handlePostRequest(ctx: Context, parsedBody?: unknown) { try { // Validate the Accept header - const acceptHeader = ctx.req.header('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') - ) { + const acceptHeader = ctx.req.header('Accept') || '*/*' + const acceptsJson = + acceptHeader.includes('application/json') || acceptHeader.includes('*/*') + const acceptsSse = + acceptHeader.includes('text/event-stream') || acceptHeader.includes('*/*') + + if (!acceptsJson && !acceptsSse) { throw new HTTPException(406, { res: Response.json({ jsonrpc: '2.0', error: { code: ErrorCode.ConnectionClosed, message: - 'Not Acceptable: Client must accept both application/json and text/event-stream', + 'Not Acceptable: Client must accept application/json or text/event-stream', }, id: null, }), From 9742e5e10798622bd66d059de3ff6b95bebcc9b0 Mon Sep 17 00:00:00 2001 From: Charlie Tonneslan Date: Tue, 17 Mar 2026 19:57:05 -0400 Subject: [PATCH 2/2] add changeset --- .changeset/relax-accept-header.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/relax-accept-header.md diff --git a/.changeset/relax-accept-header.md b/.changeset/relax-accept-header.md new file mode 100644 index 000000000..c8aa3705f --- /dev/null +++ b/.changeset/relax-accept-header.md @@ -0,0 +1,5 @@ +--- +"@hono/mcp": patch +--- + +Relax Accept header validation in MCP Streamable HTTP transport for broader client compatibility