feat: SEP-2260 stream-based enforcement of client receive-side request association - #1055
Open
gocamille wants to merge 10 commits into
Open
feat: SEP-2260 stream-based enforcement of client receive-side request association#1055gocamille wants to merge 10 commits into
gocamille wants to merge 10 commits into
Conversation
Replaces the has_pending_outbound_request bool on enforce_peer_request_association with PeerRequestAssociation, so a stream-separating transport can report per-request association (modelcontextprotocol#1033). Behavior-preserving: the event loop still passes the coarse signal as Unknown.
…nt (modelcontextprotocol#1033) The worker attaches an InboundStreamOrigin extension to each inbound server request: Unassociated for the standalone GET stream, OutboundRequest(id) for a POST's SSE stream. Mirror of the OriginatingRequestId marker used by the server side.
…origin (modelcontextprotocol#1033) The event loop maps InboundStreamOrigin plus the in-flight responder pool to PeerRequestAssociation: restricted requests arriving on the standalone GET stream are now rejected with -32602 even while unrelated outbound requests are in flight.
…tocol#1033) A POST SSE stream resumed via GET + Last-Event-ID (SEP-1699) reconnects beneath execute_sse_stream, so requests replayed after a resume keep their OutboundRequest origin. Pins the layering invariant: hoisting reconnection above the marker attach point would wrongly reject associated requests with -32602.
…sep-2260-stream-enforcement
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the stream-based enforcement described in SEP-2260.
Tracking issue: #1033 (follow-up to #873 / #1029)
Motivation and Context
PR #1029 implemented the receive-side SHOULD ("Clients receiving server-to-client requests with no associated outbound request SHOULD respond with a
-32602error") with a coarse check: reject restricted requests (sampling/createMessage,elicitation/create,roots/list;pingexempt) only when the client has no outbound request in flight. It couldn't tell which request a server request belongs to. SEP-2260 defines no wire field, so association is only observable at the transport layer via which HTTP response stream a message arrived on.This PR closes that gap for streamable HTTP. Now, a restricted request arriving on the GET stream (or on the SSE stream of a POST whose request is no longer in flight) is rejected with
-32602even when unrelated requests are in flight.How
InboundStreamOriginmarker (Unassociated|OutboundRequest(RequestId)) to each inbound request's non-serializedExtensions, recording whether it arrived on the standalone GET stream or a specific POST's SSE response stream. The marker survives SSE reconnection/resumption.PeerRequestAssociationenum (Associated|Unassociated|Unknown { has_pending_outbound_request }) passed toServiceRole::enforce_peer_request_association.RoleClient::enforce_peer_request_associationrejectsUnassociatedrestricted requests with-32602;Associatedis accepted;Unknownfalls back to the coarse in-flight check from feat: route SEP-2260 associated server requests to the originating SSE stream #1029.2026-07-28; older peers keep legacy behavior.Unknown.How Has This Been Tested?
InboundStreamOrigin→PeerRequestAssociationmapping and forenforce_peer_request_associationacross all three association states.execute_sse_streammarks inbound requests with their stream origin (responses are untouched) and that the origin marker survives SSE resumption/reconnect.test_sep_2260_stream_enforcement.rs): a restricted request on the originating POST's SSE stream reaches the handler; the same request on the standalone GET stream is rejected with-32602while an unrelated request is in flight. The scripted server negotiates2026-07-28and creates a session id. It's deliberately non-conforming (SEP-2567 removes sessions and the GET endpoint at that version), since receive-side enforcement exists to defend against exactly such servers and rmcp's client tolerates the session id and opens the GET stream.Breaking Changes
None. New public API:
PeerRequestAssociationand theServiceRole::enforce_peer_request_associationhook (with a permissive default). Behavior changes only for streamable HTTP clients negotiating2026-07-28+against servers that send restricted requests on the wrong stream.Types of changes
Checklist
I have read the MCP Documentation
My code follows the repository's style guidelines
New and existing tests pass locally
I have added appropriate error handling
I have added or updated documentation as needed
Additional context
Possible follow-up: skip session tracking / the standalone GET stream entirely when the negotiated version is ≥
2026-07-28, making the unassociated-GET-stream state unreachable.