diff --git a/crates/rmcp/src/service/client.rs b/crates/rmcp/src/service/client.rs index 9fc1f641..6d0041a7 100644 --- a/crates/rmcp/src/service/client.rs +++ b/crates/rmcp/src/service/client.rs @@ -568,7 +568,8 @@ pub enum ClientLifecycleMode { Discover { preferred_versions: Vec, }, - /// Probe with `server/discover`, falling back only when the peer proves it is legacy. + /// Probe with `server/discover`, falling back to legacy initialization on failures that do + /// not identify a modern server. Auto { preferred_versions: Vec, legacy_version: Option, @@ -718,9 +719,17 @@ where .await; match discover_result { Ok(()) => {} - Err(ClientInitializeError::JsonRpcError(error)) - if error.code == crate::model::ErrorCode::METHOD_NOT_FOUND => - { + // `UnsupportedProtocolVersionError` is handled by `discover_startup`, which + // retries with an advertised compatible version. Reaching this point with no + // compatible version therefore positively identifies a modern server and must + // not trigger the legacy handshake. + Err(error @ ClientInitializeError::NoCompatibleProtocolVersion { .. }) => { + return Err(error); + } + // On stdio, legacy servers may reject an unknown pre-initialize request with + // any implementation-defined error (or not respond at all). Treat every + // failure not recognized as modern version negotiation as a legacy peer. + Err(_) => { let mut legacy_info = client_info; if let Some(version) = legacy_version { legacy_info.protocol_version = version; @@ -728,7 +737,6 @@ where legacy_startup(&service, &mut transport, &id_provider, &peer, legacy_info) .await?; } - Err(error) => return Err(error), } } } diff --git a/crates/rmcp/tests/test_client_lifecycle_modes.rs b/crates/rmcp/tests/test_client_lifecycle_modes.rs index 4b75c355..64383f13 100644 --- a/crates/rmcp/tests/test_client_lifecycle_modes.rs +++ b/crates/rmcp/tests/test_client_lifecycle_modes.rs @@ -337,6 +337,66 @@ async fn auto_startup_falls_back_after_discover_method_not_found() { server_task.await.expect("server task"); } +#[tokio::test] +async fn auto_startup_falls_back_after_discover_invalid_params() { + let (server_transport, client_transport) = tokio::io::duplex(4096); + let mut server = IntoTransport::::into_transport(server_transport); + let server_task = tokio::spawn(async move { + let ClientJsonRpcMessage::Request(discover) = + server.receive().await.expect("expected discover request") + else { + panic!("expected request"); + }; + assert!(matches!( + discover.request, + ClientRequest::DiscoverRequest(_) + )); + server + .send(ServerJsonRpcMessage::error( + ErrorData::new(ErrorCode::INVALID_PARAMS, "Invalid params", None), + Some(discover.id), + )) + .await + .expect("send invalid-params"); + + let ClientJsonRpcMessage::Request(initialize) = + server.receive().await.expect("expected initialize request") + else { + panic!("expected request"); + }; + assert!(matches!( + initialize.request, + ClientRequest::InitializeRequest(_) + )); + server + .send(ServerJsonRpcMessage::response( + ServerResult::InitializeResult( + InitializeResult::new(ServerCapabilities::default()), + ), + initialize.id, + )) + .await + .expect("send initialize response"); + assert!(matches!( + server.receive().await, + Some(ClientJsonRpcMessage::Notification(_)) + )); + }); + + let client = DiscoverClient + .serve_with_lifecycle( + client_transport, + ClientLifecycleMode::Auto { + preferred_versions: vec![ProtocolVersion::V_2026_07_28], + legacy_version: Some(ProtocolVersion::V_2025_11_25), + }, + ) + .await + .expect("auto client should fall back"); + client.cancel().await.expect("cancel client"); + server_task.await.expect("server task"); +} + #[tokio::test] async fn discover_startup_retries_a_mutually_supported_version() { let unsupported: ProtocolVersion =