@@ -17,6 +17,24 @@ use common::calculator::Calculator;
1717
1818const INIT_BODY : & str = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}"# ;
1919const CALL_WITH_PROGRESS_BODY : & str = r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"progress","arguments":{},"_meta":{"progressToken":"progress-test-1"}}}"# ;
20+ const NEGOTIATED_CALL_WITH_PROGRESS_BODY : & str = r#"{
21+ "jsonrpc": "2.0",
22+ "id": 2,
23+ "method": "tools/call",
24+ "params": {
25+ "name": "progress",
26+ "arguments": {},
27+ "_meta": {
28+ "progressToken": "progress-test-1",
29+ "io.modelcontextprotocol/protocolVersion": "2026-07-28",
30+ "io.modelcontextprotocol/clientInfo": {
31+ "name": "test",
32+ "version": "1.0"
33+ },
34+ "io.modelcontextprotocol/clientCapabilities": {}
35+ }
36+ }
37+ }"# ;
2038
2139#[ derive( Clone ) ]
2240struct ProgressServer ;
@@ -192,6 +210,61 @@ async fn stateless_json_response_falls_back_to_sse_for_progress() -> anyhow::Res
192210 Ok ( ( ) )
193211}
194212
213+ #[ tokio:: test]
214+ async fn stateless_negotiated_json_response_falls_back_to_sse_for_progress ( ) -> anyhow:: Result < ( ) > {
215+ let ct = CancellationToken :: new ( ) ;
216+ let ( client, url, ct) = spawn_progress_server (
217+ StreamableHttpServerConfig :: default ( )
218+ . with_stateful_mode ( false )
219+ . with_json_response ( true )
220+ . with_sse_keep_alive ( None )
221+ . with_cancellation_token ( ct. child_token ( ) ) ,
222+ )
223+ . await ;
224+
225+ let response = client
226+ . post ( & url)
227+ . header ( "Content-Type" , "application/json" )
228+ . header ( "Accept" , "application/json, text/event-stream" )
229+ . header ( "MCP-Protocol-Version" , "2026-07-28" )
230+ . header ( "Mcp-Method" , "tools/call" )
231+ . header ( "Mcp-Name" , "progress" )
232+ . body ( NEGOTIATED_CALL_WITH_PROGRESS_BODY )
233+ . send ( )
234+ . await ?;
235+
236+ assert_eq ! ( response. status( ) , 200 ) ;
237+
238+ let content_type = response
239+ . headers ( )
240+ . get ( "content-type" )
241+ . and_then ( |value| value. to_str ( ) . ok ( ) )
242+ . unwrap_or ( "" ) ;
243+ assert ! (
244+ content_type. contains( "text/event-stream" ) ,
245+ "Expected SSE fallback, got: {content_type}"
246+ ) ;
247+
248+ let body = response. text ( ) . await ?;
249+ let messages: Vec < serde_json:: Value > = body
250+ . lines ( )
251+ . filter_map ( |line| line. strip_prefix ( "data:" ) )
252+ . map ( str:: trim)
253+ . filter ( |data| !data. is_empty ( ) )
254+ . map ( serde_json:: from_str)
255+ . collect :: < Result < _ , _ > > ( ) ?;
256+ assert_eq ! ( messages. len( ) , 2 , "Expected progress and result: {body}" ) ;
257+ assert_eq ! ( messages[ 0 ] [ "method" ] , "notifications/progress" ) ;
258+ assert_eq ! ( messages[ 1 ] [ "id" ] , 2 ) ;
259+ assert ! (
260+ messages[ 1 ] [ "result" ] . is_object( ) ,
261+ "Expected result object: {body}"
262+ ) ;
263+
264+ ct. cancel ( ) ;
265+ Ok ( ( ) )
266+ }
267+
195268#[ tokio:: test]
196269async fn stateless_sse_mode_default_unchanged ( ) -> anyhow:: Result < ( ) > {
197270 let ct = CancellationToken :: new ( ) ;
0 commit comments