Skip to content

Commit 084d9d3

Browse files
authored
fix: prevent termination caused by client using older mcp schema versions (#40)
* fix: protocol version check * chore: add more tests * chore: cleanup * chore: fix clippy warnings * chore: typo * chore: typo * fix: incorrect versions
1 parent 5ebcec2 commit 084d9d3

File tree

16 files changed

+255
-22
lines changed

16 files changed

+255
-22
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ async fn main() -> SdkResult<()> {
212212
name: "simple-rust-mcp-client".into(),
213213
version: "0.1.0".into(),
214214
},
215-
protocol_version: JSONRPC_VERSION.into(),
215+
protocol_version: LATEST_PROTOCOL_VERSION.into(),
216216
};
217217

218218
// Step3 : Create a transport, with options to launch @modelcontextprotocol/server-everything MCP Server

crates/rust-mcp-sdk/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ async fn main() -> SdkResult<()> {
212212
name: "simple-rust-mcp-client".into(),
213213
version: "0.1.0".into(),
214214
},
215-
protocol_version: JSONRPC_VERSION.into(),
215+
protocol_version: LATEST_PROTOCOL_VERSION.into(),
216216
};
217217

218218
// Step3 : Create a transport, with options to launch @modelcontextprotocol/server-everything MCP Server

crates/rust-mcp-sdk/src/error.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ pub enum McpSdkError {
1616
#[error("{0}")]
1717
TransportError(#[from] TransportError),
1818
#[error("{0}")]
19-
AnyErrorStatic(Box<(dyn std::error::Error + Send + Sync + 'static)>),
20-
#[error("{0}")]
2119
AnyError(Box<(dyn std::error::Error + Send + Sync)>),
2220
#[error("{0}")]
2321
SdkError(#[from] rust_mcp_schema::schema_utils::SdkError),
2422
#[cfg(feature = "hyper-server")]
2523
#[error("{0}")]
2624
TransportServerError(#[from] TransportServerError),
25+
#[error("Incompatible mcp protocol version!\n client:{0}\nserver:{1}")]
26+
IncompatibleProtocolVersion(String, String),
2727
}
2828

2929
#[deprecated(since = "0.2.0", note = "Use `McpSdkError` instead.")]

crates/rust-mcp-sdk/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ pub mod mcp_client {
3636
pub use super::mcp_runtimes::client_runtime::mcp_client_runtime as client_runtime;
3737
pub use super::mcp_runtimes::client_runtime::mcp_client_runtime_core as client_runtime_core;
3838
pub use super::mcp_runtimes::client_runtime::ClientRuntime;
39+
pub use super::utils::ensure_server_protocole_compatibility;
3940
}
4041

4142
#[cfg(feature = "server")]
@@ -75,6 +76,7 @@ pub mod mcp_server {
7576
pub use super::hyper_servers::hyper_server_core;
7677
#[cfg(feature = "hyper-server")]
7778
pub use super::hyper_servers::*;
79+
pub use super::utils::enforce_compatible_protocol_version;
7880
}
7981

8082
#[cfg(feature = "client")]

crates/rust-mcp-sdk/src/mcp_handlers/mcp_server_handler.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use async_trait::async_trait;
22
use rust_mcp_schema::{schema_utils::CallToolError, *};
33
use serde_json::Value;
44

5-
use crate::mcp_traits::mcp_server::McpServer;
5+
use crate::{mcp_traits::mcp_server::McpServer, utils::enforce_compatible_protocol_version};
66

77
/// Defines the `ServerHandler` trait for handling Model Context Protocol (MCP) operations on a server.
88
/// This trait provides default implementations for request and notification handlers in an MCP server,
@@ -35,7 +35,19 @@ pub trait ServerHandler: Send + Sync + 'static {
3535
.set_client_details(initialize_request.params.clone())
3636
.map_err(|err| RpcError::internal_error().with_message(format!("{}", err)))?;
3737

38-
Ok(runtime.server_info().to_owned())
38+
let mut server_info = runtime.server_info().to_owned();
39+
// Provide compatibility for clients using older MCP protocol versions.
40+
41+
if let Some(updated_protocol_version) = enforce_compatible_protocol_version(
42+
&initialize_request.params.protocol_version,
43+
&server_info.protocol_version,
44+
)
45+
.map_err(|err| RpcError::internal_error().with_message(err.to_string()))?
46+
{
47+
server_info.protocol_version = initialize_request.params.protocol_version;
48+
}
49+
50+
Ok(server_info)
3951
}
4052

4153
/// Handles ping requests from clients.

crates/rust-mcp-sdk/src/mcp_runtimes/client_runtime.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use tokio::sync::Mutex;
1717
use crate::error::{McpSdkError, SdkResult};
1818
use crate::mcp_traits::mcp_client::McpClient;
1919
use crate::mcp_traits::mcp_handler::McpClientHandler;
20+
use crate::utils::ensure_server_protocole_compatibility;
2021

2122
pub struct ClientRuntime {
2223
// The transport interface for handling messages between client and server
@@ -57,6 +58,11 @@ impl ClientRuntime {
5758
let result: ServerResult = self.request(request.into(), None).await?.try_into()?;
5859

5960
if let ServerResult::InitializeResult(initialize_result) = result {
61+
ensure_server_protocole_compatibility(
62+
&self.client_details.protocol_version,
63+
&initialize_result.protocol_version,
64+
)?;
65+
6066
// store server details
6167
self.set_server_details(initialize_result)?;
6268
// send a InitializedNotification to the server

crates/rust-mcp-sdk/src/utils.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
use std::cmp::Ordering;
2+
3+
use crate::error::{McpSdkError, SdkResult};
4+
15
/// Formats an assertion error message for unsupported capabilities.
26
///
37
/// Constructs a string describing that a specific entity (e.g., server or client) lacks
@@ -23,6 +27,126 @@ pub fn format_assertion_message(entity: &str, capability: &str, method_name: &st
2327
)
2428
}
2529

30+
/// Checks if the client and server protocol versions are compatible by ensuring they are equal.
31+
///
32+
/// This function compares the provided client and server protocol versions. If they are equal,
33+
/// it returns `Ok(())`, indicating compatibility. If they differ (either the client version is
34+
/// lower or higher than the server version), it returns an error with details about the
35+
/// incompatible versions.
36+
///
37+
/// # Arguments
38+
///
39+
/// * `client_protocol_version` - A string slice representing the client's protocol version.
40+
/// * `server_protocol_version` - A string slice representing the server's protocol version.
41+
///
42+
/// # Returns
43+
///
44+
/// * `Ok(())` if the versions are equal.
45+
/// * `Err(McpSdkError::IncompatibleProtocolVersion)` if the versions differ, containing the
46+
/// client and server versions as strings.
47+
///
48+
/// # Examples
49+
///
50+
/// ```
51+
/// use rust_mcp_sdk::mcp_client::ensure_server_protocole_compatibility;
52+
/// use rust_mcp_sdk::error::McpSdkError;
53+
///
54+
/// // Compatible versions
55+
/// let result = ensure_server_protocole_compatibility("2024_11_05", "2024_11_05");
56+
/// assert!(result.is_ok());
57+
///
58+
/// // Incompatible versions (client < server)
59+
/// let result = ensure_server_protocole_compatibility("2024_11_05", "2025_03_26");
60+
/// assert!(matches!(
61+
/// result,
62+
/// Err(McpSdkError::IncompatibleProtocolVersion(client, server))
63+
/// if client == "2024_11_05" && server == "2025_03_26"
64+
/// ));
65+
///
66+
/// // Incompatible versions (client > server)
67+
/// let result = ensure_server_protocole_compatibility("2025_03_26", "2024_11_05");
68+
/// assert!(matches!(
69+
/// result,
70+
/// Err(McpSdkError::IncompatibleProtocolVersion(client, server))
71+
/// if client == "2025_03_26" && server == "2024_11_05"
72+
/// ));
73+
/// ```
74+
#[allow(unused)]
75+
pub fn ensure_server_protocole_compatibility(
76+
client_protocol_version: &str,
77+
server_protocol_version: &str,
78+
) -> SdkResult<()> {
79+
match client_protocol_version.cmp(server_protocol_version) {
80+
Ordering::Less | Ordering::Greater => Err(McpSdkError::IncompatibleProtocolVersion(
81+
client_protocol_version.to_string(),
82+
server_protocol_version.to_string(),
83+
)),
84+
Ordering::Equal => Ok(()),
85+
}
86+
}
87+
88+
/// Enforces protocol version compatibility on for MCP Server , allowing the client to use a lower or equal version.
89+
///
90+
/// This function compares the client and server protocol versions. If the client version is
91+
/// higher than the server version, it returns an error indicating incompatibility. If the
92+
/// versions are equal, it returns `Ok(None)`, indicating no downgrade is needed. If the client
93+
/// version is lower, it returns `Ok(Some(client_protocol_version))`, suggesting the server
94+
/// can use the client's version for compatibility.
95+
///
96+
/// # Arguments
97+
///
98+
/// * `client_protocol_version` - The client's protocol version.
99+
/// * `server_protocol_version` - The server's protocol version.
100+
///
101+
/// # Returns
102+
///
103+
/// * `Ok(None)` if the versions are equal, indicating no downgrade is needed.
104+
/// * `Ok(Some(client_protocol_version))` if the client version is lower, returning the client
105+
/// version to use for compatibility.
106+
/// * `Err(McpSdkError::IncompatibleProtocolVersion)` if the client version is higher, containing
107+
/// the client and server versions as strings.
108+
///
109+
/// # Examples
110+
///
111+
/// ```
112+
/// use rust_mcp_sdk::mcp_server::enforce_compatible_protocol_version;
113+
/// use rust_mcp_sdk::error::McpSdkError;
114+
///
115+
/// // Equal versions
116+
/// let result = enforce_compatible_protocol_version("2024_11_05", "2024_11_05");
117+
/// assert!(matches!(result, Ok(None)));
118+
///
119+
/// // Client version lower (downgrade allowed)
120+
/// let result = enforce_compatible_protocol_version("2024_11_05", "2025_03_26");
121+
/// assert!(matches!(result, Ok(Some(ref v)) if v == "2024_11_05"));
122+
///
123+
/// // Client version higher (incompatible)
124+
/// let result = enforce_compatible_protocol_version("2025_03_26", "2024_11_05");
125+
/// assert!(matches!(
126+
/// result,
127+
/// Err(McpSdkError::IncompatibleProtocolVersion(client, server))
128+
/// if client == "2025_03_26" && server == "2024_11_05"
129+
/// ));
130+
/// ```
131+
#[allow(unused)]
132+
pub fn enforce_compatible_protocol_version(
133+
client_protocol_version: &str,
134+
server_protocol_version: &str,
135+
) -> SdkResult<Option<String>> {
136+
match client_protocol_version.cmp(server_protocol_version) {
137+
// if client protocol version is higher
138+
Ordering::Greater => Err(McpSdkError::IncompatibleProtocolVersion(
139+
client_protocol_version.to_string(),
140+
server_protocol_version.to_string(),
141+
)),
142+
Ordering::Equal => Ok(None),
143+
Ordering::Less => {
144+
// return the same version that was received from the client
145+
Ok(Some(client_protocol_version.to_string()))
146+
}
147+
}
148+
}
149+
26150
/// Removes query string and hash fragment from a URL, returning the base path.
27151
///
28152
/// # Arguments

crates/rust-mcp-sdk/tests/common/common.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
mod test_server;
22
use async_trait::async_trait;
33
use rust_mcp_schema::{
4-
ClientCapabilities, Implementation, InitializeRequestParams, JSONRPC_VERSION,
4+
ClientCapabilities, Implementation, InitializeRequestParams, LATEST_PROTOCOL_VERSION,
55
};
66
use rust_mcp_sdk::mcp_client::ClientHandler;
77
pub use test_server::*;
@@ -18,7 +18,7 @@ pub fn test_client_info() -> InitializeRequestParams {
1818
name: "test-rust-mcp-client".into(),
1919
version: "0.1.0".into(),
2020
},
21-
protocol_version: JSONRPC_VERSION.into(),
21+
protocol_version: LATEST_PROTOCOL_VERSION.into(),
2222
}
2323
}
2424

crates/rust-mcp-sdk/tests/common/test_server.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ pub mod test_server_common {
55

66
use rust_mcp_schema::{
77
Implementation, InitializeResult, ServerCapabilities, ServerCapabilitiesTools,
8-
LATEST_PROTOCOL_VERSION,
98
};
109
use rust_mcp_sdk::{
1110
mcp_server::{hyper_server, HyperServer, HyperServerOptions, IdGenerator, ServerHandler},
@@ -32,7 +31,7 @@ pub mod test_server_common {
3231
},
3332
meta: None,
3433
instructions: Some("server instructions...".to_string()),
35-
protocol_version: LATEST_PROTOCOL_VERSION.to_string(),
34+
protocol_version: "2025-03-26".to_string(),
3635
}
3736
}
3837

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#[path = "common/common.rs"]
2+
pub mod common;
3+
4+
mod protocol_compatibility_on_server {
5+
6+
use rust_mcp_schema::{InitializeRequest, InitializeResult, RpcError, INTERNAL_ERROR};
7+
use rust_mcp_sdk::mcp_server::ServerHandler;
8+
9+
use crate::common::{
10+
test_client_info,
11+
test_server_common::{test_server_details, TestServerHandler},
12+
};
13+
14+
async fn handle_initialize_request(
15+
client_protocol_version: &str,
16+
) -> Result<InitializeResult, RpcError> {
17+
let handler = TestServerHandler {};
18+
19+
let mut initialize_request = test_client_info();
20+
initialize_request.protocol_version = client_protocol_version.to_string();
21+
22+
let transport =
23+
rust_mcp_sdk::StdioTransport::new(rust_mcp_sdk::TransportOptions::default()).unwrap();
24+
25+
// mock unused runtime
26+
let runtime = rust_mcp_sdk::mcp_server::server_runtime::create_server(
27+
test_server_details(),
28+
transport,
29+
TestServerHandler {},
30+
);
31+
32+
handler
33+
.handle_initialize_request(InitializeRequest::new(initialize_request), &runtime)
34+
.await
35+
}
36+
37+
#[tokio::test]
38+
async fn tets_protocol_compatibility_equal() {
39+
let result = handle_initialize_request("2025-03-26").await;
40+
assert!(result.is_ok());
41+
let protocol_version = result.unwrap().protocol_version;
42+
assert_eq!(protocol_version, "2025-03-26");
43+
}
44+
45+
#[tokio::test]
46+
async fn tets_protocol_compatibility_downgrade() {
47+
let result = handle_initialize_request("2024_11_05").await;
48+
assert!(result.is_ok());
49+
let protocol_version = result.unwrap().protocol_version;
50+
assert_eq!(protocol_version, "2024_11_05");
51+
}
52+
53+
#[tokio::test]
54+
async fn tets_protocol_compatibility_unsupported() {
55+
let result = handle_initialize_request("2034_11_05").await;
56+
assert!(result.is_err());
57+
assert!(matches!(result, Err(err) if err.code == INTERNAL_ERROR));
58+
}
59+
}

examples/hello-world-mcp-server-core/src/handler.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ use rust_mcp_schema::{
44
schema_utils::{CallToolError, NotificationFromClient, RequestFromClient, ResultFromServer},
55
ClientRequest, ListToolsResult, RpcError,
66
};
7-
use rust_mcp_sdk::{mcp_server::ServerHandlerCore, McpServer};
7+
use rust_mcp_sdk::{
8+
mcp_server::{enforce_compatible_protocol_version, ServerHandlerCore},
9+
McpServer,
10+
};
811

912
use crate::tools::GreetingTools;
1013

@@ -26,7 +29,20 @@ impl ServerHandlerCore for MyServerHandler {
2629
//Handle client requests according to their specific type.
2730
RequestFromClient::ClientRequest(client_request) => match client_request {
2831
// Handle the initialization request
29-
ClientRequest::InitializeRequest(_) => Ok(runtime.server_info().to_owned().into()),
32+
ClientRequest::InitializeRequest(initialize_request) => {
33+
let mut server_info = runtime.server_info().to_owned();
34+
35+
if let Some(updated_protocol_version) = enforce_compatible_protocol_version(
36+
&initialize_request.params.protocol_version,
37+
&server_info.protocol_version,
38+
)
39+
.map_err(|err| RpcError::internal_error().with_message(err.to_string()))?
40+
{
41+
server_info.protocol_version = initialize_request.params.protocol_version;
42+
}
43+
44+
return Ok(server_info.into());
45+
}
3046

3147
// Handle ListToolsRequest, return list of available tools
3248
ClientRequest::ListToolsRequest(_) => Ok(ListToolsResult {

examples/hello-world-server-core-sse/src/handler.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ use rust_mcp_schema::{
44
schema_utils::{CallToolError, NotificationFromClient, RequestFromClient, ResultFromServer},
55
ClientRequest, ListToolsResult, RpcError,
66
};
7-
use rust_mcp_sdk::{mcp_server::ServerHandlerCore, McpServer};
7+
use rust_mcp_sdk::{
8+
mcp_server::{enforce_compatible_protocol_version, ServerHandlerCore},
9+
McpServer,
10+
};
811

912
use crate::tools::GreetingTools;
1013

@@ -26,8 +29,20 @@ impl ServerHandlerCore for MyServerHandler {
2629
//Handle client requests according to their specific type.
2730
RequestFromClient::ClientRequest(client_request) => match client_request {
2831
// Handle the initialization request
29-
ClientRequest::InitializeRequest(_) => Ok(runtime.server_info().to_owned().into()),
32+
ClientRequest::InitializeRequest(initialize_request) => {
33+
let mut server_info = runtime.server_info().to_owned();
34+
35+
if let Some(updated_protocol_version) = enforce_compatible_protocol_version(
36+
&initialize_request.params.protocol_version,
37+
&server_info.protocol_version,
38+
)
39+
.map_err(|err| RpcError::internal_error().with_message(err.to_string()))?
40+
{
41+
server_info.protocol_version = initialize_request.params.protocol_version;
42+
}
3043

44+
return Ok(server_info.into());
45+
}
3146
// Handle ListToolsRequest, return list of available tools
3247
ClientRequest::ListToolsRequest(_) => Ok(ListToolsResult {
3348
meta: None,

0 commit comments

Comments
 (0)