Skip to content
Closed
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
5 changes: 2 additions & 3 deletions crates/rmcp/src/handler/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use crate::{
model::{TaskSupport, *},
service::{
MaybeSendFuture, NotificationContext, RequestContext, RoleServer, Service, ServiceRole,
negotiate_protocol_version,
},
};

Expand Down Expand Up @@ -137,7 +136,7 @@ impl<H: ServerHandler> Service<RoleServer> for H {
// resource-not-found; older peers keep RESOURCE_NOT_FOUND. ISO `YYYY-MM-DD` versions
// compare lexically the same as chronologically.
let use_invalid_params =
protocol_version.is_some_and(|v| v.as_str() >= ProtocolVersion::V_2026_07_28.as_str());
protocol_version.is_some_and(|v| v >= ProtocolVersion::V_2026_07_28);
result.map_err(|mut error| {
if use_invalid_params && error.code == ErrorCode::RESOURCE_NOT_FOUND {
error.code = ErrorCode::INVALID_PARAMS;
Expand Down Expand Up @@ -205,7 +204,7 @@ macro_rules! server_handler_methods {
) -> impl Future<Output = Result<InitializeResult, McpError>> + MaybeSendFuture + '_ {
context.peer.set_peer_info(request.clone());
let mut info = self.get_info();
info.protocol_version = negotiate_protocol_version(
info.protocol_version = ProtocolVersion::negotiate(
&request.protocol_version,
info.protocol_version,
);
Expand Down
101 changes: 88 additions & 13 deletions crates/rmcp/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,17 @@ const_string!(JsonRpcVersion2_0 = "2.0");
///
/// This ensures compatibility between clients and servers by specifying
/// which version of the Model Context Protocol is being used.
// Ordering (derived `PartialOrd`) is lexical over the underlying string. MCP
// versions are ISO `YYYY-MM-DD`, so lexical order matches chronological order,
// and `a < b` means "`a` is older than `b`". Kept as a non-doc comment so it
// does not leak into the generated JSON schema.
#[derive(Debug, Clone, Eq, PartialEq, Hash, PartialOrd)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ProtocolVersion(Cow<'static, str>);

impl Default for ProtocolVersion {
fn default() -> Self {
Self::LATEST
Self::LATEST_PROTOCOL_VERSION
}
}

Expand All @@ -164,21 +168,63 @@ impl ProtocolVersion {
pub const V_2025_06_18: Self = Self(Cow::Borrowed("2025-06-18"));
pub const V_2025_03_26: Self = Self(Cow::Borrowed("2025-03-26"));
pub const V_2024_11_05: Self = Self(Cow::Borrowed("2024-11-05"));
pub const LATEST: Self = Self::V_2025_11_25;
/// Protocol version this SDK uses by default for initialize requests and responses.
pub const LATEST_PROTOCOL_VERSION: Self = Self::V_2025_11_25;

/// Protocol version assumed when a Streamable HTTP request omits the
/// `MCP-Protocol-Version` header and no negotiated version is available.
pub const DEFAULT_NEGOTIATED_PROTOCOL_VERSION: Self = Self::V_2025_03_26;

pub const LATEST: Self = Self::LATEST_PROTOCOL_VERSION;

/// All protocol versions known to this SDK.
pub const KNOWN_VERSIONS: &[Self] = &[
pub const SUPPORTED_PROTOCOL_VERSIONS: &[Self] = &[
Self::V_2024_11_05,
Self::V_2025_03_26,
Self::V_2025_06_18,
Self::V_2025_11_25,
Self::V_2026_07_28,
];
pub const KNOWN_VERSIONS: &[Self] = Self::SUPPORTED_PROTOCOL_VERSIONS;

/// Returns the string representation of this protocol version.
pub fn as_str(&self) -> &str {
&self.0
}

/// Parses a wire string into a [`ProtocolVersion`].
///
/// Known versions map to their interned `'static` constant so that
/// [`SUPPORTED_PROTOCOL_VERSIONS`](Self::SUPPORTED_PROTOCOL_VERSIONS) stays
/// the only source of truth for the supported set; any other string is kept
/// verbatim for forward compatibility with versions newer than this SDK.
pub(crate) fn from_wire_str(s: &str) -> Self {
Self::SUPPORTED_PROTOCOL_VERSIONS
.iter()
.find(|known| known.as_str() == s)
.cloned()
.unwrap_or_else(|| Self(Cow::Owned(s.to_owned())))
}

/// Negotiates the protocol version to advertise back to the client.
///
/// Echoes `client_requested` when it is a version this SDK knows; otherwise
/// falls back to `server_fallback` (and logs a warning). `server_fallback`
/// is the server's own version, used only as the default for unknown
/// requests rather than as a ceiling.
#[cfg(feature = "server")]
pub(crate) fn negotiate(client_requested: &Self, server_fallback: Self) -> Self {
if Self::SUPPORTED_PROTOCOL_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
}
}
}

impl Serialize for ProtocolVersion {
Expand All @@ -196,16 +242,7 @@ impl<'de> Deserialize<'de> for ProtocolVersion {
D: serde::Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
#[allow(clippy::single_match)]
match s.as_str() {
"2024-11-05" => return Ok(ProtocolVersion::V_2024_11_05),
"2025-03-26" => return Ok(ProtocolVersion::V_2025_03_26),
"2025-06-18" => return Ok(ProtocolVersion::V_2025_06_18),
"2025-11-25" => return Ok(ProtocolVersion::V_2025_11_25),
"2026-07-28" => return Ok(ProtocolVersion::V_2026_07_28),
_ => {}
}
Ok(ProtocolVersion(Cow::Owned(s)))
Ok(ProtocolVersion::from_wire_str(&s))
}
}

Expand Down Expand Up @@ -4093,6 +4130,44 @@ mod tests {
assert!(v3 < v4);
}

#[cfg(feature = "server")]
mod negotiate {
use super::*;

#[test]
fn echoes_known_client_version() {
let negotiated =
ProtocolVersion::negotiate(&ProtocolVersion::V_2025_06_18, ProtocolVersion::LATEST);
assert_eq!(negotiated, ProtocolVersion::V_2025_06_18);
}

#[test]
fn echoes_known_client_version_newer_than_server() {
// The client requested a version this SDK knows that is newer than the
// server's own (`LATEST`), so negotiation echoes it (up-negotiation):
// `server_fallback` is a default for unknown versions, not a ceiling.
let negotiated =
ProtocolVersion::negotiate(&ProtocolVersion::V_2026_07_28, ProtocolVersion::LATEST);
assert_eq!(negotiated, ProtocolVersion::V_2026_07_28);
}

#[test]
fn keeps_version_when_client_equals_server() {
let negotiated = ProtocolVersion::negotiate(
&ProtocolVersion::V_2025_06_18,
ProtocolVersion::V_2025_06_18,
);
assert_eq!(negotiated, ProtocolVersion::V_2025_06_18);
}

#[test]
fn falls_back_to_server_when_client_version_unknown() {
let unknown = ProtocolVersion(Cow::Borrowed("1999-01-01"));
let negotiated = ProtocolVersion::negotiate(&unknown, ProtocolVersion::LATEST);
assert_eq!(negotiated, ProtocolVersion::LATEST);
}
}

#[test]
fn test_icon_serialization() {
let icon = Icon {
Expand Down
19 changes: 1 addition & 18 deletions crates/rmcp/src/service/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,23 +161,6 @@ where
}
}

/// Echoes the client-requested version if known; otherwise returns `server_fallback`.
pub(crate) 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 @@ -250,7 +233,7 @@ where
return Err(ServerInitializeError::InitializeFailed(e));
}
};
init_response.protocol_version = negotiate_protocol_version(
init_response.protocol_version = ProtocolVersion::negotiate(
&peer_info.params.protocol_version,
init_response.protocol_version,
);
Expand Down
7 changes: 4 additions & 3 deletions crates/rmcp/src/transport/streamable_http_server/tower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ fn validate_protocol_version_header(headers: &http::HeaderMap) -> Result<(), Box
)
.expect("valid response")
})?;
let is_known = ProtocolVersion::KNOWN_VERSIONS
let is_known = ProtocolVersion::SUPPORTED_PROTOCOL_VERSIONS
.iter()
.any(|v| v.as_str() == version_str);
if !is_known {
Expand Down Expand Up @@ -1359,8 +1359,9 @@ where
headers
.get(HEADER_MCP_PROTOCOL_VERSION)
.and_then(|v| v.to_str().ok())
.and_then(|s| serde_json::from_value(serde_json::Value::String(s.to_owned())).ok())
.unwrap_or(ProtocolVersion::V_2025_03_26)
.map(ProtocolVersion::from_wire_str)
// Spec backwards-compat: assume 2025-03-26 when the header is absent.
.unwrap_or(ProtocolVersion::DEFAULT_NEGOTIATED_PROTOCOL_VERSION)
};
Some(InitializeRequestParams {
meta: None,
Expand Down
15 changes: 14 additions & 1 deletion crates/rmcp/tests/test_custom_headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -861,11 +861,24 @@ async fn test_server_rejects_unsupported_protocol_version() {
);
}

/// Unit test: ProtocolVersion::as_str and KNOWN_VERSIONS
/// Unit test: ProtocolVersion::as_str and version constants
#[test]
fn test_protocol_version_utilities() {
use rmcp::model::ProtocolVersion;

assert_eq!(
ProtocolVersion::LATEST,
ProtocolVersion::LATEST_PROTOCOL_VERSION
);
assert_eq!(
ProtocolVersion::DEFAULT_NEGOTIATED_PROTOCOL_VERSION,
ProtocolVersion::V_2025_03_26
);
assert_eq!(
ProtocolVersion::KNOWN_VERSIONS,
ProtocolVersion::SUPPORTED_PROTOCOL_VERSIONS
);

assert_eq!(ProtocolVersion::V_2026_07_28.as_str(), "2026-07-28");
assert_eq!(ProtocolVersion::V_2025_11_25.as_str(), "2025-11-25");
assert_eq!(ProtocolVersion::V_2025_06_18.as_str(), "2025-06-18");
Expand Down