fix(mcp): offer both media types and unframe an event-stream answer (ADR-181) - #835
Conversation
The transport sent Accept: application/json alone, and the public Streamable-HTTP servers answer that with 406 — measured against mcp.deepwiki.com, mcp.context7.com and learn.microsoft.com/api/mcp, all three, before this change. The spec requires a client to offer both media types on every POST, and the reference SDKs enforce it. The client now offers both and reads an answer framed as text/event-stream like a plain JSON one: the single JSON-RPC response is unwrapped by the SSE rules that matter for one exchange (events split on a blank line, data: lines of one event join with a newline, framing and comment lines ignored, CRLF accepted); a server notification on the same stream is passed over; a stream with no response is a malformed answer and says which way. Nothing else moves — no stream is held open, no server request is answered, the size cap and the operation budget apply unchanged. ADR-181 records it and amends ADR-116's transport section and ADR-161's edge. Conformance suite: one positive case (a framed tool answer reads like a plain one) and two failing shapes; transport tests pin the header and the framing rules. Closes #833 Assisted-by: claude-code:claude-fable-5 Agent-Session: https://claude.ai/code/session_0124WZK5UoAM9oYQoSYF1Y8z Agent-Host: 32116e Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
There was a problem hiding this comment.
Automated approval for maintainer PR
All automated quality gates passed. See SECURITY_CONTROLS.md for compensating controls.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #835 +/- ##
=========================================
Coverage 86.74% 86.75%
- Complexity 9865 9880 +15
=========================================
Files 573 573
Lines 31920 31949 +29
=========================================
+ Hits 27689 27717 +28
- Misses 4231 4232 +1
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
NewlineAfterStatementRector wants the comment that follows the `data:` branch separated from it by an empty line. Assisted-by: claude-code:claude-fable-5 Agent-Session: https://claude.ai/code/session_0124WZK5UoAM9oYQoSYF1Y8z Agent-Host: 32116e Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
There was a problem hiding this comment.
Automated approval for maintainer PR
All automated quality gates passed. See SECURITY_CONTROLS.md for compensating controls.
|
Self-review: 28390be The review this pull request demands is unsatisfiable (Copilot quota wall or repeated bot failures on this head). Per the documented fallback, the diff on this head was reviewed by the PR author; this comment is the on-the-record attestation the merge gate reads back. It stops matching on the next push. |
Both ADR-180 (#832) and ADR-181 appended to the ADR toctree; the merge keeps both entries in order. Assisted-by: claude-code:claude-fable-5 Agent-Session: https://claude.ai/code/session_0124WZK5UoAM9oYQoSYF1Y8z Agent-Host: 32116e Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
|
There was a problem hiding this comment.
Automated approval for maintainer PR
All automated quality gates passed. See SECURITY_CONTROLS.md for compensating controls.
|
Self-review: 41bb809 The review this pull request demands is unsatisfiable (Copilot quota wall or repeated bot failures on this head). Per the documented fallback, the diff on this head was reviewed by the PR author; this comment is the on-the-record attestation the merge gate reads back. It stops matching on the next push. |



Closes #833.
What
The MCP client sent
Accept: application/jsonalone; the public Streamable-HTTP servers answer that with 406 ("Client must accept both application/json and text/event-stream") — measured before this change againstmcp.deepwiki.com/mcp,mcp.context7.com/mcpandlearn.microsoft.com/api/mcp, all three. The transport now offers both media types, as the spec requires of every POST, and reads an answer a server frames astext/event-streamlike a plain JSON one: the single JSON-RPC response is unwrapped (events split on a blank line, thedata:lines of one event join with a newline,event:/id:/retry:/comment lines ignored, CRLF accepted), a server notification on the same stream is passed over, and a stream with no response is a malformed answer that says which way ("no message" vs "no response to the request").Nothing else moves: no stream is held open or resumed, no server-initiated request is answered, the response size cap and the operation budget (ADR-170) apply unchanged, stdio stays out of scope. The unsupported-content-type refusal keeps its exception and code; only its message stops claiming "JSON only".
Records
ADR-181 (new) — amends ADR-116's transport section ("HTTP only" was right about what the client does, wrong about what it says) and ADR-161's "no SSE" edge (now "no live stream").
McpServers.rstgains a "What the client speaks" paragraph. CHANGELOG under Unreleased/Changed.Tests
Transport: the header is pinned; a CRLF, multi-
data:,event:+id:-framed response unframes to the result; a server notification before the response is passed over; an empty stream and a notification-only stream are malformed answers;text/htmlis still refused with the same code. Conformance suite (ADR-161): one positive case — an event-stream framed tool answer reads like a plain one — and two new failing shapes inserverFailures.McpTestServerrecords theAcceptheader.Gates run locally (one matrix cell, PHP 8.5 / TYPO3 14)
cgl ✓ · phpstan (level 10) ✓ · unit (7226 tests, includes the conformance suites) ✓ · changelog ✓. Rector cannot run in this worktree (
.Buildresolved under 8.4, seeTests/AGENTS.md); the functional suite is untouched by this change and left to CI.Assisted by claude-code:claude-fable-5 — Session