@@ -17,6 +17,41 @@ 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_BODY : & str = r#"{
21+ "jsonrpc": "2.0",
22+ "id": 2,
23+ "method": "tools/call",
24+ "params": {
25+ "name": "terminal",
26+ "arguments": {},
27+ "_meta": {
28+ "io.modelcontextprotocol/protocolVersion": "2026-07-28",
29+ "io.modelcontextprotocol/clientInfo": {
30+ "name": "test",
31+ "version": "1.0"
32+ },
33+ "io.modelcontextprotocol/clientCapabilities": {}
34+ }
35+ }
36+ }"# ;
37+ const NEGOTIATED_CALL_WITH_PROGRESS_BODY : & str = r#"{
38+ "jsonrpc": "2.0",
39+ "id": 2,
40+ "method": "tools/call",
41+ "params": {
42+ "name": "progress",
43+ "arguments": {},
44+ "_meta": {
45+ "progressToken": "progress-test-1",
46+ "io.modelcontextprotocol/protocolVersion": "2026-07-28",
47+ "io.modelcontextprotocol/clientInfo": {
48+ "name": "test",
49+ "version": "1.0"
50+ },
51+ "io.modelcontextprotocol/clientCapabilities": {}
52+ }
53+ }
54+ }"# ;
2055
2156#[ derive( Clone ) ]
2257struct ProgressServer ;
@@ -28,22 +63,24 @@ impl ServerHandler for ProgressServer {
2863
2964 async fn call_tool (
3065 & self ,
31- _request : CallToolRequestParams ,
66+ request : CallToolRequestParams ,
3267 context : RequestContext < rmcp:: RoleServer > ,
3368 ) -> Result < CallToolResponse , ErrorData > {
34- let progress_token = context
35- . meta
36- . get_progress_token ( )
37- . expect ( "request includes progressToken" ) ;
38- context
39- . peer
40- . notify_progress (
41- ProgressNotificationParam :: new ( progress_token, 50.0 )
42- . with_total ( 100.0 )
43- . with_message ( "working" ) ,
44- )
45- . await
46- . expect ( "progress notification is delivered" ) ;
69+ if request. name == "progress" {
70+ let progress_token = context
71+ . meta
72+ . get_progress_token ( )
73+ . expect ( "request includes progressToken" ) ;
74+ context
75+ . peer
76+ . notify_progress (
77+ ProgressNotificationParam :: new ( progress_token, 50.0 )
78+ . with_total ( 100.0 )
79+ . with_message ( "working" ) ,
80+ )
81+ . await
82+ . expect ( "progress notification is delivered" ) ;
83+ }
4784 Ok ( CallToolResult :: success ( vec ! [ ContentBlock :: text( "done" ) ] ) . into ( ) )
4885 }
4986}
@@ -140,6 +177,49 @@ async fn stateless_json_response_returns_application_json() -> anyhow::Result<()
140177 Ok ( ( ) )
141178}
142179
180+ #[ tokio:: test]
181+ async fn stateless_negotiated_terminal_response_returns_application_json ( ) -> anyhow:: Result < ( ) > {
182+ let ct = CancellationToken :: new ( ) ;
183+ let ( client, url, ct) = spawn_progress_server (
184+ StreamableHttpServerConfig :: default ( )
185+ . with_stateful_mode ( false )
186+ . with_json_response ( true )
187+ . with_sse_keep_alive ( None )
188+ . with_cancellation_token ( ct. child_token ( ) ) ,
189+ )
190+ . await ;
191+
192+ let response = client
193+ . post ( & url)
194+ . header ( "Content-Type" , "application/json" )
195+ . header ( "Accept" , "application/json, text/event-stream" )
196+ . header ( "MCP-Protocol-Version" , "2026-07-28" )
197+ . header ( "Mcp-Method" , "tools/call" )
198+ . header ( "Mcp-Name" , "terminal" )
199+ . body ( NEGOTIATED_CALL_BODY )
200+ . send ( )
201+ . await ?;
202+
203+ assert_eq ! ( response. status( ) , 200 ) ;
204+
205+ let content_type = response
206+ . headers ( )
207+ . get ( "content-type" )
208+ . and_then ( |value| value. to_str ( ) . ok ( ) )
209+ . unwrap_or ( "" ) ;
210+ assert ! (
211+ content_type. contains( "application/json" ) ,
212+ "Expected application/json, got: {content_type}"
213+ ) ;
214+
215+ let body: serde_json:: Value = response. json ( ) . await ?;
216+ assert_eq ! ( body[ "id" ] , 2 ) ;
217+ assert ! ( body[ "result" ] . is_object( ) , "Expected result object" ) ;
218+
219+ ct. cancel ( ) ;
220+ Ok ( ( ) )
221+ }
222+
143223#[ tokio:: test]
144224async fn stateless_json_response_falls_back_to_sse_for_progress ( ) -> anyhow:: Result < ( ) > {
145225 let ct = CancellationToken :: new ( ) ;
@@ -192,6 +272,61 @@ async fn stateless_json_response_falls_back_to_sse_for_progress() -> anyhow::Res
192272 Ok ( ( ) )
193273}
194274
275+ #[ tokio:: test]
276+ async fn stateless_negotiated_json_response_falls_back_to_sse_for_progress ( ) -> anyhow:: Result < ( ) > {
277+ let ct = CancellationToken :: new ( ) ;
278+ let ( client, url, ct) = spawn_progress_server (
279+ StreamableHttpServerConfig :: default ( )
280+ . with_stateful_mode ( false )
281+ . with_json_response ( true )
282+ . with_sse_keep_alive ( None )
283+ . with_cancellation_token ( ct. child_token ( ) ) ,
284+ )
285+ . await ;
286+
287+ let response = client
288+ . post ( & url)
289+ . header ( "Content-Type" , "application/json" )
290+ . header ( "Accept" , "application/json, text/event-stream" )
291+ . header ( "MCP-Protocol-Version" , "2026-07-28" )
292+ . header ( "Mcp-Method" , "tools/call" )
293+ . header ( "Mcp-Name" , "progress" )
294+ . body ( NEGOTIATED_CALL_WITH_PROGRESS_BODY )
295+ . send ( )
296+ . await ?;
297+
298+ assert_eq ! ( response. status( ) , 200 ) ;
299+
300+ let content_type = response
301+ . headers ( )
302+ . get ( "content-type" )
303+ . and_then ( |value| value. to_str ( ) . ok ( ) )
304+ . unwrap_or ( "" ) ;
305+ assert ! (
306+ content_type. contains( "text/event-stream" ) ,
307+ "Expected SSE fallback, got: {content_type}"
308+ ) ;
309+
310+ let body = response. text ( ) . await ?;
311+ let messages: Vec < serde_json:: Value > = body
312+ . lines ( )
313+ . filter_map ( |line| line. strip_prefix ( "data:" ) )
314+ . map ( str:: trim)
315+ . filter ( |data| !data. is_empty ( ) )
316+ . map ( serde_json:: from_str)
317+ . collect :: < Result < _ , _ > > ( ) ?;
318+ assert_eq ! ( messages. len( ) , 2 , "Expected progress and result: {body}" ) ;
319+ assert_eq ! ( messages[ 0 ] [ "method" ] , "notifications/progress" ) ;
320+ assert_eq ! ( messages[ 1 ] [ "id" ] , 2 ) ;
321+ assert ! (
322+ messages[ 1 ] [ "result" ] . is_object( ) ,
323+ "Expected result object: {body}"
324+ ) ;
325+
326+ ct. cancel ( ) ;
327+ Ok ( ( ) )
328+ }
329+
195330#[ tokio:: test]
196331async fn stateless_sse_mode_default_unchanged ( ) -> anyhow:: Result < ( ) > {
197332 let ct = CancellationToken :: new ( ) ;
0 commit comments