Streamable proxy: Modern MCP message-shape enforcement (deferred from #5830)
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 2.2k
- Forks
- 300
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 184
Description
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/handleBatchRequestpath is Legacy-only by construction today. - Reject client-sent responses (client POSTing
result/error).handleNotificationOrClientResponseaccepts them today (Legacy behavior). Must not break backend→client response delivery through the dispatcher (MRTRInputRequiredResultpass-through). - GET/DELETE → 405 under Modern (
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-Version header (a client must not be able to flip enforcement by spoofing it). Options discussed:
- Reuse the existing
--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. - 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
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).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with RFC THV-0081 and the architecture section on Modern message-shape enforcement, then inspect the streamable proxy's resolveSessionForBatch, handleBatchRequest, and handleNotificationOrClientResponse paths. Review streamable/utils.go and the existing --stateless wiring before choosing a deployment-level mode signal. Done means Modern rejects batches, client responses, and GET/DELETE as specified without breaking dispatcher pass-through, and the session-TTL issue is addressed.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- backend-api-design, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100