Skip to content

Serve the 2026-07-28 protocol over stdio: decide the era from the opening request - #3152

Merged
maxisbey merged 4 commits into
mainfrom
stdio-serve-2026
Jul 24, 2026
Merged

maxisbey merged 4 commits into
mainfrom
stdio-serve-2026

Serve the 2026-07-28 protocol over stdio by deciding the era from the…

fee8c52
Select commit
Loading
Failed to load commit list.
Claude / Claude Code Review completed Jul 23, 2026 in 24m 42s

Code review found 2 important issues

Found 5 candidates, confirmed 4. See review comments for details.

Details

Severity Count
🔴 Important 2
🟡 Nit 2
🟣 Pre-existing 0
Severity File:Line Issue
🔴 Important src/mcp/server/runner.py:658-666 Replay stream drops sender contextvars propagation (last_context)
🔴 Important docs/whats-new.md:194 Stale admonition: handlers/subscriptions.md still says listen is rejected over stdio
🟡 Nit src/mcp/server/runner.py:630-636 Stale _has_modern_envelope docstring describes removed lock-on-success model
🟡 Nit src/mcp/server/runner.py:658-687 Unbounded pre-request frame buffering in _replay_from_opening_request

Annotations

Check failure on line 666 in src/mcp/server/runner.py

See this annotation in the file changed.

@claude claude / Claude Code Review

Replay stream drops sender contextvars propagation (last_context)

The new replay indirection silently breaks sender contextvars propagation for every request served through `serve_dual_era_loop`: `replay_then_relay` iterates the original `ContextReceiveStream` (discarding the per-message context envelope) and re-sends bare items through a plain anyio memory stream that has no `last_context` attribute, so the dispatcher's `sender_ctx` is now always `None` on this path, in both eras. This regresses a deliberate, test-pinned feature (e.g. `Client(server, mode="le

Check failure on line 194 in docs/whats-new.md

See this annotation in the file changed.

@claude claude / Claude Code Review

Stale admonition: handlers/subscriptions.md still says listen is rejected over stdio

This PR deletes the stdio refusal of `subscriptions/listen` (and correctly updates the caveat here in whats-new.md), but the canonical Subscriptions page still documents the removed behavior: `docs/handlers/subscriptions.md` lines 55-60 keep the "Streamable HTTP only, for now" admonition saying a 2026-07-28 stdio connection rejects the method with METHOD_NOT_FOUND. Please remove or rewrite that warning block in this PR — AGENTS.md requires the owning docs page to be updated in the same PR as a u

Check warning on line 636 in src/mcp/server/runner.py

See this annotation in the file changed.

@claude claude / Claude Code Review

Stale _has_modern_envelope docstring describes removed lock-on-success model

The docstring on `_has_modern_envelope` still ends with "and, like every failed classification, locks no era" — a description of the first-success-locks model this PR removes. Under the new design this helper *is* the era decision (the `opens_modern` check on the opening request), and a half-built envelope on the opening frame now does permanently open a 2026 connection, as this PR's own tests pin. Trimming the stale clause keeps the one helper documenting the era rule from asserting the opposit

Check warning on line 687 in src/mcp/server/runner.py

See this annotation in the file changed.

@claude claude / Claude Code Review

Unbounded pre-request frame buffering in _replay_from_opening_request

The peek loop in `_replay_from_opening_request` appends every non-request frame (notifications, responses, `Exception` items from unparseable input) into the unbounded `peeked` list until the first JSON-RPC request arrives, draining the transport's zero-buffer stream eagerly — so a peer that streams non-request frames without ever sending a request grows server memory without bound, where the pre-PR dispatcher consumed those frames incrementally under transport backpressure. A small cap on the p