Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/relax-accept-header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hono/mcp": patch
---

Relax Accept header validation in MCP Streamable HTTP transport for broader client compatibility
70 changes: 66 additions & 4 deletions packages/mcp/src/streamable-http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand All @@ -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()

Expand Down
15 changes: 8 additions & 7 deletions packages/mcp/src/streamable-http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
Expand Down