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