@@ -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 } ,
@@ -635,6 +636,7 @@ impl<C: StreamableHttpClient> StreamableHttpClientWorker<C> {
635636 + Send
636637 + ' static ,
637638 sse_worker_tx : tokio:: sync:: mpsc:: Sender < ServerJsonRpcMessage > ,
639+ origin : InboundStreamOrigin ,
638640 close_on_response : bool ,
639641 ct : CancellationToken ,
640642 ) -> Result < ( ) , StreamableHttpError < C :: Error > > {
@@ -649,9 +651,14 @@ impl<C: StreamableHttpClient> StreamableHttpClientWorker<C> {
649651 break ;
650652 }
651653 } ;
652- let Some ( message) = message. transpose ( ) ? else {
654+ let Some ( mut message) = message. transpose ( ) ? else {
653655 break ;
654656 } ;
657+ // SEP-2260: record which stream this request arrived on so the
658+ // service layer can enforce receive-side request association.
659+ if let ServerJsonRpcMessage :: Request ( request) = & mut message {
660+ request. request . extensions_mut ( ) . insert ( origin. clone ( ) ) ;
661+ }
655662 let is_response = matches ! (
656663 message,
657664 ServerJsonRpcMessage :: Response ( _) | ServerJsonRpcMessage :: Error ( _)
@@ -719,6 +726,7 @@ impl<C: StreamableHttpClient> StreamableHttpClientWorker<C> {
719726 Self :: execute_sse_stream (
720727 sse_stream,
721728 sse_worker_tx,
729+ InboundStreamOrigin :: Unassociated ,
722730 false ,
723731 transport_task_ct. child_token ( ) ,
724732 )
@@ -1283,9 +1291,18 @@ impl<C: StreamableHttpClient> Worker for StreamableHttpClientWorker<C> {
12831291 ) ;
12841292 }
12851293 let stream_tx = sse_worker_tx. clone ( ) ;
1294+ let origin = match & stream_request_id {
1295+ Some ( id) => {
1296+ InboundStreamOrigin :: OutboundRequest (
1297+ id. clone ( ) ,
1298+ )
1299+ }
1300+ None => InboundStreamOrigin :: Unassociated ,
1301+ } ;
12861302 streams. spawn ( async move {
12871303 let result = Self :: execute_sse_stream (
1288- sse_stream, stream_tx, true , stream_ct,
1304+ sse_stream, stream_tx, origin, true ,
1305+ stream_ct,
12891306 )
12901307 . await ;
12911308 ( stream_request_id, result)
@@ -1342,9 +1359,13 @@ impl<C: StreamableHttpClient> Worker for StreamableHttpClientWorker<C> {
13421359 . insert ( request_id. clone ( ) , stream_ct. clone ( ) ) ;
13431360 }
13441361 let stream_tx = sse_worker_tx. clone ( ) ;
1362+ let origin = match & stream_request_id {
1363+ Some ( id) => InboundStreamOrigin :: OutboundRequest ( id. clone ( ) ) ,
1364+ None => InboundStreamOrigin :: Unassociated ,
1365+ } ;
13451366 streams. spawn ( async move {
13461367 let result = Self :: execute_sse_stream (
1347- sse_stream, stream_tx, true , stream_ct,
1368+ sse_stream, stream_tx, origin , true , stream_ct,
13481369 )
13491370 . await ;
13501371 ( stream_request_id, result)
@@ -1769,7 +1790,66 @@ mod tests {
17691790 use serde_json:: json;
17701791
17711792 use super :: * ;
1772- use crate :: model:: { ListToolsResult , NumberOrString , ServerResult , Tool } ;
1793+ use crate :: {
1794+ model:: {
1795+ GetExtensions , ListToolsResult , NumberOrString , ServerRequest , ServerResult , Tool ,
1796+ } ,
1797+ service:: InboundStreamOrigin ,
1798+ } ;
1799+
1800+ #[ expect(
1801+ deprecated,
1802+ reason = "sampling (SEP-2577) is still a representative server request"
1803+ ) ]
1804+ fn sampling_request_message ( id : i64 ) -> ServerJsonRpcMessage {
1805+ use crate :: model:: { CreateMessageRequest , CreateMessageRequestParams , SamplingMessage } ;
1806+ ServerJsonRpcMessage :: request (
1807+ ServerRequest :: CreateMessageRequest ( CreateMessageRequest :: new (
1808+ CreateMessageRequestParams :: new ( vec ! [ SamplingMessage :: user_text( "hi" ) ] , 16 ) ,
1809+ ) ) ,
1810+ NumberOrString :: Number ( id) ,
1811+ )
1812+ }
1813+
1814+ #[ tokio:: test]
1815+ async fn execute_sse_stream_marks_inbound_requests_with_origin ( ) {
1816+ for origin in [
1817+ InboundStreamOrigin :: Unassociated ,
1818+ InboundStreamOrigin :: OutboundRequest ( RequestId :: Number ( 3 ) ) ,
1819+ ] {
1820+ let response = ServerJsonRpcMessage :: response (
1821+ ServerResult :: ListToolsResult ( ListToolsResult :: default ( ) ) ,
1822+ NumberOrString :: Number ( 1 ) ,
1823+ ) ;
1824+ let stream = futures:: stream:: iter ( [ Ok ( sampling_request_message ( 9 ) ) , Ok ( response) ] ) ;
1825+ let ( tx, mut rx) = tokio:: sync:: mpsc:: channel ( 4 ) ;
1826+ StreamableHttpClientWorker :: < StatelessReconnectClient > :: execute_sse_stream (
1827+ stream,
1828+ tx,
1829+ origin. clone ( ) ,
1830+ false ,
1831+ CancellationToken :: new ( ) ,
1832+ )
1833+ . await
1834+ . expect ( "stream completes" ) ;
1835+
1836+ let ServerJsonRpcMessage :: Request ( request) =
1837+ rx. recv ( ) . await . expect ( "request forwarded" )
1838+ else {
1839+ panic ! ( "expected request first" ) ;
1840+ } ;
1841+ assert_eq ! (
1842+ request. request. extensions( ) . get:: <InboundStreamOrigin >( ) ,
1843+ Some ( & origin) ,
1844+ "inbound requests must carry their stream origin"
1845+ ) ;
1846+ // Responses are correlated by JSON-RPC id; no marker needed or added.
1847+ assert ! ( matches!(
1848+ rx. recv( ) . await . expect( "response forwarded" ) ,
1849+ ServerJsonRpcMessage :: Response ( _)
1850+ ) ) ;
1851+ }
1852+ }
17731853
17741854 type ReconnectAttempt = ( Option < String > , Option < String > ) ;
17751855
0 commit comments