From d03bf5cbaba420b5b7e8c125f807e32737f4a2a6 Mon Sep 17 00:00:00 2001 From: Trailgenic Date: Mon, 13 Jul 2026 17:17:48 -0700 Subject: [PATCH 1/2] Relax MCP Accept negotiation --- app/mcp/route.ts | 12 ++++++------ tests/mcp.test.ts | 7 ++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/mcp/route.ts b/app/mcp/route.ts index f7fb1d5..c3ebb67 100644 --- a/app/mcp/route.ts +++ b/app/mcp/route.ts @@ -6,7 +6,7 @@ export const runtime = 'nodejs' const DEFAULT_ALLOWED_ORIGINS = ['https://ellaentity.ai', 'https://www.ellaentity.ai', 'https://mcp.ellaentity.ai'] const ALLOW_METHODS = 'POST, OPTIONS' const ALLOW_HEADERS = 'Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version' -const REQUIRED_ACCEPT_TYPES = ['application/json', 'text/event-stream'] as const +const ACCEPTED_RESPONSE_TYPES = ['application/json', 'text/event-stream'] as const function allowedOrigins() { return process.env.MCP_ALLOWED_ORIGINS?.split(',').map((origin) => origin.trim()).filter(Boolean) ?? DEFAULT_ALLOWED_ORIGINS @@ -62,14 +62,14 @@ function hasJsonContentType(request: Request) { return request.headers.get('content-type')?.toLowerCase().includes('application/json') ?? false } -function hasRequiredAcceptTypes(request: Request) { +function hasCompatibleAcceptType(request: Request) { const accept = request.headers.get('accept')?.toLowerCase() if (!accept) { - return false + return true } - return REQUIRED_ACCEPT_TYPES.every((type) => accept.includes(type)) + return accept.includes('*/*') || ACCEPTED_RESPONSE_TYPES.some((type) => accept.includes(type)) } export function OPTIONS(request: Request) { @@ -108,8 +108,8 @@ export async function POST(request: Request) { return jsonRpcError(request, 415, -32600, 'Content-Type must be application/json') } - if (!hasRequiredAcceptTypes(request)) { - return jsonRpcError(request, 406, -32600, 'Accept must include application/json and text/event-stream') + if (!hasCompatibleAcceptType(request)) { + return jsonRpcError(request, 406, -32600, 'Accept must include application/json, text/event-stream, or */*') } try { diff --git a/tests/mcp.test.ts b/tests/mcp.test.ts index b8f6f34..3399690 100644 --- a/tests/mcp.test.ts +++ b/tests/mcp.test.ts @@ -64,9 +64,10 @@ test('GET and OPTIONS implement method and CORS behavior', async () => { test('POST validates content negotiation and origins', async () => { const body = { jsonrpc: '2.0', id: 1, method: 'ping', params: {} } - assert.equal((await POST(request('POST', body, { accept: '' }))).status, 406) - assert.equal((await POST(request('POST', body, { accept: 'application/json' }))).status, 406) - assert.equal((await POST(request('POST', body, { accept: 'text/event-stream' }))).status, 406) + assert.notEqual((await POST(request('POST', body, { accept: '' }))).status, 406) + assert.notEqual((await POST(request('POST', body, { accept: 'application/json' }))).status, 406) + assert.notEqual((await POST(request('POST', body, { accept: 'text/event-stream' }))).status, 406) + assert.notEqual((await POST(request('POST', body, { accept: '*/*' }))).status, 406) assert.equal((await POST(request('POST', body, { accept: 'text/plain' }))).status, 406) assert.equal((await POST(request('POST', body, { 'content-type': 'text/plain' }))).status, 415) assert.equal((await POST(request('POST', body, { origin: 'https://evil.example' }))).status, 403) From f0a3829dddd17998974ba53733192155b5db2287 Mon Sep 17 00:00:00 2001 From: Trailgenic Date: Mon, 13 Jul 2026 17:27:44 -0700 Subject: [PATCH 2/2] Align MCP Accept header handling --- app/mcp/route.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/mcp/route.ts b/app/mcp/route.ts index c3ebb67..48b847d 100644 --- a/app/mcp/route.ts +++ b/app/mcp/route.ts @@ -63,13 +63,17 @@ function hasJsonContentType(request: Request) { } function hasCompatibleAcceptType(request: Request) { - const accept = request.headers.get('accept')?.toLowerCase() + const accept = request.headers.get('accept') - if (!accept) { + if (!accept?.trim()) { return true } - return accept.includes('*/*') || ACCEPTED_RESPONSE_TYPES.some((type) => accept.includes(type)) + const acceptedTypes = accept.toLowerCase().split(',').map((type) => type.trim()) + + return acceptedTypes.some( + (type) => type === '*/*' || ACCEPTED_RESPONSE_TYPES.some((acceptedType) => type.startsWith(acceptedType)), + ) } export function OPTIONS(request: Request) {