fix: keep initialize on legacy protocol versions (#1228)
Claude-Session: https://claude.ai/code/session_01EPcBfiUWJJueJn5cve1sjp
This commit is contained in:
parent
4e74499783
commit
ad9832ec21
1 changed files with 369 additions and 120 deletions
|
|
@ -1,6 +1,7 @@
|
|||
//! Tests for protocol version negotiation in the default ServerHandler::initialize impl.
|
||||
//!
|
||||
//! Known versions are echoed back; unknown versions fall back to LATEST.
|
||||
//! Handshake versions are echoed back; every other version falls back to one
|
||||
//! the server can serve over `initialize`.
|
||||
#![cfg(not(feature = "local"))]
|
||||
#![cfg(feature = "client")]
|
||||
|
||||
|
|
@ -8,8 +9,11 @@ use std::borrow::Cow;
|
|||
|
||||
use rmcp::{
|
||||
ClientHandler, ErrorData, RoleServer, ServerHandler, ServiceExt,
|
||||
model::{ClientInfo, InitializeRequestParams, InitializeResult, ProtocolVersion, ServerInfo},
|
||||
service::RequestContext,
|
||||
model::{
|
||||
ClientInfo, ErrorCode, InitializeRequestParams, InitializeResult, ProtocolVersion,
|
||||
ServerInfo,
|
||||
},
|
||||
service::{ClientInitializeError, RequestContext},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
|
@ -21,9 +25,10 @@ impl ServerHandler for EchoServer {
|
|||
}
|
||||
}
|
||||
|
||||
/// Every known version except `2026-07-28`, standing in for a server that has
|
||||
/// not implemented that revision.
|
||||
const NARROWED_VERSIONS: &[ProtocolVersion] = &[
|
||||
/// Every known version whose lifecycle still runs the `initialize` handshake.
|
||||
/// `2026-07-28` replaced the handshake with per-request metadata, so this is
|
||||
/// also the list a server that has not implemented that revision supports.
|
||||
const HANDSHAKE_VERSIONS: &[ProtocolVersion] = &[
|
||||
ProtocolVersion::V_2024_11_05,
|
||||
ProtocolVersion::V_2025_03_26,
|
||||
ProtocolVersion::V_2025_06_18,
|
||||
|
|
@ -39,7 +44,25 @@ impl ServerHandler for NarrowedServer {
|
|||
}
|
||||
|
||||
fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
|
||||
Cow::Borrowed(NARROWED_VERSIONS)
|
||||
Cow::Borrowed(HANDSHAKE_VERSIONS)
|
||||
}
|
||||
}
|
||||
|
||||
/// Supports only revisions that have no `initialize` handshake at all.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct ModernOnlyServer;
|
||||
|
||||
const MODERN_ONLY_VERSIONS: &[ProtocolVersion] = &[ProtocolVersion::V_2026_07_28];
|
||||
|
||||
impl ServerHandler for ModernOnlyServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
let mut info = ServerInfo::default();
|
||||
info.protocol_version = ProtocolVersion::V_2026_07_28;
|
||||
info
|
||||
}
|
||||
|
||||
fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
|
||||
Cow::Borrowed(MODERN_ONLY_VERSIONS)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,7 +78,7 @@ impl ServerHandler for NarrowedOverridingServer {
|
|||
}
|
||||
|
||||
fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
|
||||
Cow::Borrowed(NARROWED_VERSIONS)
|
||||
Cow::Borrowed(HANDSHAKE_VERSIONS)
|
||||
}
|
||||
|
||||
async fn initialize(
|
||||
|
|
@ -88,23 +111,28 @@ async fn negotiated_version_with<S: ServerHandler>(
|
|||
server: S,
|
||||
client_version: ProtocolVersion,
|
||||
) -> ProtocolVersion {
|
||||
negotiate_with(server, client_version)
|
||||
.await
|
||||
.expect("client should connect")
|
||||
}
|
||||
|
||||
async fn negotiate_with<S: ServerHandler>(
|
||||
server: S,
|
||||
client_version: ProtocolVersion,
|
||||
) -> Result<ProtocolVersion, ClientInitializeError> {
|
||||
let (server_transport, client_transport) = tokio::io::duplex(4096);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _ = server
|
||||
.serve(server_transport)
|
||||
.await
|
||||
.expect("server should start")
|
||||
.waiting()
|
||||
.await;
|
||||
if let Ok(running) = server.serve(server_transport).await {
|
||||
let _ = running.waiting().await;
|
||||
}
|
||||
});
|
||||
|
||||
let client = VersionedClient {
|
||||
protocol_version: client_version,
|
||||
}
|
||||
.serve(client_transport)
|
||||
.await
|
||||
.expect("client should connect");
|
||||
.await?;
|
||||
|
||||
let version = client
|
||||
.peer_info()
|
||||
|
|
@ -113,20 +141,55 @@ async fn negotiated_version_with<S: ServerHandler>(
|
|||
.clone();
|
||||
|
||||
client.cancel().await.expect("client should cancel");
|
||||
version
|
||||
Ok(version)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn known_version_echoed_back() {
|
||||
for version in ProtocolVersion::KNOWN_VERSIONS {
|
||||
async fn handshake_version_echoed_back() {
|
||||
for version in HANDSHAKE_VERSIONS {
|
||||
let negotiated = negotiated_version(version.clone()).await;
|
||||
assert_eq!(
|
||||
negotiated, *version,
|
||||
"known version {version} should be echoed back"
|
||||
"handshake version {version} should be echoed back"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `initialize` disappeared in `2026-07-28`, so agreeing to it here would leave
|
||||
/// the peers speaking a revision that has no handshake at all.
|
||||
#[tokio::test]
|
||||
async fn handshake_never_agrees_to_a_version_that_dropped_it() {
|
||||
let negotiated = negotiated_version(ProtocolVersion::V_2026_07_28).await;
|
||||
assert_eq!(
|
||||
negotiated,
|
||||
ProtocolVersion::LATEST,
|
||||
"a version that dropped the handshake should fall back to the server's own"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn modern_only_server_rejects_the_handshake() {
|
||||
let error = negotiate_with(ModernOnlyServer, ProtocolVersion::V_2026_07_28)
|
||||
.await
|
||||
.expect_err("a server with no handshake version cannot answer initialize");
|
||||
let ClientInitializeError::JsonRpcError(error) = error else {
|
||||
panic!("expected a JSON-RPC error, got {error:?}");
|
||||
};
|
||||
assert_eq!(
|
||||
error.code,
|
||||
ErrorCode::UNSUPPORTED_PROTOCOL_VERSION,
|
||||
"a server with no handshake version should reject initialize"
|
||||
);
|
||||
assert_eq!(
|
||||
error.data,
|
||||
Some(serde_json::json!({
|
||||
"requested": "2026-07-28",
|
||||
"supported": ["2026-07-28"],
|
||||
})),
|
||||
"the rejection should name the versions the server does support"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unknown_version_falls_back_to_latest() {
|
||||
let unknown: ProtocolVersion = serde_json::from_str(r#""1999-01-01""#).unwrap();
|
||||
|
|
@ -140,7 +203,7 @@ async fn unknown_version_falls_back_to_latest() {
|
|||
|
||||
#[tokio::test]
|
||||
async fn narrowed_server_still_echoes_versions_it_supports() {
|
||||
for version in NARROWED_VERSIONS {
|
||||
for version in HANDSHAKE_VERSIONS {
|
||||
let negotiated = negotiated_version_with(NarrowedServer, version.clone()).await;
|
||||
assert_eq!(
|
||||
negotiated, *version,
|
||||
|
|
|
|||
Loading…
Reference in a new issue