@@ -19,10 +19,11 @@ use super::common::client_side_sse::{
1919use crate :: {
2020 RoleClient ,
2121 model:: {
22- ClientJsonRpcMessage , ClientNotification , ClientRequest , ErrorData , GetMeta ,
22+ ClientJsonRpcMessage , ClientNotification , ClientRequest , ErrorData , GetExtensions , GetMeta ,
2323 InitializedNotification , JsonObject , ProtocolVersion , RequestId , ServerJsonRpcMessage ,
2424 ServerResult ,
2525 } ,
26+ service:: InboundStreamOrigin ,
2627 transport:: {
2728 common:: { client_side_sse:: SseAutoReconnectStream , mcp_headers} ,
2829 worker:: { Worker , WorkerQuitReason , WorkerSendRequest , WorkerTransport } ,
@@ -617,6 +618,7 @@ impl<C: StreamableHttpClient> StreamableHttpClientWorker<C> {
617618 + Send
618619 + ' static ,
619620 sse_worker_tx : tokio:: sync:: mpsc:: Sender < ServerJsonRpcMessage > ,
621+ origin : InboundStreamOrigin ,
620622 close_on_response : bool ,
621623 ct : CancellationToken ,
622624 ) -> Result < ( ) , StreamableHttpError < C :: Error > > {
@@ -631,9 +633,14 @@ impl<C: StreamableHttpClient> StreamableHttpClientWorker<C> {
631633 break ;
632634 }
633635 } ;
634- let Some ( message) = message. transpose ( ) ? else {
636+ let Some ( mut message) = message. transpose ( ) ? else {
635637 break ;
636638 } ;
639+ // SEP-2260: record which stream this request arrived on so the
640+ // service layer can enforce receive-side request association.
641+ if let ServerJsonRpcMessage :: Request ( request) = & mut message {
642+ request. request . extensions_mut ( ) . insert ( origin. clone ( ) ) ;
643+ }
637644 let is_response = matches ! (
638645 message,
639646 ServerJsonRpcMessage :: Response ( _) | ServerJsonRpcMessage :: Error ( _)
@@ -701,6 +708,7 @@ impl<C: StreamableHttpClient> StreamableHttpClientWorker<C> {
701708 Self :: execute_sse_stream (
702709 sse_stream,
703710 sse_worker_tx,
711+ InboundStreamOrigin :: Unassociated ,
704712 false ,
705713 transport_task_ct. child_token ( ) ,
706714 )
@@ -1265,9 +1273,18 @@ impl<C: StreamableHttpClient> Worker for StreamableHttpClientWorker<C> {
12651273 ) ;
12661274 }
12671275 let stream_tx = sse_worker_tx. clone ( ) ;
1276+ let origin = match & stream_request_id {
1277+ Some ( id) => {
1278+ InboundStreamOrigin :: OutboundRequest (
1279+ id. clone ( ) ,
1280+ )
1281+ }
1282+ None => InboundStreamOrigin :: Unassociated ,
1283+ } ;
12681284 streams. spawn ( async move {
12691285 let result = Self :: execute_sse_stream (
1270- sse_stream, stream_tx, true , stream_ct,
1286+ sse_stream, stream_tx, origin, true ,
1287+ stream_ct,
12711288 )
12721289 . await ;
12731290 ( stream_request_id, result)
@@ -1324,9 +1341,13 @@ impl<C: StreamableHttpClient> Worker for StreamableHttpClientWorker<C> {
13241341 . insert ( request_id. clone ( ) , stream_ct. clone ( ) ) ;
13251342 }
13261343 let stream_tx = sse_worker_tx. clone ( ) ;
1344+ let origin = match & stream_request_id {
1345+ Some ( id) => InboundStreamOrigin :: OutboundRequest ( id. clone ( ) ) ,
1346+ None => InboundStreamOrigin :: Unassociated ,
1347+ } ;
13271348 streams. spawn ( async move {
13281349 let result = Self :: execute_sse_stream (
1329- sse_stream, stream_tx, true , stream_ct,
1350+ sse_stream, stream_tx, origin , true , stream_ct,
13301351 )
13311352 . await ;
13321353 ( stream_request_id, result)
@@ -1751,7 +1772,66 @@ mod tests {
17511772 use serde_json:: json;
17521773
17531774 use super :: * ;
1754- use crate :: model:: { ListToolsResult , NumberOrString , ServerResult , Tool } ;
1775+ use crate :: {
1776+ model:: {
1777+ GetExtensions , ListToolsResult , NumberOrString , ServerRequest , ServerResult , Tool ,
1778+ } ,
1779+ service:: InboundStreamOrigin ,
1780+ } ;
1781+
1782+ #[ expect(
1783+ deprecated,
1784+ reason = "sampling (SEP-2577) is still a representative server request"
1785+ ) ]
1786+ fn sampling_request_message ( id : i64 ) -> ServerJsonRpcMessage {
1787+ use crate :: model:: { CreateMessageRequest , CreateMessageRequestParams , SamplingMessage } ;
1788+ ServerJsonRpcMessage :: request (
1789+ ServerRequest :: CreateMessageRequest ( CreateMessageRequest :: new (
1790+ CreateMessageRequestParams :: new ( vec ! [ SamplingMessage :: user_text( "hi" ) ] , 16 ) ,
1791+ ) ) ,
1792+ NumberOrString :: Number ( id) ,
1793+ )
1794+ }
1795+
1796+ #[ tokio:: test]
1797+ async fn execute_sse_stream_marks_inbound_requests_with_origin ( ) {
1798+ for origin in [
1799+ InboundStreamOrigin :: Unassociated ,
1800+ InboundStreamOrigin :: OutboundRequest ( RequestId :: Number ( 3 ) ) ,
1801+ ] {
1802+ let response = ServerJsonRpcMessage :: response (
1803+ ServerResult :: ListToolsResult ( ListToolsResult :: default ( ) ) ,
1804+ NumberOrString :: Number ( 1 ) ,
1805+ ) ;
1806+ let stream = futures:: stream:: iter ( [ Ok ( sampling_request_message ( 9 ) ) , Ok ( response) ] ) ;
1807+ let ( tx, mut rx) = tokio:: sync:: mpsc:: channel ( 4 ) ;
1808+ StreamableHttpClientWorker :: < StatelessReconnectClient > :: execute_sse_stream (
1809+ stream,
1810+ tx,
1811+ origin. clone ( ) ,
1812+ false ,
1813+ CancellationToken :: new ( ) ,
1814+ )
1815+ . await
1816+ . expect ( "stream completes" ) ;
1817+
1818+ let ServerJsonRpcMessage :: Request ( request) =
1819+ rx. recv ( ) . await . expect ( "request forwarded" )
1820+ else {
1821+ panic ! ( "expected request first" ) ;
1822+ } ;
1823+ assert_eq ! (
1824+ request. request. extensions( ) . get:: <InboundStreamOrigin >( ) ,
1825+ Some ( & origin) ,
1826+ "inbound requests must carry their stream origin"
1827+ ) ;
1828+ // Responses are correlated by JSON-RPC id; no marker needed or added.
1829+ assert ! ( matches!(
1830+ rx. recv( ) . await . expect( "response forwarded" ) ,
1831+ ServerJsonRpcMessage :: Response ( _)
1832+ ) ) ;
1833+ }
1834+ }
17551835
17561836 type ReconnectAttempt = ( Option < String > , Option < String > ) ;
17571837
0 commit comments