11#![ cfg( not( feature = "local" ) ) ]
2- use rmcp:: transport:: streamable_http_server:: {
3- StreamableHttpServerConfig , StreamableHttpService , session:: local:: LocalSessionManager ,
2+ use rmcp:: {
3+ ErrorData , ServerHandler ,
4+ model:: {
5+ CallToolRequestParams , CallToolResponse , CallToolResult , ContentBlock ,
6+ ProgressNotificationParam , ServerCapabilities , ServerInfo ,
7+ } ,
8+ service:: RequestContext ,
9+ transport:: streamable_http_server:: {
10+ StreamableHttpServerConfig , StreamableHttpService , session:: local:: LocalSessionManager ,
11+ } ,
412} ;
513use tokio_util:: sync:: CancellationToken ;
614
715mod common;
816use common:: calculator:: Calculator ;
917
1018const INIT_BODY : & str = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}"# ;
19+ const CALL_WITH_PROGRESS_BODY : & str = r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"progress","arguments":{},"_meta":{"progressToken":"progress-test-1"}}}"# ;
20+
21+ #[ derive( Clone ) ]
22+ struct ProgressServer ;
23+
24+ impl ServerHandler for ProgressServer {
25+ fn get_info ( & self ) -> ServerInfo {
26+ ServerInfo :: new ( ServerCapabilities :: builder ( ) . enable_tools ( ) . build ( ) )
27+ }
28+
29+ async fn call_tool (
30+ & self ,
31+ _request : CallToolRequestParams ,
32+ context : RequestContext < rmcp:: RoleServer > ,
33+ ) -> 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" ) ;
47+ Ok ( CallToolResult :: success ( vec ! [ ContentBlock :: text( "done" ) ] ) . into ( ) )
48+ }
49+ }
1150
1251async fn spawn_server (
1352 config : StreamableHttpServerConfig ,
@@ -34,6 +73,31 @@ async fn spawn_server(
3473 ( client, base_url, ct)
3574}
3675
76+ async fn spawn_progress_server (
77+ config : StreamableHttpServerConfig ,
78+ ) -> ( reqwest:: Client , String , CancellationToken ) {
79+ let ct = config. cancellation_token . clone ( ) ;
80+ let service: StreamableHttpService < ProgressServer , LocalSessionManager > =
81+ StreamableHttpService :: new ( || Ok ( ProgressServer ) , Default :: default ( ) , config) ;
82+
83+ let router = axum:: Router :: new ( ) . nest_service ( "/mcp" , service) ;
84+ let tcp_listener = tokio:: net:: TcpListener :: bind ( "127.0.0.1:0" ) . await . unwrap ( ) ;
85+ let addr = tcp_listener. local_addr ( ) . unwrap ( ) ;
86+
87+ tokio:: spawn ( {
88+ let ct = ct. clone ( ) ;
89+ async move {
90+ let _ = axum:: serve ( tcp_listener, router)
91+ . with_graceful_shutdown ( async move { ct. cancelled_owned ( ) . await } )
92+ . await ;
93+ }
94+ } ) ;
95+
96+ let client = reqwest:: Client :: new ( ) ;
97+ let base_url = format ! ( "http://{addr}/mcp" ) ;
98+ ( client, base_url, ct)
99+ }
100+
37101#[ tokio:: test]
38102async fn stateless_json_response_returns_application_json ( ) -> anyhow:: Result < ( ) > {
39103 let ct = CancellationToken :: new ( ) ;
@@ -76,6 +140,58 @@ async fn stateless_json_response_returns_application_json() -> anyhow::Result<()
76140 Ok ( ( ) )
77141}
78142
143+ #[ tokio:: test]
144+ async fn stateless_json_response_falls_back_to_sse_for_progress ( ) -> anyhow:: Result < ( ) > {
145+ let ct = CancellationToken :: new ( ) ;
146+ let ( client, url, ct) = spawn_progress_server (
147+ StreamableHttpServerConfig :: default ( )
148+ . with_stateful_mode ( false )
149+ . with_json_response ( true )
150+ . with_sse_keep_alive ( None )
151+ . with_cancellation_token ( ct. child_token ( ) ) ,
152+ )
153+ . await ;
154+
155+ let response = client
156+ . post ( & url)
157+ . header ( "Content-Type" , "application/json" )
158+ . header ( "Accept" , "application/json, text/event-stream" )
159+ . body ( CALL_WITH_PROGRESS_BODY )
160+ . send ( )
161+ . await ?;
162+
163+ assert_eq ! ( response. status( ) , 200 ) ;
164+
165+ let content_type = response
166+ . headers ( )
167+ . get ( "content-type" )
168+ . and_then ( |value| value. to_str ( ) . ok ( ) )
169+ . unwrap_or ( "" ) ;
170+ assert ! (
171+ content_type. contains( "text/event-stream" ) ,
172+ "Expected SSE fallback, got: {content_type}"
173+ ) ;
174+
175+ let body = response. text ( ) . await ?;
176+ let messages: Vec < serde_json:: Value > = body
177+ . lines ( )
178+ . filter_map ( |line| line. strip_prefix ( "data:" ) )
179+ . map ( str:: trim)
180+ . filter ( |data| !data. is_empty ( ) )
181+ . map ( serde_json:: from_str)
182+ . collect :: < Result < _ , _ > > ( ) ?;
183+ assert_eq ! ( messages. len( ) , 2 , "Expected progress and result: {body}" ) ;
184+ assert_eq ! ( messages[ 0 ] [ "method" ] , "notifications/progress" ) ;
185+ assert_eq ! ( messages[ 1 ] [ "id" ] , 2 ) ;
186+ assert ! (
187+ messages[ 1 ] [ "result" ] . is_object( ) ,
188+ "Expected result object: {body}"
189+ ) ;
190+
191+ ct. cancel ( ) ;
192+ Ok ( ( ) )
193+ }
194+
79195#[ tokio:: test]
80196async fn stateless_sse_mode_default_unchanged ( ) -> anyhow:: Result < ( ) > {
81197 let ct = CancellationToken :: new ( ) ;
0 commit comments