rust-sdk/crates/rmcp/tests/test_server_initialization.rs
Teddy Tennant 3075dc9152
fix(model): deprecate ServerInfo and ClientInfo aliases (#1156)
* fix(model): deprecate ServerInfo and ClientInfo aliases

Those names collide with the protocol's serverInfo/clientInfo fields,
which are only Implementation identity. Prefer InitializeResult and
InitializeRequestParams. Handler trait signatures, the tool_handler
macro output, tests, examples and docs now use the canonical types; the
aliases remain as deprecated shims for the rest of 3.x.

Fixes #1082

* fix(model): keep public signatures on the deprecated aliases

The public API check compares rendered signatures, and rustdoc records a
type alias by the name it was written with. Respelling
`ServerHandler::get_info`, `ClientHandler::get_info`,
`DiscoverResult::from_server_info` and `impl ClientHandler for ClientInfo`
onto `InitializeResult`/`InitializeRequestParams` therefore reads as
changed public items even though the aliases resolve to those exact types
and nothing downstream breaks.

Put those four signatures back on the aliases so the published API is
byte-identical, and note why at the alias definitions. The deprecation
still steers new code, and the macro output, tests, examples and docs
keep using the canonical names.

* fix(model): migrate leftover ClientInfo test sites

Two tests landed on main after the alias deprecation and still named
ClientInfo. Clippy -D warnings and the no-local feature test job both
failed on them.
2026-09-12 12:11:30 -04:00

396 lines
14 KiB
Rust

// cargo test --features "client" --package rmcp -- server_init
#![cfg(all(feature = "client", not(feature = "local")))]
mod common;
use common::handlers::TestServer;
use rmcp::{
ServerHandler, ServiceExt,
model::{
ClientJsonRpcMessage, InitializeResult, ProtocolVersion, ServerCapabilities,
ServerJsonRpcMessage, ServerResult,
},
transport::{IntoTransport, Transport},
};
fn msg(raw: &str) -> ClientJsonRpcMessage {
serde_json::from_str(raw).expect("invalid test message JSON")
}
fn init_request() -> ClientJsonRpcMessage {
msg(r#"{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": { "name": "test-client", "version": "0.0.1" }
}
}"#)
}
fn initialized_notification() -> ClientJsonRpcMessage {
msg(r#"{ "jsonrpc": "2.0", "method": "notifications/initialized" }"#)
}
fn set_level_request(id: u64) -> ClientJsonRpcMessage {
msg(&format!(
r#"{{ "jsonrpc": "2.0", "id": {id}, "method": "logging/setLevel", "params": {{ "level": "info" }} }}"#
))
}
fn ping_request(id: u64) -> ClientJsonRpcMessage {
msg(&format!(
r#"{{ "jsonrpc": "2.0", "id": {id}, "method": "ping" }}"#
))
}
fn list_tools_request(id: u64) -> ClientJsonRpcMessage {
msg(&format!(
r#"{{ "jsonrpc": "2.0", "id": {id}, "method": "tools/list" }}"#
))
}
async fn do_initialize(client: &mut impl Transport<rmcp::RoleClient>) {
client.send(init_request()).await.unwrap();
let _response = client.receive().await.unwrap();
}
// Server handles setLevel sent before initialized notification (processed by serve_inner).
#[tokio::test]
async fn server_init_set_level_response_is_empty_result() {
let (server_transport, client_transport) = tokio::io::duplex(4096);
let _server = tokio::spawn(async move { TestServer::new().serve(server_transport).await });
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_transport);
do_initialize(&mut client).await;
client.send(set_level_request(2)).await.unwrap();
// The handler may send logging notifications before the response;
// skip notifications to find the EmptyResult response.
let response = loop {
let msg = client.receive().await.unwrap();
if matches!(msg, ServerJsonRpcMessage::Response(_)) {
break msg;
}
};
assert!(
matches!(
response,
ServerJsonRpcMessage::Response(ref r)
if matches!(r.result, ServerResult::EmptyResult(_))
),
"expected EmptyResult for setLevel, got: {response:?}"
);
}
// Server initializes successfully when setLevel is sent before the initialized notification.
#[tokio::test]
async fn server_init_succeeds_after_set_level_before_initialized() {
let (server_transport, client_transport) = tokio::io::duplex(4096);
let server_handle =
tokio::spawn(async move { TestServer::new().serve(server_transport).await });
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_transport);
do_initialize(&mut client).await;
client.send(set_level_request(2)).await.unwrap();
// Skip notifications until we get the response
loop {
let msg = client.receive().await.unwrap();
if matches!(msg, ServerJsonRpcMessage::Response(_)) {
break;
}
}
client.send(initialized_notification()).await.unwrap();
let result = server_handle.await.unwrap();
assert!(
result.is_ok(),
"server should initialize successfully after setLevel"
);
result.unwrap().cancel().await.unwrap();
}
// Server responds with EmptyResult to ping received before initialize request.
#[tokio::test]
async fn server_init_ping_response_is_empty_result_before_initialize() {
let (server_transport, client_transport) = tokio::io::duplex(4096);
let _server = tokio::spawn(async move { TestServer::new().serve(server_transport).await });
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_transport);
client.send(ping_request(1)).await.unwrap();
let response = client.receive().await.unwrap();
assert!(
matches!(
response,
ServerJsonRpcMessage::Response(ref r)
if matches!(r.result, ServerResult::EmptyResult(_))
),
"expected EmptyResult for pre-initialize ping, got: {response:?}"
);
}
// Server initializes successfully when ping is sent before the initialize request.
#[tokio::test]
async fn server_init_succeeds_after_ping_before_initialize() {
let (server_transport, client_transport) = tokio::io::duplex(4096);
let server_handle =
tokio::spawn(async move { TestServer::new().serve(server_transport).await });
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_transport);
client.send(ping_request(1)).await.unwrap();
let _pong = client.receive().await.unwrap();
do_initialize(&mut client).await;
client.send(initialized_notification()).await.unwrap();
let result = server_handle.await.unwrap();
assert!(
result.is_ok(),
"server should initialize successfully after pre-initialize ping"
);
result.unwrap().cancel().await.unwrap();
}
// Server responds with EmptyResult to ping received before initialized.
#[tokio::test]
async fn server_init_ping_response_is_empty_result() {
let (server_transport, client_transport) = tokio::io::duplex(4096);
let _server = tokio::spawn(async move { TestServer::new().serve(server_transport).await });
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_transport);
do_initialize(&mut client).await;
client.send(ping_request(2)).await.unwrap();
let response = client.receive().await.unwrap();
assert!(
matches!(
response,
ServerJsonRpcMessage::Response(ref r)
if matches!(r.result, ServerResult::EmptyResult(_))
),
"expected EmptyResult for ping, got: {response:?}"
);
}
// Server initializes successfully when ping is sent before the initialized notification.
#[tokio::test]
async fn server_init_succeeds_after_ping_before_initialized() {
let (server_transport, client_transport) = tokio::io::duplex(4096);
let server_handle =
tokio::spawn(async move { TestServer::new().serve(server_transport).await });
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_transport);
do_initialize(&mut client).await;
client.send(ping_request(2)).await.unwrap();
let _response = client.receive().await.unwrap();
client.send(initialized_notification()).await.unwrap();
let result = server_handle.await.unwrap();
assert!(
result.is_ok(),
"server should initialize successfully after ping"
);
result.unwrap().cancel().await.unwrap();
}
// Server buffers tools/list sent before initialized and processes it after initialization.
#[tokio::test]
async fn server_init_buffers_request_before_initialized() {
let (server_transport, client_transport) = tokio::io::duplex(4096);
let server_handle =
tokio::spawn(async move { TestServer::new().serve(server_transport).await });
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_transport);
do_initialize(&mut client).await;
// Send tools/list before initialized notification
client.send(list_tools_request(2)).await.unwrap();
// Now send initialized notification
client.send(initialized_notification()).await.unwrap();
// The buffered tools/list should be processed — expect a response
let response = client.receive().await.unwrap();
assert!(
matches!(response, ServerJsonRpcMessage::Response(_)),
"expected response for buffered tools/list, got: {response:?}"
);
let result = server_handle.await.unwrap();
assert!(
result.is_ok(),
"server should initialize successfully when buffering pre-init messages"
);
result.unwrap().cancel().await.unwrap();
}
fn init_request_with_version(v: &str) -> ClientJsonRpcMessage {
msg(&format!(
r#"{{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {{
"protocolVersion": "{v}",
"capabilities": {{}},
"clientInfo": {{ "name": "test-client", "version": "0.0.1" }}
}}
}}"#
))
}
async fn negotiate_version<H>(handler: H, client_version: &str) -> ProtocolVersion
where
H: ServerHandler + 'static,
{
let (server_transport, client_transport) = tokio::io::duplex(4096);
let _server = tokio::spawn(async move { handler.serve(server_transport).await });
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_transport);
client
.send(init_request_with_version(client_version))
.await
.unwrap();
let response = client.receive().await.unwrap();
let ServerJsonRpcMessage::Response(r) = response else {
panic!("expected initialize response, got {response:?}");
};
let ServerResult::InitializeResult(init) = r.result else {
panic!("expected InitializeResult");
};
init.protocol_version
}
#[tokio::test]
async fn server_echoes_client_protocol_version_when_known_old() {
let negotiated = negotiate_version(TestServer::new(), "2024-11-05").await;
assert_eq!(negotiated, ProtocolVersion::V_2024_11_05);
}
#[tokio::test]
async fn server_echoes_client_protocol_version_when_latest() {
let negotiated = negotiate_version(TestServer::new(), "2025-11-25").await;
assert_eq!(negotiated, ProtocolVersion::LATEST);
}
#[tokio::test]
async fn server_falls_back_when_client_protocol_version_unknown() {
let negotiated = negotiate_version(TestServer::new(), "2099-99-99").await;
assert_eq!(negotiated, ProtocolVersion::LATEST);
}
struct PinnedServer;
impl ServerHandler for PinnedServer {
fn get_info(&self) -> InitializeResult {
InitializeResult::new(ServerCapabilities::builder().build())
.with_protocol_version(ProtocolVersion::V_2025_06_18)
}
}
#[tokio::test]
async fn server_pinned_version_does_not_override_known_client_request() {
let negotiated = negotiate_version(PinnedServer, "2025-11-25").await;
assert_eq!(negotiated, ProtocolVersion::LATEST);
}
#[tokio::test]
async fn server_pinned_version_used_as_fallback_for_unknown_client_request() {
let negotiated = negotiate_version(PinnedServer, "2099-99-99").await;
assert_eq!(negotiated, ProtocolVersion::V_2025_06_18);
}
fn duplicate_init_request(id: u64, version: &str) -> ClientJsonRpcMessage {
msg(&format!(
r#"{{
"jsonrpc": "2.0",
"id": {id},
"method": "initialize",
"params": {{
"protocolVersion": "{version}",
"capabilities": {{ "sampling": {{}} }},
"clientInfo": {{ "name": "renegotiated-client", "version": "9.9.9" }}
}}
}}"#
))
}
#[tokio::test]
async fn server_accepts_duplicate_initialize() {
let (server_transport, client_transport) = tokio::io::duplex(4096);
let _server = tokio::spawn(async move { TestServer::new().serve(server_transport).await });
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_transport);
do_initialize(&mut client).await;
client.send(initialized_notification()).await.unwrap();
client
.send(duplicate_init_request(2, "2025-11-25"))
.await
.unwrap();
let response = client.receive().await.unwrap();
assert!(
matches!(response, ServerJsonRpcMessage::Response(_)),
"expected successful InitializeResult, got: {response:?}"
);
}
#[tokio::test]
async fn server_session_remains_usable_after_renegotiation() {
let (server_transport, client_transport) = tokio::io::duplex(4096);
let _server = tokio::spawn(async move { TestServer::new().serve(server_transport).await });
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_transport);
do_initialize(&mut client).await;
client.send(initialized_notification()).await.unwrap();
client
.send(duplicate_init_request(2, "2025-11-25"))
.await
.unwrap();
let _renegotiated = client.receive().await.unwrap();
client.send(ping_request(3)).await.unwrap();
let pong = client.receive().await.unwrap();
assert!(
matches!(
pong,
ServerJsonRpcMessage::Response(ref r)
if matches!(r.result, ServerResult::EmptyResult(_))
),
"expected EmptyResult ping after renegotiation, got: {pong:?}"
);
}
// Server buffers multiple requests before initialized and processes them in order.
#[tokio::test]
async fn server_init_buffers_multiple_requests_before_initialized() {
let (server_transport, client_transport) = tokio::io::duplex(4096);
let server_handle =
tokio::spawn(async move { TestServer::new().serve(server_transport).await });
let mut client = IntoTransport::<rmcp::RoleClient, _, _>::into_transport(client_transport);
do_initialize(&mut client).await;
// Send two requests before initialized
client.send(list_tools_request(2)).await.unwrap();
client.send(ping_request(3)).await.unwrap();
// Now send initialized notification
client.send(initialized_notification()).await.unwrap();
// Both buffered messages should get responses
let response1 = client.receive().await.unwrap();
let response2 = client.receive().await.unwrap();
assert!(
matches!(response1, ServerJsonRpcMessage::Response(_)),
"expected response for first buffered message, got: {response1:?}"
);
assert!(
matches!(response2, ServerJsonRpcMessage::Response(_)),
"expected response for second buffered message, got: {response2:?}"
);
let result = server_handle.await.unwrap();
assert!(
result.is_ok(),
"server should initialize successfully with multiple buffered messages"
);
result.unwrap().cancel().await.unwrap();
}