Follow-up to #5830 / #5839. The routing half of #5830 (per-request Modern/Legacy classification → stateless routing) shipped in #5839. This issue tracks the message-shape enforcement half, deferred deliberately.
Background
Under the Modern (2026-07-28) revision the HTTP transport narrows what a POST may be. These rules apply to messages that cannot be classified per-request (no _meta, no body, or illegal-by-definition), so they can't ride on the per-request classifier the way routing does — they need a deployment-level signal.
Changes
- Reject JSON-RPC batches for Modern. Batches don't exist under Modern; the streamable proxy's
resolveSessionForBatch / handleBatchRequest path is Legacy-only by construction today.
- Reject client-sent responses (client POSTing
result/error). handleNotificationOrClientResponse accepts them today (Legacy behavior). Must not break backend→client response delivery through the dispatcher (MRTR InputRequiredResult pass-through).
- GET/DELETE → 405 under Modern (
Mcp-Session-Id/Last-Event-ID ignored, not errors). GET is already 405 in this proxy; DELETE currently tears down a session.
The mode signal
Enforcement needs a Legacy/Modern signal that is not driven off the client-controlled MCP-Protocol-Version header (a client must not be able to flip enforcement by spoofing it). Options discussed:
- Reuse the existing
--stateless run flag — the streamable proxy ignores it today; teaching it to enforce Modern shape when set adds no new user-facing surface. Preferred if/when a real consumer needs Modern-only enforcement.
- A dedicated proxy config field + Option — avoid landing it inert (no wiring = dead config).
Decide the wiring story before building, so enforcement is reachable by a real deployment rather than tests only.
Related finding (from #5839 review)
handleNotificationOrClientResponse refreshes a session's TTL using a client-supplied Mcp-Session-Id that is never classified or ownership-checked — a client sending only notifications/responses can keep an arbitrary session it knows the id of alive. Does not break the #5839 confidentiality invariant (that path never touches waiters), but belongs to this Modern-aware notification/response work.
Also worth folding in
- The early
isSupportedMCPVersion gate (streamable/utils.go, returns true today) is now redundant with the classifier and a latent landmine — reconcile or remove.
Design: RFC THV-0081; arch doc §"Modern message-shape enforcement" (#5808).
Follow-up to #5830 / #5839. The routing half of #5830 (per-request Modern/Legacy classification → stateless routing) shipped in #5839. This issue tracks the message-shape enforcement half, deferred deliberately.
Background
Under the Modern (2026-07-28) revision the HTTP transport narrows what a POST may be. These rules apply to messages that cannot be classified per-request (no
_meta, no body, or illegal-by-definition), so they can't ride on the per-request classifier the way routing does — they need a deployment-level signal.Changes
resolveSessionForBatch/handleBatchRequestpath is Legacy-only by construction today.result/error).handleNotificationOrClientResponseaccepts them today (Legacy behavior). Must not break backend→client response delivery through the dispatcher (MRTRInputRequiredResultpass-through).Mcp-Session-Id/Last-Event-IDignored, not errors). GET is already 405 in this proxy; DELETE currently tears down a session.The mode signal
Enforcement needs a Legacy/Modern signal that is not driven off the client-controlled
MCP-Protocol-Versionheader (a client must not be able to flip enforcement by spoofing it). Options discussed:--statelessrun flag — the streamable proxy ignores it today; teaching it to enforce Modern shape when set adds no new user-facing surface. Preferred if/when a real consumer needs Modern-only enforcement.Decide the wiring story before building, so enforcement is reachable by a real deployment rather than tests only.
Related finding (from #5839 review)
handleNotificationOrClientResponserefreshes a session's TTL using a client-suppliedMcp-Session-Idthat is never classified or ownership-checked — a client sending only notifications/responses can keep an arbitrary session it knows the id of alive. Does not break the #5839 confidentiality invariant (that path never toucheswaiters), but belongs to this Modern-aware notification/response work.Also worth folding in
isSupportedMCPVersiongate (streamable/utils.go, returnstruetoday) is now redundant with the classifier and a latent landmine — reconcile or remove.Design: RFC THV-0081; arch doc §"Modern message-shape enforcement" (#5808).