From a065091a1c57bf57208c90c75f14fafa7ff5e199 Mon Sep 17 00:00:00 2001 From: pragnyanramtha Date: Sat, 16 May 2026 01:49:09 +0000 Subject: [PATCH] fix(server): allow streamable HTTP JSON without content type --- .../fix-streamable-http-content-type.md | 5 +++ .../node/test/streamableHttp.test.ts | 21 ++++++++-- packages/server/src/server/streamableHttp.ts | 6 --- .../server/test/server/streamableHttp.test.ts | 40 +++++++++++++------ 4 files changed, 49 insertions(+), 23 deletions(-) create mode 100644 .changeset/fix-streamable-http-content-type.md diff --git a/.changeset/fix-streamable-http-content-type.md b/.changeset/fix-streamable-http-content-type.md new file mode 100644 index 0000000000..a658b1d68a --- /dev/null +++ b/.changeset/fix-streamable-http-content-type.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/server': patch +--- + +Allow Streamable HTTP clients to send valid JSON POST requests without requiring `Content-Type: application/json`. diff --git a/packages/middleware/node/test/streamableHttp.test.ts b/packages/middleware/node/test/streamableHttp.test.ts index c427aa2eea..656570de88 100644 --- a/packages/middleware/node/test/streamableHttp.test.ts +++ b/packages/middleware/node/test/streamableHttp.test.ts @@ -286,6 +286,20 @@ describe('Zod v4', () => { expect(response.headers.get('mcp-session-id')).toBeDefined(); }); + it('should initialize from valid JSON without Content-Type header', async () => { + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + Accept: 'application/json, text/event-stream' + }, + body: new TextEncoder().encode(JSON.stringify(TEST_MESSAGES.initialize)) + }); + + expect(response.status).toBe(200); + expect(response.headers.get('content-type')).toBe('text/event-stream'); + expect(response.headers.get('mcp-session-id')).toBeDefined(); + }); + it('should reject second initialization request', async () => { // First initialize const sessionId = await initializeServer(); @@ -680,10 +694,9 @@ describe('Zod v4', () => { expectErrorResponse(errorData, -32_000, /Client must accept both application\/json and text\/event-stream/); }); - it('should reject unsupported Content-Type', async () => { + it('should reject invalid JSON regardless of Content-Type', async () => { sessionId = await initializeServer(); - // Try POST with text/plain Content-Type const response = await fetch(baseUrl, { method: 'POST', headers: { @@ -694,9 +707,9 @@ describe('Zod v4', () => { body: 'This is plain text' }); - expect(response.status).toBe(415); + expect(response.status).toBe(400); const errorData = await response.json(); - expectErrorResponse(errorData, -32_000, /Content-Type must be application\/json/); + expectErrorResponse(errorData, -32_700, /Parse error.*Invalid JSON/); }); it('should handle JSON-RPC batch notification messages with 202 response', async () => { diff --git a/packages/server/src/server/streamableHttp.ts b/packages/server/src/server/streamableHttp.ts index fd3563a077..b8ac67ed37 100644 --- a/packages/server/src/server/streamableHttp.ts +++ b/packages/server/src/server/streamableHttp.ts @@ -629,12 +629,6 @@ export class WebStandardStreamableHTTPServerTransport implements Transport { ); } - const ct = req.headers.get('content-type'); - if (!ct || !ct.includes('application/json')) { - this.onerror?.(new Error('Unsupported Media Type: Content-Type must be application/json')); - return this.createJsonErrorResponse(415, -32_000, 'Unsupported Media Type: Content-Type must be application/json'); - } - const request = req; let rawMessage; diff --git a/packages/server/test/server/streamableHttp.test.ts b/packages/server/test/server/streamableHttp.test.ts index 7a23dd56bb..a05bc17478 100644 --- a/packages/server/test/server/streamableHttp.test.ts +++ b/packages/server/test/server/streamableHttp.test.ts @@ -301,7 +301,24 @@ describe('Zod v4', () => { expectErrorResponse(errorData, -32_000, /Not Acceptable/); }); - it('should reject request with wrong Content-Type header', async () => { + it('should accept valid JSON without Content-Type header', async () => { + const request = new Request('http://localhost/mcp', { + method: 'POST', + headers: { + Accept: 'application/json, text/event-stream' + }, + body: new TextEncoder().encode(JSON.stringify(TEST_MESSAGES.initialize)) + }); + + expect(request.headers.get('content-type')).toBeNull(); + + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); + expect(response.headers.get('mcp-session-id')).toBeDefined(); + }); + + it('should not reject valid JSON based on Content-Type header', async () => { const request = new Request('http://localhost/mcp', { method: 'POST', headers: { @@ -312,9 +329,8 @@ describe('Zod v4', () => { }); const response = await transport.handleRequest(request); - expect(response.status).toBe(415); - const errorData = await response.json(); - expectErrorResponse(errorData, -32_000, /Unsupported Media Type/); + expect(response.status).toBe(200); + expect(response.headers.get('mcp-session-id')).toBeDefined(); }); it('should reject invalid JSON', async () => { @@ -844,23 +860,21 @@ describe('Zod v4', () => { expect(error?.message).toContain('Not Acceptable'); }); - it('should call onerror for unsupported Content-Type', async () => { + it('should not call onerror for valid JSON without Content-Type', async () => { const request = new Request('http://localhost/mcp', { method: 'POST', headers: { - Accept: 'application/json, text/event-stream', - 'Content-Type': 'text/plain' + Accept: 'application/json, text/event-stream' }, - body: JSON.stringify(TEST_MESSAGES.initialize) + body: new TextEncoder().encode(JSON.stringify(TEST_MESSAGES.initialize)) }); + expect(request.headers.get('content-type')).toBeNull(); + const response = await transport.handleRequest(request); - expect(response.status).toBe(415); - expect(errors.length).toBeGreaterThan(0); - const error = errors[0]; - expect(error).toBeDefined(); - expect(error?.message).toContain('Unsupported Media Type'); + expect(response.status).toBe(200); + expect(errors).toHaveLength(0); }); it('should call onerror for server not initialized', async () => {