Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ jobs:
cargo semver-checks \
--package rmcp \
--baseline-rev ${{ github.event.pull_request.base.sha }} \
--release-type minor \
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this, the CI fails completely on any non-patch change. With it, release-plz still determines the actual version bump, but the CI gate now only catches major bumps with breaking regressions. Minor additions, like this deprecation, are allowed.

--only-explicit-features \
--features default

Expand All @@ -97,6 +98,7 @@ jobs:
cargo semver-checks \
--package rmcp \
--baseline-rev ${{ github.event.pull_request.base.sha }} \
--release-type minor \
--only-explicit-features \
--features "$FEATURES"

Expand Down
35 changes: 25 additions & 10 deletions crates/rmcp/src/service/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ pub enum ServerInitializeError {
#[error("initialize failed: {0}")]
InitializeFailed(ErrorData),

#[deprecated(
since = "1.8.0",
note = "Negotiation now falls back to the server-configured version. This variant is never constructed and will be removed in a future major release."
)]
#[error("unsupported protocol version: {0}")]
UnsupportedProtocolVersion(ProtocolVersion),

Expand Down Expand Up @@ -155,6 +159,23 @@ where
}
}

/// Echoes the client-requested version if known; otherwise returns `server_fallback`.
fn negotiate_protocol_version(
client_requested: &ProtocolVersion,
server_fallback: ProtocolVersion,
) -> ProtocolVersion {
if ProtocolVersion::KNOWN_VERSIONS.contains(client_requested) {
client_requested.clone()
} else {
tracing::warn!(
client_requested = %client_requested,
server_fallback = %server_fallback,
"client requested unsupported protocol version; falling back to server default"
);
server_fallback
}
}

async fn serve_server_with_ct_inner<S, T>(
service: S,
transport: T,
Expand Down Expand Up @@ -227,16 +248,10 @@ where
return Err(ServerInitializeError::InitializeFailed(e));
}
};
let peer_protocol_version = peer_info.params.protocol_version.clone();
let protocol_version = match peer_protocol_version
.partial_cmp(&init_response.protocol_version)
.ok_or(ServerInitializeError::UnsupportedProtocolVersion(
peer_protocol_version,
))? {
std::cmp::Ordering::Less => peer_info.params.protocol_version.clone(),
_ => init_response.protocol_version,
};
init_response.protocol_version = protocol_version;
init_response.protocol_version = negotiate_protocol_version(
&peer_info.params.protocol_version,
init_response.protocol_version,
);
transport
.send(ServerJsonRpcMessage::response(
ServerResult::InitializeResult(init_response),
Expand Down
83 changes: 81 additions & 2 deletions crates/rmcp/tests/test_server_initialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ mod common;

use common::handlers::TestServer;
use rmcp::{
ServiceExt,
model::{ClientJsonRpcMessage, ServerJsonRpcMessage, ServerResult},
ServerHandler, ServiceExt,
model::{
ClientJsonRpcMessage, ProtocolVersion, ServerCapabilities, ServerInfo,
ServerJsonRpcMessage, ServerResult,
},
transport::{IntoTransport, Transport},
};

Expand Down Expand Up @@ -220,6 +223,82 @@ async fn server_init_buffers_request_before_initialized() {
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) -> ServerInfo {
ServerInfo::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);
}

// Server buffers multiple requests before initialized and processes them in order.
#[tokio::test]
async fn server_init_buffers_multiple_requests_before_initialized() {
Expand Down