Skip to content

v2025-http: two conformance defects on the 2025 Streamable HTTP binding - #202

Merged
dinstein merged 2 commits into
mainfrom
v2025-http
Aug 6, 2026
Merged

v2025-http: two conformance defects on the 2025 Streamable HTTP binding#202
dinstein merged 2 commits into
mainfrom
v2025-http

Conversation

@dinstein

@dinstein dinstein commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Two MCP conformance defects found reviewing the 2025 family (2025-06-18, 2025-11-25) rather
than 2026-07-28, which rounds 1-16 covered. Each was independently verified by a subagent before any
code was written.

  • 1. MCP-Protocol-Version naming a version this server does not speak is refused (400)
  • 2. The SSE retry field is honoured on the one path that reconnects

dinstein and others added 2 commits August 6, 2026 10:26
Every MCP revision that defines MCP-Protocol-Version requires a server to
answer 400 when the value names a version it does not speak — 2025-06-18 and
2025-11-25 as a bare 400, 2026-07-28 as 400 plus the supported list. This face
never judged the value at all. The one check compared it against the version
the body's _meta declares, and a request without _meta made `declared == ""`
skip the comparison entirely — so the rule was unenforced for precisely the
≤ 2025-11-25 generations that demand it, since only 2026 sends that _meta.

`MCP-Protocol-Version: banana` did not merely earn a 200: on initialize it
minted a session. DELETE was worse off still, running no header checks at all.

checkProtocolVersion refuses anything outside mcp.SupportedVersions with
-32022 and the supported list, which the existing status mapping renders as
400, and DELETE now calls it too. Absence stays allowed — that is the separate
carve-out for clients predating the header, and the comment justifying the old
behaviour cited it as though it covered both cases. Reading the accepted set
from mcp.SupportedVersions is what keeps "would we negotiate this" and "would
we accept this in a header" one answer; a test drives that list directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MCP 2025-11-25 makes waiting out an SSE `retry` field a MUST — a server that
closes a connection without ending the stream asks for a delay, and the client
"MUST respect the retry field". It is the only revision carrying the rule
(2025-06-18 draws no connection-versus-stream distinction, 2026-07-28 removed
resumable streams), but 2025-11-25 is what mcp.ProtocolVersion names.

sseScanner parsed the field and discarded it, under a comment saying the
package used its own bounded backoff instead. That backoff lives in
streamLoop, which no production configuration opens. The path the shipped
product does reach is the per-POST resume, and it had no delay at all:
measured at 117µs against a server asking for 250ms.

The hint is kept now, and the two reconnect sites spend it differently
because only one has a caller waiting. The out-of-call loop honours it in
full, max-ed with its own backoff. The per-call resume runs under the
caller's deadline, so waitBeforeResume takes the wait when it fits and
otherwise abandons the resume — sleeping out a long hint would turn an answer
into a timeout, and coming back early is the one option the MUST forbids.

An unparseable retry leaves the previous hint standing rather than clearing
it, per the SSE standard; clearing would be a reconnect storm dressed as
conformance. Both new wire-level tests fail with the guard removed, and
FuzzSSEScanner ran 7.8M execs over the changed parser with no findings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dinstein
dinstein marked this pull request as ready for review August 6, 2026 02:36
@dinstein
dinstein merged commit f0640b8 into main Aug 6, 2026
3 checks passed
@dinstein
dinstein deleted the v2025-http branch August 6, 2026 02:41
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