Skip to content

fix(sdk): serve a legacy SSE message post whatever its Accept header says - #957

Closed
frontegg-david wants to merge 1 commit into
mainfrom
fix/legacy-sse-message-any-accept
Closed

frontegg-david wants to merge 1 commit into
mainfrom
fix/legacy-sse-message-any-accept

Conversation

@frontegg-david

Copy link
Copy Markdown
Contributor

What and why

Problem

The Node server serves the legacy HTTP+SSE transport (MCP 2024-11-05) by default. With http.entryPath: '/mcp', GET /mcp/sse opens the stream and names /mcp/message?sessionId=… as the endpoint. A post to that endpoint:

  • with Accept: application/json or Accept: text/event-stream gets 202, and the answer arrives on the stream (correct);
  • with Accept: */* or Accept: application/* gets Express's 404 Cannot POST /mcp/message.

Accept: */* is what Node's fetch (and curl) sends when the client sets no Accept header, so the official SDK's SSEClientTransport (@modelcontextprotocol/sdk 1.32.x) cannot connect: its first post, initialize, gets the 404. The 2024-11-05 transport asks nothing of the Accept header on these posts.

Reproduction

# terminal 1: open the stream; it prints `event: endpoint` / `data: /mcp/message?sessionId=<id>`
curl -N -H 'Accept: text/event-stream' http://localhost:3000/mcp/sse

# terminal 2: post initialize to that endpoint (curl sends Accept: */* by default)
curl -i -X POST 'http://localhost:3000/mcp/message?sessionId=<id>' \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'
# before: HTTP/1.1 404, "Cannot POST /mcp/message"
# after:  HTTP/1.1 202 Accepted, and the result arrives on the stream
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';

const client = new Client({ name: 'repro', version: '1.0.0' });
await client.connect(new SSEClientTransport(new URL('http://localhost:3000/mcp/sse')));
// before: Error POSTing to endpoint (HTTP 404): <!DOCTYPE html> ... Cannot POST /mcp/message

Cause

decideIntent (libs/sdk/src/common/utils/decide-request-intent.utils.ts) ran Streamable HTTP's Accept check on every POST, /message included: a post that listed neither application/json nor text/event-stream was decided unknown before the legacy /message rule was reached. For initialize, the http:request router hands an unknown decision to the next middleware, which is Express's 404.

A request with no Accept header at all was already routed, because the router tolerates a missing Accept. It only looked broken because curl and fetch fill in */*.

Fix

decideIntent skips the Accept check for the legacy /message channel only. A post there is answered 202 with its response on the SSE stream, so any Accept value, or none, is routed to the legacy SSE handler, whatever tolerateMissingAccept says. Legacy SSE that is turned off is still refused, and a /message post without a session id is still not routed.

Deliberately unchanged

  • Streamable HTTP: a POST to the entry path keeps its current Accept rules. */*, application/* and text/plain are still not served. Both media types listed still routes to Streamable HTTP, and the existing JSON and missing-Accept routing is unchanged. New tests pin this at the unit level and against a real server.
  • MCP 2026-07-28: the router claims these requests before the session-era decision is consulted, so this change cannot reach them. A test pins that a 2026-07-28 tools/list on the entry path is still served sessionless.

The change cherry-picks cleanly onto release/1.10.x: the touched files are the same on both branches.

User-facing change

  • What changed: the legacy HTTP+SSE message endpoint (<entryPath>/message?sessionId=…) accepts a post whatever its Accept header says.
  • Before → after: Accept: */*, application/* or another value that lists neither application/json nor text/event-stream used to get 404 Cannot POST <entryPath>/message. It now gets 202 Accepted, with the response on the SSE stream. The official SDK's SSEClientTransport now connects with its default headers.
  • frontmcp.dev pages affected (if known): none known; no page documents an Accept requirement for the legacy message endpoint.
  • Skills catalog entries updated: none. The catalog states no Accept rule for this endpoint, and the 'legacy' preset already promises compatibility with all MCP clients.

Tests

New:

  • libs/sdk/src/common/utils/__tests__/decide-request-intent.accept.spec.ts: unit tests of the routing decision.
    • A legacy /message post routes to legacy SSE with application/json, text/event-stream, both, */*, application/*, text/plain or no Accept, whether or not a missing Accept is tolerated. It is still refused when legacy SSE is off, and still not routed without a session id.
    • The entry path keeps its current Streamable HTTP rules (listed above).
  • libs/sdk/src/transport/__tests__/legacy-sse.message-accept.spec.ts: a real in-process server with http.entryPath: '/mcp', in the style of verified-session.legacy-sse.spec.ts.
    • The stream names /mcp/message, which answers 202 plus the result on the stream for every Accept value above.
    • A whole session (initialize, initialized, tools/call) posted with */* is served.
    • The official SDK Client with SSEClientTransport and default headers connects, lists tools and calls one.
    • Streamable HTTP initialize with both media types gets 200 and a session; with */* or application/* it still gets 404.
    • A 2026-07-28 tools/list is served sessionless.
  • apps/e2e/demo-e2e-standalone/e2e/legacy-sse-sdk-client.e2e.spec.ts: the official SDK's SSEClientTransport, with default headers, against the spawned demo server at FRONTMCP_HTTP_ENTRY_PATH=/mcp. It connects, lists tools and calls parent-hello.

Before the fix, the new specs failed the way the report describes. The legacy */* and application/* posts got 404, the SDK client threw Error POSTing to endpoint (HTTP 404), and the e2e failed the same way. All the Streamable HTTP and 2026-07-28 cases passed on the unmodified code.

Run after the fix:

Command Result
yarn nx test sdk 561/561 suites, 7182/7182 tests passed
yarn nx run-many -t lint,typecheck -p sdk demo-e2e-standalone passed (no new warnings)
yarn nx test demo-e2e-standalone 12/12 suites, 50/50 tests passed
yarn nx test demo-e2e-protocol-20260728 13/13 suites, 145/145 tests passed
yarn nx test demo-e2e-testing --testPathPatterns=hooks-and-sse 3/3 tests passed (the @frontmcp/testing SSE client)
node scripts/fix-unused-imports.mjs no changes

…says

The HTTP+SSE transport (MCP 2024-11-05) answers a post to its message
endpoint with 202 and puts the response on the SSE stream, so it asks
nothing of the post's Accept header. The router applied Streamable HTTP's
Accept rule to /message too: a post that listed neither application/json
nor text/event-stream, such as the Accept: */* that Node's fetch sends for
the official SDK's SSEClientTransport, decided as unknown and fell through
to Express's 404 Cannot POST /mcp/message.

decideIntent now skips the Accept check for the legacy /message channel
only. A Streamable HTTP post to the entry path keeps its Accept rules, and
MCP 2026-07-28 routing does not consult them.
@coderabbitai

coderabbitai Bot commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Performance Test Results

Status: ✅ All tests passed

Summary

Project Tests Passed Warnings Failed Leaks
✅ demo-e2e-agents 4 4 0 0 0
✅ demo-e2e-cache 11 11 0 0 0
✅ demo-e2e-codecall 4 4 0 0 0
✅ demo-e2e-config 4 4 0 0 0
✅ demo-e2e-direct 3 3 0 0 0
✅ demo-e2e-elicitation 1 1 0 0 0
✅ demo-e2e-errors 4 4 0 0 0
✅ demo-e2e-hooks 3 3 0 0 0
✅ demo-e2e-multiapp 4 4 0 0 0
✅ demo-e2e-notifications 3 3 0 0 0
✅ demo-e2e-openapi 2 2 0 0 0
✅ demo-e2e-providers 4 4 0 0 0
✅ demo-e2e-public 4 4 0 0 0
✅ demo-e2e-redis 15 15 0 0 0
✅ demo-e2e-remember 4 4 0 0 0
✅ demo-e2e-remote 5 5 0 0 0
✅ demo-e2e-serverless 2 2 0 0 0
✅ demo-e2e-skills 15 15 0 0 0
✅ demo-e2e-standalone 2 2 0 0 0
✅ demo-e2e-transport-recreation 3 3 0 0 0
✅ demo-e2e-ui 4 4 0 0 0

Total: 101 tests across 21 projects

📊 View full report in workflow run


Generated at: 2026-10-25T19:35:13.210Z
Commit: 0b6351ab

@frontegg-david

Copy link
Copy Markdown
Contributor Author

Superseded by #958, which targets release/1.10.x so the fix ships in the next 1.10 release; the cherry-pick workflow brings it to main after merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant