#![cfg(all( not(feature = "local"), feature = "client", feature = "reqwest", feature = "transport-streamable-http-server" ))] use std::borrow::Cow; use rmcp::{ ClientLifecycleMode, ClientServiceExt, ServerHandler, model::{ClientInfo, DiscoverResult, ErrorCode, ErrorData, ProtocolVersion}, service::{MaybeSendFuture, RequestContext, RoleServer}, transport::{ StreamableHttpClientTransport, streamable_http_client::StreamableHttpClientTransportConfig, streamable_http_server::{ StreamableHttpServerConfig, StreamableHttpService, session::local::LocalSessionManager, }, }, }; use tokio_util::sync::CancellationToken; #[derive(Clone, Default)] struct DiscoverHttpServer; impl ServerHandler for DiscoverHttpServer { fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> { Cow::Borrowed(&[ProtocolVersion::V_2026_07_28]) } } #[derive(Clone, Default)] struct LegacyHttpServer; impl ServerHandler for LegacyHttpServer { fn discover( &self, _context: RequestContext, ) -> impl Future> + MaybeSendFuture + '_ { std::future::ready(Err(ErrorData::new( ErrorCode::METHOD_NOT_FOUND, "Method not found", None, ))) } } #[tokio::test] async fn discover_http_client_bootstraps_headers_without_initialize() { let ct = CancellationToken::new(); let service: StreamableHttpService = StreamableHttpService::new( || Ok(DiscoverHttpServer), Default::default(), StreamableHttpServerConfig::default() .with_legacy_session_mode(false) .with_json_response(true) .with_cancellation_token(ct.child_token()), ); let router = axum::Router::new().nest_service("/mcp", service); let listener = tokio::net::TcpListener::bind("127.0.0.1:0") .await .expect("listener should bind"); let address = listener.local_addr().expect("listener address"); let server = tokio::spawn({ let ct = ct.clone(); async move { let _ = axum::serve(listener, router) .with_graceful_shutdown(async move { ct.cancelled_owned().await }) .await; } }); let transport = StreamableHttpClientTransport::from_config( StreamableHttpClientTransportConfig::with_uri(format!("http://{address}/mcp")), ); let client = ClientInfo::default() .serve_with_lifecycle( transport, ClientLifecycleMode::Discover { preferred_versions: vec![ProtocolVersion::V_2026_07_28], }, ) .await .expect("discover HTTP client should start"); client.list_tools(None).await.expect("list tools"); client.cancel().await.expect("cancel client"); ct.cancel(); server.await.expect("server task"); } #[tokio::test] async fn auto_http_client_falls_back_to_stateful_legacy_startup() { let ct = CancellationToken::new(); let service: StreamableHttpService = StreamableHttpService::new( || Ok(LegacyHttpServer), Default::default(), StreamableHttpServerConfig::default() .with_json_response(true) .with_cancellation_token(ct.child_token()), ); let router = axum::Router::new().nest_service("/mcp", service); let listener = tokio::net::TcpListener::bind("127.0.0.1:0") .await .expect("listener should bind"); let address = listener.local_addr().expect("listener address"); let server = tokio::spawn({ let ct = ct.clone(); async move { let _ = axum::serve(listener, router) .with_graceful_shutdown(async move { ct.cancelled_owned().await }) .await; } }); let transport = StreamableHttpClientTransport::from_config( StreamableHttpClientTransportConfig::with_uri(format!("http://{address}/mcp")), ); let client = ClientInfo::default() .serve_with_lifecycle( transport, ClientLifecycleMode::Auto { preferred_versions: vec![ProtocolVersion::V_2026_07_28], legacy_version: Some(ProtocolVersion::V_2025_11_25), }, ) .await .expect("auto HTTP client should fall back"); client.list_tools(None).await.expect("list tools"); client.cancel().await.expect("cancel client"); ct.cancel(); server.await.expect("server task"); }