fix: honor supported_protocol_versions when negotiating initialize (#1093)
This commit is contained in:
parent
51c8ee60e3
commit
d272389fd4
2 changed files with 246 additions and 16 deletions
|
|
@ -4,9 +4,12 @@
|
|||
#![cfg(not(feature = "local"))]
|
||||
#![cfg(feature = "client")]
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use rmcp::{
|
||||
ClientHandler, ServerHandler, ServiceExt,
|
||||
model::{ClientInfo, ProtocolVersion, ServerInfo},
|
||||
ClientHandler, ErrorData, RoleServer, ServerHandler, ServiceExt,
|
||||
model::{ClientInfo, InitializeRequestParams, InitializeResult, ProtocolVersion, ServerInfo},
|
||||
service::RequestContext,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
|
@ -18,6 +21,52 @@ 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] = &[
|
||||
ProtocolVersion::V_2024_11_05,
|
||||
ProtocolVersion::V_2025_03_26,
|
||||
ProtocolVersion::V_2025_06_18,
|
||||
ProtocolVersion::V_2025_11_25,
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct NarrowedServer;
|
||||
|
||||
impl ServerHandler for NarrowedServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::default()
|
||||
}
|
||||
|
||||
fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
|
||||
Cow::Borrowed(NARROWED_VERSIONS)
|
||||
}
|
||||
}
|
||||
|
||||
/// Narrows the supported versions *and* overrides `initialize`, so the
|
||||
/// handler's own answer never runs the default negotiation. The handshake layer
|
||||
/// must still honor the narrowed list.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct NarrowedOverridingServer;
|
||||
|
||||
impl ServerHandler for NarrowedOverridingServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::default()
|
||||
}
|
||||
|
||||
fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
|
||||
Cow::Borrowed(NARROWED_VERSIONS)
|
||||
}
|
||||
|
||||
async fn initialize(
|
||||
&self,
|
||||
_request: InitializeRequestParams,
|
||||
_context: RequestContext<RoleServer>,
|
||||
) -> Result<InitializeResult, ErrorData> {
|
||||
Ok(self.get_info())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct VersionedClient {
|
||||
protocol_version: ProtocolVersion,
|
||||
|
|
@ -32,10 +81,17 @@ impl ClientHandler for VersionedClient {
|
|||
}
|
||||
|
||||
async fn negotiated_version(client_version: ProtocolVersion) -> ProtocolVersion {
|
||||
negotiated_version_with(EchoServer, client_version).await
|
||||
}
|
||||
|
||||
async fn negotiated_version_with<S: ServerHandler>(
|
||||
server: S,
|
||||
client_version: ProtocolVersion,
|
||||
) -> ProtocolVersion {
|
||||
let (server_transport, client_transport) = tokio::io::duplex(4096);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _ = EchoServer
|
||||
let _ = server
|
||||
.serve(server_transport)
|
||||
.await
|
||||
.expect("server should start")
|
||||
|
|
@ -81,3 +137,35 @@ async fn unknown_version_falls_back_to_latest() {
|
|||
"unknown version should fall back to LATEST"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn narrowed_server_still_echoes_versions_it_supports() {
|
||||
for version in NARROWED_VERSIONS {
|
||||
let negotiated = negotiated_version_with(NarrowedServer, version.clone()).await;
|
||||
assert_eq!(
|
||||
negotiated, *version,
|
||||
"supported version {version} should be echoed back"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn narrowed_server_does_not_agree_to_version_it_excludes() {
|
||||
let negotiated = negotiated_version_with(NarrowedServer, ProtocolVersion::V_2026_07_28).await;
|
||||
assert_eq!(
|
||||
negotiated,
|
||||
ProtocolVersion::V_2025_11_25,
|
||||
"a version outside supported_protocol_versions should not be echoed back"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn narrowed_server_caps_even_when_it_overrides_initialize() {
|
||||
let negotiated =
|
||||
negotiated_version_with(NarrowedOverridingServer, ProtocolVersion::V_2026_07_28).await;
|
||||
assert_eq!(
|
||||
negotiated,
|
||||
ProtocolVersion::V_2025_11_25,
|
||||
"the handshake layer should not raise the version above what the server supports"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
//! Tests for protocol version negotiation in stateless HTTP mode.
|
||||
//!
|
||||
//! Known versions are echoed back; unknown versions fall back to LATEST.
|
||||
//! Supported versions are echoed back; unknown versions, and versions outside
|
||||
//! the server's `supported_protocol_versions`, fall back to the handler's own
|
||||
//! version.
|
||||
#![cfg(not(feature = "local"))]
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use rmcp::{
|
||||
ErrorData, RoleServer, ServerHandler,
|
||||
model::{
|
||||
|
|
@ -15,7 +19,7 @@ use rmcp::{
|
|||
};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Default)]
|
||||
struct OverridingInitialize;
|
||||
|
||||
impl ServerHandler for OverridingInitialize {
|
||||
|
|
@ -32,6 +36,38 @@ impl ServerHandler for OverridingInitialize {
|
|||
}
|
||||
}
|
||||
|
||||
/// Every known version except `2026-07-28`, standing in for a server that has
|
||||
/// not implemented that revision.
|
||||
const NARROWED_VERSIONS: &[ProtocolVersion] = &[
|
||||
ProtocolVersion::V_2024_11_05,
|
||||
ProtocolVersion::V_2025_03_26,
|
||||
ProtocolVersion::V_2025_06_18,
|
||||
ProtocolVersion::V_2025_11_25,
|
||||
];
|
||||
|
||||
/// Overrides `initialize`, so the handler-side default negotiation never runs,
|
||||
/// *and* narrows the supported versions.
|
||||
#[derive(Clone, Default)]
|
||||
struct NarrowedOverridingInitialize;
|
||||
|
||||
impl ServerHandler for NarrowedOverridingInitialize {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::new(ServerCapabilities::default())
|
||||
}
|
||||
|
||||
fn supported_protocol_versions(&self) -> Cow<'static, [ProtocolVersion]> {
|
||||
Cow::Borrowed(NARROWED_VERSIONS)
|
||||
}
|
||||
|
||||
async fn initialize(
|
||||
&self,
|
||||
_request: InitializeRequestParams,
|
||||
_context: RequestContext<RoleServer>,
|
||||
) -> Result<InitializeResult, ErrorData> {
|
||||
Ok(self.get_info())
|
||||
}
|
||||
}
|
||||
|
||||
fn stateless_sse_config() -> StreamableHttpServerConfig {
|
||||
StreamableHttpServerConfig::default()
|
||||
.with_legacy_session_mode(false)
|
||||
|
|
@ -45,10 +81,16 @@ fn stateless_json_config() -> StreamableHttpServerConfig {
|
|||
|
||||
async fn spawn_server(
|
||||
config: StreamableHttpServerConfig,
|
||||
) -> (reqwest::Client, String, CancellationToken) {
|
||||
spawn_server_of::<OverridingInitialize>(config).await
|
||||
}
|
||||
|
||||
async fn spawn_server_of<H: ServerHandler + Default>(
|
||||
config: StreamableHttpServerConfig,
|
||||
) -> (reqwest::Client, String, CancellationToken) {
|
||||
let ct = config.cancellation_token.clone();
|
||||
let service: StreamableHttpService<OverridingInitialize, LocalSessionManager> =
|
||||
StreamableHttpService::new(|| Ok(OverridingInitialize), Default::default(), config);
|
||||
let service: StreamableHttpService<H, LocalSessionManager> =
|
||||
StreamableHttpService::new(|| Ok(H::default()), Default::default(), config);
|
||||
|
||||
let router = axum::Router::new().nest_service("/mcp", service);
|
||||
let tcp_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
|
|
@ -150,3 +192,35 @@ async fn stateless_json_init_preserves_handler_fallback_for_unknown_version() {
|
|||
|
||||
ct.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stateless_json_init_echoes_versions_the_server_narrowed_to() {
|
||||
let (client, url, ct) =
|
||||
spawn_server_of::<NarrowedOverridingInitialize>(stateless_json_config()).await;
|
||||
|
||||
for version in NARROWED_VERSIONS {
|
||||
let resp = post_init(&client, &url, version.as_str()).await;
|
||||
assert_eq!(
|
||||
resp["result"]["protocolVersion"],
|
||||
version.as_str(),
|
||||
"supported version {version} should be echoed back"
|
||||
);
|
||||
}
|
||||
|
||||
ct.cancel();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stateless_json_init_does_not_agree_to_version_outside_supported_list() {
|
||||
let (client, url, ct) =
|
||||
spawn_server_of::<NarrowedOverridingInitialize>(stateless_json_config()).await;
|
||||
|
||||
let resp = post_init(&client, &url, ProtocolVersion::V_2026_07_28.as_str()).await;
|
||||
assert_eq!(
|
||||
resp["result"]["protocolVersion"],
|
||||
ProtocolVersion::V_2025_11_25.as_str(),
|
||||
"a version outside supported_protocol_versions should not be echoed back"
|
||||
);
|
||||
|
||||
ct.cancel();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue