@@ -1948,6 +1948,132 @@ mod tests {
19481948 ) ;
19491949 }
19501950
1951+ #[ derive( Clone , Default ) ]
1952+ struct ResumedRequestClient {
1953+ reconnects : Arc < Mutex < Vec < ReconnectAttempt > > > ,
1954+ }
1955+
1956+ impl StreamableHttpClient for ResumedRequestClient {
1957+ type Error = std:: io:: Error ;
1958+
1959+ async fn post_message (
1960+ & self ,
1961+ _uri : Arc < str > ,
1962+ _message : ClientJsonRpcMessage ,
1963+ _session_id : Option < Arc < str > > ,
1964+ _auth_header : Option < String > ,
1965+ _custom_headers : HashMap < HeaderName , HeaderValue > ,
1966+ ) -> Result < StreamableHttpPostResponse , StreamableHttpError < Self :: Error > > {
1967+ Err ( StreamableHttpError :: UnexpectedServerResponse (
1968+ "unexpected POST" . into ( ) ,
1969+ ) )
1970+ }
1971+
1972+ async fn delete_session (
1973+ & self ,
1974+ _uri : Arc < str > ,
1975+ _session_id : Arc < str > ,
1976+ _auth_header : Option < String > ,
1977+ _custom_headers : HashMap < HeaderName , HeaderValue > ,
1978+ ) -> Result < ( ) , StreamableHttpError < Self :: Error > > {
1979+ Ok ( ( ) )
1980+ }
1981+
1982+ async fn get_stream (
1983+ & self ,
1984+ _uri : Arc < str > ,
1985+ session_id : Option < Arc < str > > ,
1986+ last_event_id : Option < String > ,
1987+ _auth_header : Option < String > ,
1988+ _custom_headers : HashMap < HeaderName , HeaderValue > ,
1989+ ) -> Result < BoxedSseStream , StreamableHttpError < Self :: Error > > {
1990+ self . reconnects
1991+ . lock ( )
1992+ . expect ( "lock reconnects" )
1993+ . push ( ( session_id. map ( |id| id. to_string ( ) ) , last_event_id) ) ;
1994+ let request = sampling_request_message ( 9 ) ;
1995+ let response = ServerJsonRpcMessage :: response (
1996+ ServerResult :: ListToolsResult ( ListToolsResult :: default ( ) ) ,
1997+ NumberOrString :: Number ( 1 ) ,
1998+ ) ;
1999+ // Stay open after the response, like a live connection, so the
2000+ // post-response drain in `execute_sse_stream` doesn't trigger
2001+ // further reconnects.
2002+ Ok ( futures:: stream:: iter ( [ request, response] . map ( |message| {
2003+ Ok ( Sse {
2004+ event : None ,
2005+ data : Some ( serde_json:: to_string ( & message) . expect ( "serialize message" ) ) ,
2006+ id : None ,
2007+ retry : None ,
2008+ } )
2009+ } ) )
2010+ . chain ( futures:: stream:: pending ( ) )
2011+ . boxed ( ) )
2012+ }
2013+ }
2014+
2015+ /// SEP-1699 resumes a broken POST SSE stream via GET + Last-Event-ID
2016+ /// beneath `execute_sse_stream`, so the SEP-2260 origin marker must span
2017+ /// resumes; if reconnection were hoisted above the marker attach point,
2018+ /// replayed associated requests would be wrongly rejected with -32602.
2019+ #[ tokio:: test]
2020+ async fn resumed_post_stream_requests_keep_outbound_origin ( ) {
2021+ let initial = futures:: stream:: iter ( [ Ok ( Sse {
2022+ event : None ,
2023+ data : None ,
2024+ id : Some ( "e1" . into ( ) ) ,
2025+ retry : Some ( 0 ) ,
2026+ } ) ] )
2027+ . boxed ( ) ;
2028+ let client = ResumedRequestClient :: default ( ) ;
2029+ let reconnects = client. reconnects . clone ( ) ;
2030+ let sse_stream =
2031+ StreamableHttpClientWorker :: < ResumedRequestClient > :: response_sse_to_jsonrpc (
2032+ initial,
2033+ None ,
2034+ client,
2035+ Arc :: from ( "http://localhost/mcp" ) ,
2036+ None ,
2037+ HashMap :: new ( ) ,
2038+ DEFAULT_MAX_SSE_EVENT_SIZE ,
2039+ Arc :: new ( ExponentialBackoff {
2040+ max_times : Some ( 1 ) ,
2041+ base_duration : Duration :: ZERO ,
2042+ } ) ,
2043+ ) ;
2044+
2045+ let origin = InboundStreamOrigin :: OutboundRequest ( RequestId :: Number ( 3 ) ) ;
2046+ let ( tx, mut rx) = tokio:: sync:: mpsc:: channel ( 4 ) ;
2047+ StreamableHttpClientWorker :: < ResumedRequestClient > :: execute_sse_stream (
2048+ sse_stream,
2049+ tx,
2050+ origin. clone ( ) ,
2051+ true ,
2052+ CancellationToken :: new ( ) ,
2053+ )
2054+ . await
2055+ . expect ( "stream completes" ) ;
2056+
2057+ assert_eq ! (
2058+ reconnects. lock( ) . expect( "lock reconnects" ) . as_slice( ) ,
2059+ & [ ( None , Some ( "e1" . into( ) ) ) ] ,
2060+ "the request must arrive on the resumed connection"
2061+ ) ;
2062+ let ServerJsonRpcMessage :: Request ( request) = rx. recv ( ) . await . expect ( "request forwarded" )
2063+ else {
2064+ panic ! ( "expected request first" ) ;
2065+ } ;
2066+ assert_eq ! (
2067+ request. request. extensions( ) . get:: <InboundStreamOrigin >( ) ,
2068+ Some ( & origin) ,
2069+ "origin marker must survive SSE resumption"
2070+ ) ;
2071+ assert ! ( matches!(
2072+ rx. recv( ) . await . expect( "response forwarded" ) ,
2073+ ServerJsonRpcMessage :: Response ( _)
2074+ ) ) ;
2075+ }
2076+
19512077 fn tool ( name : & ' static str , annotation : serde_json:: Value ) -> Tool {
19522078 let schema = json ! ( {
19532079 "type" : "object" ,
0 commit comments