Skip to content

Commit e1a995c

Browse files
committed
fix: correct elicitation direction to comply with MCP 2025-06-18
- Remove CreateElicitationRequest from ClientRequest - clients cannot initiate elicitation - Move elicit methods from client to server - servers now request user input - Add comprehensive direction tests verifying Server→Client→Server flow - Maintain CreateElicitationResult in ClientResult for proper responses - Update handlers to reflect correct message routing - Add elicitation feature flag for typed schema generation Fixes elicitation direction to match specification where servers request interactive user input from clients, not the reverse.
1 parent ce4a03a commit e1a995c

File tree

7 files changed

+390
-214
lines changed

7 files changed

+390
-214
lines changed

crates/rmcp/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ default = ["base64", "macros", "server"]
7979
client = ["dep:tokio-stream"]
8080
server = ["transport-async-rw", "dep:schemars"]
8181
macros = ["dep:rmcp-macros", "dep:paste"]
82-
elicitation = ["client", "schemars"]
82+
elicitation = ["server", "schemars"]
8383

8484
# reqwest http client
8585
__reqwest = ["dep:reqwest"]

crates/rmcp/src/handler/server.rs

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,6 @@ impl<H: ServerHandler> Service<RoleServer> for H {
6666
.list_tools(request.params, context)
6767
.await
6868
.map(ServerResult::ListToolsResult),
69-
ClientRequest::CreateElicitationRequest(request) => self
70-
.create_elicitation(request.params, context)
71-
.await
72-
.map(ServerResult::CreateElicitationResult),
7369
}
7470
}
7571

@@ -199,31 +195,6 @@ pub trait ServerHandler: Sized + Send + Sync + 'static {
199195
std::future::ready(Ok(ListToolsResult::default()))
200196
}
201197

202-
/// Handle an elicitation request to gather interactive user input.
203-
///
204-
/// This method is typically implemented by server applications that need to
205-
/// request information from users during tool execution. The default implementation
206-
/// declines all elicitation requests.
207-
///
208-
/// # Arguments
209-
/// * `request` - The elicitation request parameters containing the message and schema
210-
/// * `context` - The request context for this elicitation
211-
///
212-
/// # Returns
213-
/// A result containing the user's response (accept/decline/cancel) and optional data
214-
fn create_elicitation(
215-
&self,
216-
request: CreateElicitationRequestParam,
217-
context: RequestContext<RoleServer>,
218-
) -> impl Future<Output = Result<CreateElicitationResult, McpError>> + Send + '_ {
219-
// Default implementation declines all elicitation requests
220-
let _ = (request, context);
221-
std::future::ready(Ok(CreateElicitationResult {
222-
action: ElicitationAction::Decline,
223-
content: None,
224-
}))
225-
}
226-
227198
fn on_cancelled(
228199
&self,
229200
notification: CancelledNotificationParam,

crates/rmcp/src/model.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1374,8 +1374,7 @@ ts_union!(
13741374
| SubscribeRequest
13751375
| UnsubscribeRequest
13761376
| CallToolRequest
1377-
| ListToolsRequest
1378-
| CreateElicitationRequest;
1377+
| ListToolsRequest;
13791378
);
13801379

13811380
ts_union!(

crates/rmcp/src/model/meta.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ variant_extension! {
6666
UnsubscribeRequest
6767
CallToolRequest
6868
ListToolsRequest
69-
CreateElicitationRequest
7069
}
7170
}
7271

crates/rmcp/src/service/client.rs

Lines changed: 0 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use crate::{
88
CallToolRequest, CallToolRequestParam, CallToolResult, CancelledNotification,
99
CancelledNotificationParam, ClientInfo, ClientJsonRpcMessage, ClientNotification,
1010
ClientRequest, ClientResult, CompleteRequest, CompleteRequestParam, CompleteResult,
11-
CreateElicitationRequest, CreateElicitationRequestParam, CreateElicitationResult,
1211
GetPromptRequest, GetPromptRequestParam, GetPromptResult, InitializeRequest,
1312
InitializedNotification, JsonRpcResponse, ListPromptsRequest, ListPromptsResult,
1413
ListResourceTemplatesRequest, ListResourceTemplatesResult, ListResourcesRequest,
@@ -22,28 +21,6 @@ use crate::{
2221
transport::DynamicTransportError,
2322
};
2423

25-
/// Errors that can occur during typed elicitation operations
26-
#[derive(Error, Debug)]
27-
pub enum ElicitationError {
28-
/// The elicitation request failed at the service level
29-
#[error("Service error: {0}")]
30-
Service(#[from] ServiceError),
31-
32-
/// User declined to provide input or cancelled the request
33-
#[error("User declined or cancelled the request")]
34-
UserDeclined,
35-
36-
/// The response data could not be parsed into the requested type
37-
#[error("Failed to parse response data: {error}\nReceived data: {data}")]
38-
ParseError {
39-
error: serde_json::Error,
40-
data: serde_json::Value,
41-
},
42-
43-
/// No response content was provided by the user
44-
#[error("No response content provided")]
45-
NoContent,
46-
}
4724

4825
/// It represents the error that may occur when serving the client.
4926
///
@@ -329,7 +306,6 @@ impl Peer<RoleClient> {
329306
method!(peer_req unsubscribe UnsubscribeRequest(UnsubscribeRequestParam));
330307
method!(peer_req call_tool CallToolRequest(CallToolRequestParam) => CallToolResult);
331308
method!(peer_req list_tools ListToolsRequest(PaginatedRequestParam)? => ListToolsResult);
332-
method!(peer_req create_elicitation CreateElicitationRequest(CreateElicitationRequestParam) => CreateElicitationResult);
333309

334310
method!(peer_not notify_cancelled CancelledNotification(CancelledNotificationParam));
335311
method!(peer_not notify_progress ProgressNotification(ProgressNotificationParam));
@@ -416,158 +392,4 @@ impl Peer<RoleClient> {
416392
Ok(resource_templates)
417393
}
418394

419-
// =============================================================================
420-
// ELICITATION CONVENIENCE METHODS
421-
// =============================================================================
422-
423-
/// Request structured data from the user using a custom JSON schema.
424-
///
425-
/// This is the most flexible elicitation method, allowing you to request
426-
/// any kind of structured input using JSON Schema validation.
427-
///
428-
/// # Arguments
429-
/// * `message` - The prompt message for the user
430-
/// * `schema` - JSON Schema defining the expected data structure
431-
///
432-
/// # Returns
433-
/// * `Ok(Some(data))` if user provided valid data
434-
/// * `Ok(None)` if user declined or cancelled
435-
///
436-
/// # Example
437-
/// ```rust,no_run
438-
/// # use rmcp::*;
439-
/// # use serde_json::json;
440-
/// # async fn example(peer: Peer<RoleClient>) -> Result<(), ServiceError> {
441-
/// let schema = json!({
442-
/// "type": "object",
443-
/// "properties": {
444-
/// "name": {"type": "string"},
445-
/// "email": {"type": "string", "format": "email"},
446-
/// "age": {"type": "integer", "minimum": 0}
447-
/// },
448-
/// "required": ["name", "email"]
449-
/// });
450-
///
451-
/// let user_data = peer.elicit_structured_input(
452-
/// "Please provide your contact information:",
453-
/// schema.as_object().unwrap()
454-
/// ).await?;
455-
///
456-
/// if let Some(data) = user_data {
457-
/// println!("Received user data: {}", data);
458-
/// }
459-
/// # Ok(())
460-
/// # }
461-
/// ```
462-
pub async fn elicit_structured_input(
463-
&self,
464-
message: impl Into<String>,
465-
schema: &crate::model::JsonObject,
466-
) -> Result<Option<serde_json::Value>, ServiceError> {
467-
let response = self
468-
.create_elicitation(CreateElicitationRequestParam {
469-
message: message.into(),
470-
requested_schema: schema.clone(),
471-
})
472-
.await?;
473-
474-
match response.action {
475-
crate::model::ElicitationAction::Accept => Ok(response.content),
476-
_ => Ok(None),
477-
}
478-
}
479-
480-
/// Request typed data from the user with automatic schema generation.
481-
///
482-
/// This method automatically generates the JSON schema from the Rust type using `schemars`,
483-
/// eliminating the need to manually create schemas. The response is automatically parsed
484-
/// into the requested type.
485-
///
486-
/// **Requires the `elicitation` feature to be enabled.**
487-
///
488-
/// # Type Requirements
489-
/// The type `T` must implement:
490-
/// - `schemars::JsonSchema` - for automatic schema generation
491-
/// - `serde::Deserialize` - for parsing the response
492-
///
493-
/// # Arguments
494-
/// * `message` - The prompt message for the user
495-
///
496-
/// # Returns
497-
/// * `Ok(Some(data))` if user provided valid data that matches type T
498-
/// * `Err(ElicitationError::UserDeclined)` if user declined or cancelled the request
499-
/// * `Err(ElicitationError::ParseError { .. })` if response data couldn't be parsed into type T
500-
/// * `Err(ElicitationError::NoContent)` if no response content was provided
501-
/// * `Err(ElicitationError::Service(_))` if the underlying service call failed
502-
///
503-
/// # Example
504-
///
505-
/// Add to your `Cargo.toml`:
506-
/// ```toml
507-
/// [dependencies]
508-
/// rmcp = { version = "0.3", features = ["elicitation"] }
509-
/// serde = { version = "1.0", features = ["derive"] }
510-
/// schemars = "1.0"
511-
/// ```
512-
///
513-
/// ```rust,no_run
514-
/// # use rmcp::*;
515-
/// # use serde::{Deserialize, Serialize};
516-
/// # use schemars::JsonSchema;
517-
/// #
518-
/// #[derive(Debug, Serialize, Deserialize, JsonSchema)]
519-
/// struct UserProfile {
520-
/// #[schemars(description = "Full name")]
521-
/// name: String,
522-
/// #[schemars(description = "Email address")]
523-
/// email: String,
524-
/// #[schemars(description = "Age")]
525-
/// age: u8,
526-
/// }
527-
///
528-
/// # async fn example(peer: Peer<RoleClient>) -> Result<(), Box<dyn std::error::Error>> {
529-
/// match peer.elicit::<UserProfile>("Please enter your profile information").await {
530-
/// Ok(Some(profile)) => {
531-
/// println!("Name: {}, Email: {}, Age: {}", profile.name, profile.email, profile.age);
532-
/// }
533-
/// Err(ElicitationError::UserDeclined) => {
534-
/// println!("User declined to provide information");
535-
/// }
536-
/// Err(ElicitationError::ParseError { error, data }) => {
537-
/// println!("Failed to parse response: {}\nData: {}", error, data);
538-
/// }
539-
/// Err(e) => return Err(e.into()),
540-
/// }
541-
/// # Ok(())
542-
/// # }
543-
/// ```
544-
#[cfg(feature = "schemars")]
545-
pub async fn elicit<T>(&self, message: impl Into<String>) -> Result<Option<T>, ElicitationError>
546-
where
547-
T: schemars::JsonSchema + for<'de> serde::Deserialize<'de>,
548-
{
549-
// Generate schema automatically from type
550-
let schema = crate::handler::server::tool::schema_for_type::<T>();
551-
552-
let response = self
553-
.create_elicitation(CreateElicitationRequestParam {
554-
message: message.into(),
555-
requested_schema: schema,
556-
})
557-
.await?;
558-
559-
match response.action {
560-
crate::model::ElicitationAction::Accept => {
561-
if let Some(value) = response.content {
562-
match serde_json::from_value::<T>(value.clone()) {
563-
Ok(parsed) => Ok(Some(parsed)),
564-
Err(error) => Err(ElicitationError::ParseError { error, data: value }),
565-
}
566-
} else {
567-
Err(ElicitationError::NoContent)
568-
}
569-
}
570-
_ => Err(ElicitationError::UserDeclined),
571-
}
572-
}
573395
}

0 commit comments

Comments
 (0)