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 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, }),