Skip to content

Streamable proxy: Modern MCP message-shape enforcement (deferred from #5830) #5842

Description

@jhrozek

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions