rust-sdk/crates/rmcp/tests/test_discover_http_client_startup.rs
Dale Seo 2e2c79173c
feat: add modern client lifecycle modes (SEP-2575) (#995)
* feat: add modern client lifecycle modes

* refactor: drop modern naming and dead cleanup

* fix: gate request metadata on inline lifecycle only
2026-07-16 17:12:16 -04:00

137 lines
4.6 KiB
Rust

#![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<RoleServer>,
) -> impl Future<Output = Result<DiscoverResult, ErrorData>> + 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<DiscoverHttpServer, LocalSessionManager> =
StreamableHttpService::new(
|| Ok(DiscoverHttpServer),
Default::default(),
StreamableHttpServerConfig::default()
.with_stateful_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<LegacyHttpServer, LocalSessionManager> =
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");
}