Skip to content

Commit 3abbe7c

Browse files
committed
feat: add MCP spec version management and elicitation improvements
- Add versioned feature flags for MCP specifications (2025-03-26, 2025-06-18) - Implement conditional compilation for elicitation features based on spec version - Improve code formatting and consistency across elicitation tests - Add proper feature gating for elicitation API methods - Update Cargo.toml with new MCP spec feature structure
1 parent f939dd1 commit 3abbe7c

File tree

5 files changed

+76
-42
lines changed

5 files changed

+76
-42
lines changed

crates/rmcp/Cargo.toml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,25 @@ rmcp-macros = { version = "0.1", workspace = true, optional = true }
6767
chrono = { version = "0.4.38", features = ["serde"] }
6868

6969
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
70-
chrono = { version = "0.4.38", default-features = false, features = ["serde", "clock", "std", "oldtime"] }
70+
chrono = { version = "0.4.38", default-features = false, features = [
71+
"serde",
72+
"clock",
73+
"std",
74+
"oldtime",
75+
] }
7176

7277
[features]
73-
default = ["base64", "macros", "server"]
78+
default = ["base64", "macros", "server", "mcp_spec"]
7479
client = ["dep:tokio-stream"]
7580
server = ["transport-async-rw", "dep:schemars"]
7681
macros = ["dep:rmcp-macros", "dep:paste"]
7782

83+
mcp_spec = ["mcp_spec-2025-03-26"]
84+
# MCP Spec Version 2025-03-26
85+
mcp_spec-2025-03-26 = []
86+
# MCP Spec Version 2025-06-18 - includes all previous features plus new ones
87+
mcp_spec-2025-06-18 = ["mcp_spec-2025-03-26"]
88+
7889
# reqwest http client
7990
__reqwest = ["dep:reqwest"]
8091

@@ -195,4 +206,4 @@ path = "tests/test_message_schema.rs"
195206
[[test]]
196207
name = "test_progress_subscriber"
197208
required-features = ["server", "client", "macros"]
198-
path = "tests/test_progress_subscriber.rs"
209+
path = "tests/test_progress_subscriber.rs"

crates/rmcp/src/handler/client.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,9 @@ pub trait ClientHandler: Sized + Send + Sync + 'static {
9595
params: CreateElicitationRequestParam,
9696
context: RequestContext<RoleClient>,
9797
) -> impl Future<Output = Result<CreateElicitationResult, McpError>> + Send + '_ {
98-
std::future::ready(Err(
99-
McpError::method_not_found::<CreateElicitationRequestMethod>(),
100-
))
98+
std::future::ready(Err(McpError::method_not_found::<
99+
CreateElicitationRequestMethod,
100+
>()))
101101
}
102102

103103
fn on_cancelled(

crates/rmcp/src/model.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,13 @@ impl std::fmt::Display for ProtocolVersion {
142142
}
143143

144144
impl ProtocolVersion {
145+
#[cfg(feature = "mcp_spec-2025-06-18")]
146+
pub const V_2025_06_18: Self = Self(Cow::Borrowed("2025-06-18"));
145147
pub const V_2025_03_26: Self = Self(Cow::Borrowed("2025-03-26"));
146148
pub const V_2024_11_05: Self = Self(Cow::Borrowed("2024-11-05"));
149+
#[cfg(feature = "mcp_spec-2025-06-18")]
150+
pub const LATEST: Self = Self::V_2025_06_18;
151+
#[cfg(not(feature = "mcp_spec-2025-06-18"))]
147152
pub const LATEST: Self = Self::V_2025_03_26;
148153
}
149154

@@ -1207,7 +1212,8 @@ pub struct CreateElicitationRequestParam {
12071212
}
12081213

12091214
/// Request to create an elicitation (server to client)
1210-
pub type CreateElicitationRequest = Request<CreateElicitationRequestMethod, CreateElicitationRequestParam>;
1215+
pub type CreateElicitationRequest =
1216+
Request<CreateElicitationRequestMethod, CreateElicitationRequestParam>;
12111217

12121218
/// Result of an elicitation request
12131219
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
@@ -1220,7 +1226,6 @@ pub struct CreateElicitationResult {
12201226
#[serde(skip_serializing_if = "Option::is_none")]
12211227
pub content: Option<serde_json::Value>,
12221228
}
1223-
12241229
// =============================================================================
12251230
// TOOL EXECUTION RESULTS
12261231
// =============================================================================

crates/rmcp/src/service/server.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@ use thiserror::Error;
55
use super::*;
66
use crate::model::{
77
CancelledNotification, CancelledNotificationParam, ClientInfo, ClientJsonRpcMessage,
8-
ClientNotification, ClientRequest, ClientResult, CreateElicitationRequest,
9-
CreateElicitationRequestParam, CreateElicitationResult, CreateMessageRequest,
8+
ClientNotification, ClientRequest, ClientResult, CreateMessageRequest,
109
CreateMessageRequestParam, CreateMessageResult, ErrorData, ListRootsRequest, ListRootsResult,
1110
LoggingMessageNotification, LoggingMessageNotificationParam, ProgressNotification,
1211
ProgressNotificationParam, PromptListChangedNotification, ProtocolVersion,
1312
ResourceListChangedNotification, ResourceUpdatedNotification, ResourceUpdatedNotificationParam,
1413
ServerInfo, ServerNotification, ServerRequest, ServerResult, ToolListChangedNotification,
1514
};
15+
#[cfg(feature = "mcp_spec-2025-06-18")]
16+
use crate::model::{
17+
CreateElicitationRequest, CreateElicitationRequestParam, CreateElicitationResult,
18+
};
1619

1720
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1821
pub struct RoleServer;
@@ -318,6 +321,7 @@ macro_rules! method {
318321
impl Peer<RoleServer> {
319322
method!(peer_req create_message CreateMessageRequest(CreateMessageRequestParam) => CreateMessageResult);
320323
method!(peer_req list_roots ListRootsRequest() => ListRootsResult);
324+
#[cfg(feature = "mcp_spec-2025-06-18")]
321325
method!(peer_req create_elicitation CreateElicitationRequest(CreateElicitationRequestParam) => CreateElicitationResult);
322326

323327
method!(peer_not notify_cancelled CancelledNotification(CancelledNotificationParam));

crates/rmcp/tests/test_elicitation.rs

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
// cargo test --features "server client" --package rmcp test_elicitation
2+
#![cfg(feature = "mcp_spec-2025-06-18")]
23
mod common;
34

45
use std::sync::{Arc, Mutex};
56

67
use common::handlers::{TestClientHandler, TestServer};
7-
use rmcp::{
8-
ServiceExt,
9-
model::{CreateElicitationRequestParam, CreateElicitationResult, ElicitationAction},
10-
};
8+
use rmcp::{ServiceExt, model::*};
119
use serde_json::json;
1210
use tokio::sync::Notify;
1311

@@ -30,7 +28,12 @@ async fn test_elicitation_spec_compliance() -> anyhow::Result<()> {
3028
received_requests: Arc<Mutex<Vec<CreateElicitationRequestParam>>>,
3129
) -> Self {
3230
Self {
33-
inner: TestClientHandler::with_notification(false, false, receive_signal.clone(), Arc::new(Mutex::new(Vec::new()))),
31+
inner: TestClientHandler::with_notification(
32+
false,
33+
false,
34+
receive_signal.clone(),
35+
Arc::new(Mutex::new(Vec::new())),
36+
),
3437
receive_signal,
3538
received_requests,
3639
}
@@ -48,10 +51,10 @@ async fn test_elicitation_spec_compliance() -> anyhow::Result<()> {
4851
let mut requests = self.received_requests.lock().unwrap();
4952
requests.push(params.clone());
5053
}
51-
54+
5255
// Signal that we received a request
5356
self.receive_signal.notify_one();
54-
57+
5558
// Simulate user accepting the elicitation with test data
5659
Ok(CreateElicitationResult {
5760
action: ElicitationAction::Accept,
@@ -121,7 +124,7 @@ async fn test_elicitation_spec_compliance() -> anyhow::Result<()> {
121124
// Verify the response
122125
assert_eq!(result.action, ElicitationAction::Accept);
123126
assert!(result.content.is_some());
124-
127+
125128
let content = result.content.unwrap();
126129
assert_eq!(content["email"], "[email protected]");
127130
assert_eq!(content["age"], 25);
@@ -131,24 +134,26 @@ async fn test_elicitation_spec_compliance() -> anyhow::Result<()> {
131134
anyhow::Ok(())
132135
});
133136

134-
let client = ElicitationTestClientHandler::new(
135-
receive_signal.clone(),
136-
received_requests.clone(),
137-
)
138-
.serve(client_transport)
139-
.await?;
137+
let client =
138+
ElicitationTestClientHandler::new(receive_signal.clone(), received_requests.clone())
139+
.serve(client_transport)
140+
.await?;
140141

141142
// Wait for the elicitation request
142143
receive_signal.notified().await;
143-
144+
144145
// Verify the request was received correctly
145146
{
146147
let requests = received_requests.lock().unwrap();
147-
assert_eq!(requests.len(), 1, "Should receive exactly one elicitation request");
148-
148+
assert_eq!(
149+
requests.len(),
150+
1,
151+
"Should receive exactly one elicitation request"
152+
);
153+
149154
let request = &requests[0];
150155
assert_eq!(request.message, "Please provide your contact information");
151-
156+
152157
// Verify the schema structure
153158
let schema = &request.requested_schema;
154159
assert!(schema["type"].as_str().unwrap() == "object");
@@ -165,11 +170,8 @@ async fn test_elicitation_spec_compliance() -> anyhow::Result<()> {
165170
Ok(())
166171
}
167172

168-
#[tokio::test]
173+
#[tokio::test]
169174
async fn test_elicitation_action_types() -> anyhow::Result<()> {
170-
let (server_transport, client_transport) = tokio::io::duplex(4096);
171-
let receive_signal = Arc::new(Notify::new());
172-
173175
// Test different action types
174176
for (action_type, expected_action) in [
175177
("accept", ElicitationAction::Accept),
@@ -178,7 +180,7 @@ async fn test_elicitation_action_types() -> anyhow::Result<()> {
178180
] {
179181
let (server_transport, client_transport) = tokio::io::duplex(4096);
180182
let receive_signal = Arc::new(Notify::new());
181-
183+
182184
struct ActionTestClientHandler {
183185
action: ElicitationAction,
184186
receive_signal: Arc<Notify>,
@@ -191,7 +193,7 @@ async fn test_elicitation_action_types() -> anyhow::Result<()> {
191193
_context: rmcp::service::RequestContext<rmcp::service::RoleClient>,
192194
) -> Result<CreateElicitationResult, rmcp::Error> {
193195
self.receive_signal.notify_one();
194-
196+
195197
Ok(CreateElicitationResult {
196198
action: self.action.clone(),
197199
content: match self.action {
@@ -226,12 +228,12 @@ async fn test_elicitation_action_types() -> anyhow::Result<()> {
226228
.await?;
227229

228230
assert_eq!(result.action, expected_action);
229-
231+
230232
match expected_action {
231233
ElicitationAction::Accept => {
232234
assert!(result.content.is_some());
233235
assert_eq!(result.content.unwrap()["test"], "data");
234-
},
236+
}
235237
_ => {
236238
assert!(result.content.is_none());
237239
}
@@ -262,11 +264,20 @@ fn test_elicitation_serialization() {
262264
for (action, expected) in test_cases {
263265
let serialized = serde_json::to_string(&action).unwrap();
264266
let serialized = serialized.trim_matches('"');
265-
assert_eq!(serialized, expected, "ElicitationAction::{:?} should serialize to \"{}\"", action, expected);
266-
267+
assert_eq!(
268+
serialized, expected,
269+
"ElicitationAction::{:?} should serialize to \"{}\"",
270+
action, expected
271+
);
272+
267273
// Test deserialization
268-
let deserialized: ElicitationAction = serde_json::from_str(&format!("\"{}\"", expected)).unwrap();
269-
assert_eq!(deserialized, action, "\"{}\" should deserialize to ElicitationAction::{:?}", expected, action);
274+
let deserialized: ElicitationAction =
275+
serde_json::from_str(&format!("\"{}\"", expected)).unwrap();
276+
assert_eq!(
277+
deserialized, action,
278+
"\"{}\" should deserialize to ElicitationAction::{:?}",
279+
expected, action
280+
);
270281
}
271282

272283
// Test CreateElicitationRequestParam serialization
@@ -302,5 +313,8 @@ fn test_elicitation_serialization() {
302313

303314
let serialized = serde_json::to_value(&result_no_content).unwrap();
304315
assert_eq!(serialized["action"], "cancel");
305-
assert!(serialized.get("content").is_none(), "content field should be omitted when None");
306-
}
316+
assert!(
317+
serialized.get("content").is_none(),
318+
"content field should be omitted when None"
319+
);
320+
}

0 commit comments

Comments
 (0)