FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat: SEP-2260 stream-based enforcement of client receive-side request association by gocamille · Pull Request #1055 · modelcontextprotocol/rust-sdk · GitHub

feat: SEP-2260 stream-based enforcement of client receive-side request association - #1055

Merged
DaleSeo merged 11 commits into
modelcontextprotocol:mainfrom
gocamille:sep-2260-stream-enforcement
Jul 29, 2026
Merged

feat: SEP-2260 stream-based enforcement of client receive-side request association#1055
DaleSeo merged 11 commits into
modelcontextprotocol:mainfrom
gocamille:sep-2260-stream-enforcement

Conversation

Copy link
Copy Markdown
Contributor

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 -32602 error") with a coarse check: reject restricted requests (sampling/createMessage, elicitation/create, roots/list; ping exempt) 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-32602 even when unrelated requests are in flight.

How

  • The streamable HTTP client transport attaches an InboundStreamOrigin marker (Unassociated | OutboundRequest(RequestId)) to each inbound request's non-serialized Extensions, recording whether it arrived on the standalone GET stream or a specific POST's SSE response stream. The marker survives SSE reconnection/resumption.
  • The service layer translates the marker plus the in-flight responder pool into a PeerRequestAssociation enum (Associated | Unassociated | Unknown { has_pending_outbound_request }) passed to ServiceRole::enforce_peer_request_association.
  • RoleClient::enforce_peer_request_association rejects Unassociated restricted requests with -32602; Associated is accepted; Unknown falls back to the coarse in-flight check from feat: route SEP-2260 associated server requests to the originating SSE stream #1029.
  • Still gated on negotiated protocol ≥ 2026-07-28; older peers keep legacy behavior.
  • Transports without stream separation (stdio, in-process) attach no marker and yield Unknown.

How Has This Been Tested?

  • Unit tests for the InboundStreamOrigin → PeerRequestAssociation mapping and for enforce_peer_request_association across all three association states.
  • Transport tests verifying execute_sse_stream marks inbound requests with their stream origin (responses are untouched) and that the origin marker survives SSE resumption/reconnect.
  • End-to-end test over streamable HTTP (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 -32602 while an unrelated request is in flight. The scripted server negotiates 2026-07-28 and 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: PeerRequestAssociation and the ServiceRole::enforce_peer_request_association hook (with a permissive default). Behavior changes only for streamable HTTP clients negotiating 2026-07-28+ against servers that send restricted requests on the wrong stream.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

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.

gocamille requested a review from a team as a code owner July 27, 2026 16:21
github-actions Bot added T-test Testing related changes T-core Core library changes T-service Service layer changes T-transport Transport layer changes labels Jul 27, 2026

Copy link
Copy Markdown
Contributor

Nice. I should be able to review tomorrow if @DaleSeo @jamadeo don't get to it first!

gocamille force-pushed the sep-2260-stream-enforcement branch from fec6cae to c3bab3a Compare July 29, 2026 00:45
gocamille added 10 commits July 29, 2026 02:53
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.
…tprotocol#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.
Detect handler invocation via a channel asserted empty instead of a
panic in a spawned task (swallowed, cannot fail the test); surface
scripted-server misuse as transport errors rather than panics in the
transport task; bound the tail awaits with 5s timeouts. Correct the
header comment: a 2026-07-28 server minting a session id is not
spec-legal (SEP-2567 removes sessions and the GET endpoint) — the
scripted server is deliberately non-conforming, which is the point of
receive-side enforcement.
Rebase onto 3.0.0: RoleClient::PeerInfo is now ServerPeerInfo (modelcontextprotocol#1065)
and its constructor takes the protocol version directly.
gocamille force-pushed the sep-2260-stream-enforcement branch from c3bab3a to ac61fd8 Compare July 29, 2026 01:29
DaleSeo previously approved these changes Jul 29, 2026

DaleSeo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Thanks for implementing this, @gocamille! I only have a nitpick.

Comment thread crates/rmcp/src/service.rs Outdated
Co-authored-by: Dale Seo <5466341+DaleSeo@users.noreply.github.com>

Copy link
Copy Markdown
Contributor Author

Looks like adding suggestions requires another review, so re-requesting -- thank you again @DaleSeo !

DaleSeo merged commit 1d0473b into modelcontextprotocol:main Jul 29, 2026
22 checks passed
github-actions Bot mentioned this pull request Jul 30, 2026
howardjohn pushed a commit to agentgateway/agentgateway that referenced this pull request Jul 31, 2026
Bump to latest stable release
https://github.com/modelcontextprotocol/rust-sdk/releases/tag/rmcp-v3.1.0

```
Added
classify authorization-required errors (modelcontextprotocol/rust-sdk#1056)
add strict stateless protocol metadata validation (modelcontextprotocol/rust-sdk#1091)
SEP-2260 stream-based enforcement of client receive-side request association (modelcontextprotocol/rust-sdk#1055)
Fixed
(model) decode metadata-bearing input-required results affecting mrtr (modelcontextprotocol/rust-sdk#1097)
require metadata for modern HTTP requests (modelcontextprotocol/rust-sdk#1089)
honor supported_protocol_versions when negotiating initialize (modelcontextprotocol/rust-sdk#1093)
Other
document the ping utility with examples (modelcontextprotocol/rust-sdk#1106)
complete Tier 1 feature docs and finalize roadmap (modelcontextprotocol/rust-sdk#1101)
(conformance) meeting requirements for tier 1 (modelcontextprotocol/rust-sdk#1087)
```

Signed-off-by: Filinto Duran <1373693+filintod@users.noreply.github.com>
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

Labels

T-core Core library changes T-service Service layer changes T-test Testing related changes T-transport Transport layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants


Back | FazBrowse Home | New Git URL