feat: Add MCP Elicitation support (#332)

* feat: implement MCP elicitation support for interactive user input

Adds comprehensive elicitation functionality according to MCP 2025-06-18 specification:

Core Features:
- ElicitationAction enum (Accept, Decline, Cancel)
- CreateElicitationRequestParam and CreateElicitationResult structures
- Protocol version V_2025_06_18 with elicitation methods
- Full JSON-RPC integration with method constants

Capabilities Integration:
- ElicitationCapability with schema validation support
- ClientCapabilities builder pattern integration
- enable_elicitation() and enable_elicitation_schema_validation() methods

Handler Support:
- create_elicitation method in ClientHandler and ServerHandler traits
- Integration with existing request/response union types
- Async/await compatible implementation

Service Layer:
- Basic create_elicitation method via macro expansion
- Four convenience methods for common scenarios:
  * elicit_confirmation() - yes/no questions
  * elicit_text_input() - string input with optional requirements
  * elicit_choice() - selection from multiple options
  * elicit_structured_input() - complex data via JSON Schema

Comprehensive Testing:
- 11 test cases covering all functionality aspects
- JSON serialization/deserialization validation
- MCP specification compliance verification
- Error handling and edge cases
- Performance benchmarks
- Capabilities integration tests

All tests pass and code follows project standards.

* feat: add typed elicitation API with enhanced error handling

- Add new 'elicitation' feature that depends on 'client' and 'schemars'
- Implement elicit<T>() method for type-safe elicitation with automatic schema generation
- Remove convenience methods (elicit_confirmation, elicit_text_input, elicit_choice)
- Add ElicitationError enum with detailed error variants:
  - Service: underlying service errors
  - UserDeclined: user cancelled or declined request
  - ParseError: response parsing failed with context
  - NoContent: no response content provided
- Update documentation with comprehensive examples and error handling
- Add comprehensive tests for typed elicitation and error handling

* 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.

* feat: add elicitation capability checking for server methods

- Add supports_elicitation() method to check client capabilities
- Add CapabilityNotSupported error variant to ElicitationError
- Update elicit_structured_input() to check capabilities before execution
- Update elicit<T>() method to check capabilities before execution
- Add comprehensive tests for capability checking functionality
- Tests verify that servers check client capabilities before sending elicitation requests
- Ensures compliance with MCP 2025-06-18 specification requirement

* fix: json rpc message schema

* fix: doc tests

* fix: cargo nightly fmt checks

* fix: clippy

* refactor: separate elicitation methods into dedicated impl block for RoleServer

- Move (supports_elicitation, elicit_structured_input, elicit) to separate impl block
- Move ElicitationError definition to elicitation methods section
- Keep base methods (create_message, list_roots, notify_*) in main impl block with macro
- Add section comments to distinguish general and elicitation-specific methods

* revert: rollback LATEST protocol version to V_2025_03_26

* fix: remove protocol version assertions
- Remove assertions for V_2025_06_18 protocol version

* fix: fmt checks

* feat: add timeout support for elicitation methods

- Add peer_req_with_timeout macro variants for timeout-enabled methods
- Implement create_elicitation_with_timeout() method
- Implement elicit_with_timeout() for typed elicitation with timeout
- Refactor elicit() to use elicit_with_timeout() internally
- Add 8 comprehensive timeout tests covering validation, error handling, and realistic scenarios
- Fix elicitation feature dependencies in Cargo.toml
- Add proper feature gates for elicitation-specific code

* feat: add timeout validation to prevent DoS attacks

- Add InvalidTimeout error variant for comprehensive validation
- Implement validate_timeout function with security limits (1ms-300s)
- Integrate validation into peer_req_with_timeout macros
- Add comprehensive security tests for timeout validation
- Prevent DoS attacks through unreasonable timeout values

* feat: separate UserDeclined and UserCancelled elicitation errors

According to MCP specification and PR feedback, decline and cancel
actions should be handled differently:

- UserDeclined: explicit user rejection (clicked "Decline", "No", etc.)
- UserCancelled: dismissal without explicit choice (closed dialog, Escape, etc.)

Changes:
- Split ElicitationError::UserDeclined into two distinct error types
- Update error handling logic to map each ElicitationAction correctly
- Improve documentation with proper action semantics
- Add comprehensive tests for new error types and action mapping
- Update examples to demonstrate proper error handling

This provides better error granularity allowing servers to handle
explicit declines vs cancellations appropriately as per MCP spec.

* feat: add compile-time type safety for elicitation methods

Add ElicitationSafe trait and elicit_safe\! macro to ensure elicit<T>()
methods are only used with types that generate appropriate JSON object
schemas, addressing type safety concerns from PR feedback.

Features:
- ElicitationSafe marker trait for compile-time constraints
- elicit_safe\! macro for opt-in type safety declaration
- Updated elicit<T> and elicit_with_timeout<T> to require ElicitationSafe bound
- Comprehensive documentation with examples and rationale
- Full test coverage for new type safety features

This prevents common mistakes like:
- elicit::<String>() - primitives not suitable for object schemas
- elicit::<Vec<i32>>() - arrays don't match client expectations

Breaking change: Existing code must add elicit_safe\!(TypeName) declarations
for types used with elicit methods. This is an intentional safety improvement.

* Revert "feat: add timeout validation to prevent DoS attacks"

This reverts commit 829624212b4fb55ec32566329af7ec195c987ef5.

* fix: correct doctest example in elicit_safe macro documentation

- Remove invalid async/await usage in doctest example
- Comment out the actual usage line to show intent without compilation errors
- Maintain clear documentation of the macro's purpose and usage

* refactor: remove redundant elicitation direction tests

- Remove test_elicitation_not_in_client_request (duplicated functionality)
- Remove redundant ServerRequest match in test_elicitation_direction_server_to_client
- Direction compliance is already verified by the remaining comprehensive test
- Reduces test fragility and maintenance burden

* feat: add elicitation example with user name collection

- Add elicitation server example demonstrating real MCP usage
- Implement greet_user tool with context.peer.elicit::<T>() API
- Show type-safe elicitation with elicit_safe! macro
- Include reset_name tool and MCP Inspector instructions
- Update examples documentation and dependencies

* fix: add Default impl to ElicitationServer for clippy

Resolves clippy::new_without_default warning by implementing
Default trait for ElicitationServer struct.
This commit is contained in:
Andrei G 2025-08-19 05:48:26 +02:00 • committed by GitHub
commit 81ff3159f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 2287 additions and 4 deletions

View file

@ -21,6 +21,10 @@ impl<H: ClientHandler> Service<RoleClient> for H {
.list_roots(context)
.await
.map(ClientResult::ListRootsResult),
ServerRequest::CreateElicitationRequest(request) => self
.create_elicitation(request.params, context)
.await
.map(ClientResult::CreateElicitationResult),
}
}
@ -86,6 +90,35 @@ pub trait ClientHandler: Sized + Send + Sync + 'static {
std::future::ready(Ok(ListRootsResult::default()))
}
/// Handle an elicitation request from a server asking for user input.
///
/// This method is called when a server needs interactive input from the user
/// during tool execution. Implementations should present the message to the user,
/// collect their input according to the requested schema, and return the result.
///
/// # Arguments
/// * `request` - The elicitation request with message and schema
/// * `context` - The request context
///
/// # Returns
/// The user's response including action (accept/decline/cancel) and optional data
///
/// # Default Behavior
/// The default implementation automatically declines all elicitation requests.
/// Real clients should override this to provide user interaction.
fn create_elicitation(
&self,
request: CreateElicitationRequestParam,
context: RequestContext<RoleClient>,
) -> impl Future<Output = Result<CreateElicitationResult, McpError>> + Send + '_ {
// Default implementation declines all requests - real clients should override this
let _ = (request, context);
std::future::ready(Ok(CreateElicitationResult {
action: ElicitationAction::Decline,
content: None,
}))
}
fn on_cancelled(
&self,
params: CancelledNotificationParam,

View file

@ -143,6 +143,7 @@ impl std::fmt::Display for ProtocolVersion {
}
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_03_26;
@ -167,6 +168,7 @@ impl<'de> Deserialize<'de> for ProtocolVersion {
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),
_ => {}
}
Ok(ProtocolVersion(Cow::Owned(s)))
@ -1173,6 +1175,75 @@ pub struct ListRootsResult {
const_string!(RootsListChangedNotificationMethod = "notifications/roots/list_changed");
pub type RootsListChangedNotification = NotificationNoParam<RootsListChangedNotificationMethod>;
// =============================================================================
// ELICITATION (INTERACTIVE USER INPUT)
// =============================================================================
// Method constants for elicitation operations.
// Elicitation allows servers to request interactive input from users during tool execution.
const_string!(ElicitationCreateRequestMethod = "elicitation/create");
const_string!(ElicitationResponseNotificationMethod = "notifications/elicitation/response");
/// Represents the possible actions a user can take in response to an elicitation request.
///
/// When a server requests user input through elicitation, the user can:
/// - Accept: Provide the requested information and continue
/// - Decline: Refuse to provide the information but continue the operation
/// - Cancel: Stop the entire operation
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum ElicitationAction {
/// User accepts the request and provides the requested information
Accept,
/// User declines to provide the information but allows the operation to continue
Decline,
/// User cancels the entire operation
Cancel,
}
/// Parameters for creating an elicitation request to gather user input.
///
/// This structure contains everything needed to request interactive input from a user:
/// - A human-readable message explaining what information is needed
/// - A JSON schema defining the expected structure of the response
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CreateElicitationRequestParam {
/// Human-readable message explaining what input is needed from the user.
/// This should be clear and provide sufficient context for the user to understand
/// what information they need to provide.
pub message: String,
/// JSON Schema defining the expected structure and validation rules for the user's response.
/// This allows clients to validate input and provide appropriate UI controls.
/// Must be a valid JSON Schema Draft 2020-12 object.
pub requested_schema: JsonObject,
}
/// The result returned by a client in response to an elicitation request.
///
/// Contains the user's decision (accept/decline/cancel) and optionally their input data
/// if they chose to accept the request.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CreateElicitationResult {
/// The user's decision on how to handle the elicitation request
pub action: ElicitationAction,
/// The actual data provided by the user, if they accepted the request.
/// Must conform to the JSON schema specified in the original request.
/// Only present when action is Accept.
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<Value>,
}
/// Request type for creating an elicitation to gather user input
pub type CreateElicitationRequest =
Request<ElicitationCreateRequestMethod, CreateElicitationRequestParam>;
// =============================================================================
// TOOL EXECUTION RESULTS
// =============================================================================
@ -1430,7 +1501,7 @@ ts_union!(
);
ts_union!(
export type ClientResult = CreateMessageResult | ListRootsResult | EmptyResult;
export type ClientResult = CreateMessageResult | ListRootsResult | CreateElicitationResult | EmptyResult;
);
impl ClientResult {
@ -1445,7 +1516,8 @@ ts_union!(
export type ServerRequest =
| PingRequest
| CreateMessageRequest
| ListRootsRequest;
| ListRootsRequest
| CreateElicitationRequest;
);
ts_union!(
@ -1470,6 +1542,7 @@ ts_union!(
| ReadResourceResult
| CallToolResult
| ListToolsResult
| CreateElicitationResult
| EmptyResult
;
);

View file

@ -40,6 +40,22 @@ pub struct RootsCapabilities {
pub list_changed: Option<bool>,
}
/// Capability for handling elicitation requests from servers.
///
/// Elicitation allows servers to request interactive input from users during tool execution.
/// This capability indicates that a client can handle elicitation requests and present
/// appropriate UI to users for collecting the requested information.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ElicitationCapability {
/// Whether the client supports JSON Schema validation for elicitation responses.
/// When true, the client will validate user input against the requested_schema
/// before sending the response back to the server.
#[serde(skip_serializing_if = "Option::is_none")]
pub schema_validation: Option<bool>,
}
///
/// # Builder
/// ```rust
@ -59,6 +75,9 @@ pub struct ClientCapabilities {
pub roots: Option<RootsCapabilities>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sampling: Option<JsonObject>,
/// Capability to handle elicitation requests from servers for interactive user input
#[serde(skip_serializing_if = "Option::is_none")]
pub elicitation: Option<ElicitationCapability>,
}
///
@ -253,6 +272,7 @@ builder! {
experimental: ExperimentalCapabilities,
roots: RootsCapabilities,
sampling: JsonObject,
elicitation: ElicitationCapability,
}
}
@ -267,6 +287,21 @@ impl<const E: bool, const S: bool>
}
}
#[cfg(feature = "elicitation")]
impl<const E: bool, const R: bool, const S: bool>
ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, R, S, true>>
{
/// Enable JSON Schema validation for elicitation responses.
/// When enabled, the client will validate user input against the requested_schema
/// before sending responses back to the server.
pub fn enable_elicitation_schema_validation(mut self) -> Self {
if let Some(c) = self.elicitation.as_mut() {
c.schema_validation = Some(true);
}
self
}
}
#[cfg(test)]
mod test {
use super::*;

View file

@ -74,6 +74,7 @@ variant_extension! {
PingRequest
CreateMessageRequest
ListRootsRequest
CreateElicitationRequest
}
}

View file

@ -3,6 +3,10 @@ use std::borrow::Cow;
use thiserror::Error;
use super::*;
#[cfg(feature = "elicitation")]
use crate::model::{
CreateElicitationRequest, CreateElicitationRequestParam, CreateElicitationResult,
};
use crate::{
model::{
CancelledNotification, CancelledNotificationParam, ClientInfo, ClientJsonRpcMessage,
@ -324,11 +328,68 @@ macro_rules! method {
Ok(())
}
};
// Timeout-only variants (base method should be created separately with peer_req)
(peer_req_with_timeout $method_with_timeout:ident $Req:ident() => $Resp: ident) => {
pub async fn $method_with_timeout(
&self,
timeout: Option<std::time::Duration>,
) -> Result<$Resp, ServiceError> {
let request = ServerRequest::$Req($Req {
method: Default::default(),
extensions: Default::default(),
});
let options = crate::service::PeerRequestOptions {
timeout,
meta: None,
};
let result = self
.send_request_with_option(request, options)
.await?
.await_response()
.await?;
match result {
ClientResult::$Resp(result) => Ok(result),
_ => Err(ServiceError::UnexpectedResponse),
}
}
};
(peer_req_with_timeout $method_with_timeout:ident $Req:ident($Param: ident) => $Resp: ident) => {
pub async fn $method_with_timeout(
&self,
params: $Param,
timeout: Option<std::time::Duration>,
) -> Result<$Resp, ServiceError> {
let request = ServerRequest::$Req($Req {
method: Default::default(),
params,
extensions: Default::default(),
});
let options = crate::service::PeerRequestOptions {
timeout,
meta: None,
};
let result = self
.send_request_with_option(request, options)
.await?
.await_response()
.await?;
match result {
ClientResult::$Resp(result) => Ok(result),
_ => Err(ServiceError::UnexpectedResponse),
}
}
};
}
impl Peer<RoleServer> {
method!(peer_req create_message CreateMessageRequest(CreateMessageRequestParam) => CreateMessageResult);
method!(peer_req list_roots ListRootsRequest() => ListRootsResult);
#[cfg(feature = "elicitation")]
method!(peer_req create_elicitation CreateElicitationRequest(CreateElicitationRequestParam) => CreateElicitationResult);
#[cfg(feature = "elicitation")]
method!(peer_req_with_timeout create_elicitation_with_timeout CreateElicitationRequest(CreateElicitationRequestParam) => CreateElicitationResult);
method!(peer_not notify_cancelled CancelledNotification(CancelledNotificationParam));
method!(peer_not notify_progress ProgressNotification(ProgressNotificationParam));
@ -338,3 +399,294 @@ impl Peer<RoleServer> {
method!(peer_not notify_tool_list_changed ToolListChangedNotification);
method!(peer_not notify_prompt_list_changed PromptListChangedNotification);
}
// =============================================================================
// ELICITATION CONVENIENCE METHODS
// These methods are specific to server role and provide typed elicitation functionality
// =============================================================================
/// Errors that can occur during typed elicitation operations
#[cfg(feature = "elicitation")]
#[derive(Error, Debug)]
pub enum ElicitationError {
/// The elicitation request failed at the service level
#[error("Service error: {0}")]
Service(#[from] ServiceError),
/// User explicitly declined to provide the requested information
/// This indicates a conscious decision by the user to reject the request
/// (e.g., clicked "Reject", "Decline", "No", etc.)
#[error("User explicitly declined the request")]
UserDeclined,
/// User dismissed the request without making an explicit choice
/// This indicates the user cancelled without explicitly declining
/// (e.g., closed dialog, clicked outside, pressed Escape, etc.)
#[error("User cancelled/dismissed the request")]
UserCancelled,
/// The response data could not be parsed into the requested type
#[error("Failed to parse response data: {error}\nReceived data: {data}")]
ParseError {
error: serde_json::Error,
data: serde_json::Value,
},
/// No response content was provided by the user
#[error("No response content provided")]
NoContent,
/// Client does not support elicitation capability
#[error("Client does not support elicitation - capability not declared during initialization")]
CapabilityNotSupported,
}
/// Marker trait to ensure that elicitation types generate object-type JSON schemas.
///
/// This trait provides compile-time safety to ensure that types used with
/// `elicit<T>()` methods will generate JSON schemas of type "object", which
/// aligns with MCP client expectations for structured data input.
///
/// # Type Safety Rationale
///
/// MCP clients typically expect JSON objects for elicitation schemas to
/// provide structured forms and validation. This trait prevents common
/// mistakes like:
///
/// ```compile_fail
/// // These would not compile due to missing ElicitationSafe bound:
/// let name: String = server.elicit("Enter name").await?; // Primitive
/// let items: Vec<i32> = server.elicit("Enter items").await?; // Array
/// ```
#[cfg(feature = "elicitation")]
pub trait ElicitationSafe: schemars::JsonSchema {}
/// Macro to mark types as safe for elicitation by verifying they generate object schemas.
///
/// This macro automatically implements the `ElicitationSafe` trait for struct types
/// that should be used with `elicit<T>()` methods.
///
/// # Example
///
/// ```rust
/// use rmcp::elicit_safe;
/// use schemars::JsonSchema;
/// use serde::{Deserialize, Serialize};
///
/// #[derive(Serialize, Deserialize, JsonSchema)]
/// struct UserProfile {
/// name: String,
/// email: String,
/// }
///
/// elicit_safe!(UserProfile);
///
/// // Now safe to use in async context:
/// // let profile: UserProfile = server.elicit("Enter profile").await?;
/// ```
#[cfg(feature = "elicitation")]
#[macro_export]
macro_rules! elicit_safe {
($($t:ty),* $(,)?) => {
$(
impl $crate::service::ElicitationSafe for $t {}
)*
};
}
#[cfg(feature = "elicitation")]
impl Peer<RoleServer> {
/// Check if the client supports elicitation capability
///
/// Returns true if the client declared elicitation capability during initialization,
/// false otherwise. According to MCP 2025-06-18 specification, clients that support
/// elicitation MUST declare the capability during initialization.
pub fn supports_elicitation(&self) -> bool {
if let Some(client_info) = self.peer_info() {
client_info.capabilities.elicitation.is_some()
} else {
false
}
}
/// Request typed data from the user with automatic schema generation.
///
/// This method automatically generates the JSON schema from the Rust type using `schemars`,
/// eliminating the need to manually create schemas. The response is automatically parsed
/// into the requested type.
///
/// **Requires the `elicitation` feature to be enabled.**
///
/// # Type Requirements
/// The type `T` must implement:
/// - `schemars::JsonSchema` - for automatic schema generation
/// - `serde::Deserialize` - for parsing the response
///
/// # Arguments
/// * `message` - The prompt message for the user
///
/// # Returns
/// * `Ok(Some(data))` if user provided valid data that matches type T
/// * `Err(ElicitationError::UserDeclined)` if user explicitly declined the request
/// * `Err(ElicitationError::UserCancelled)` if user cancelled/dismissed the request
/// * `Err(ElicitationError::ParseError { .. })` if response data couldn't be parsed into type T
/// * `Err(ElicitationError::NoContent)` if no response content was provided
/// * `Err(ElicitationError::Service(_))` if the underlying service call failed
///
/// # Example
///
/// Add to your `Cargo.toml`:
/// ```toml
/// [dependencies]
/// rmcp = { version = "0.3", features = ["elicitation"] }
/// serde = { version = "1.0", features = ["derive"] }
/// schemars = "1.0"
/// ```
///
/// ```rust,no_run
/// # use rmcp::*;
/// # use rmcp::service::ElicitationError;
/// # use serde::{Deserialize, Serialize};
/// # use schemars::JsonSchema;
/// #
/// #[derive(Debug, Serialize, Deserialize, JsonSchema)]
/// struct UserProfile {
/// #[schemars(description = "Full name")]
/// name: String,
/// #[schemars(description = "Email address")]
/// email: String,
/// #[schemars(description = "Age")]
/// age: u8,
/// }
///
/// // Mark as safe for elicitation (generates object schema)
/// rmcp::elicit_safe!(UserProfile);
///
/// # async fn example(peer: Peer<RoleServer>) -> Result<(), Box<dyn std::error::Error>> {
/// match peer.elicit::<UserProfile>("Please enter your profile information").await {
/// Ok(Some(profile)) => {
/// println!("Name: {}, Email: {}, Age: {}", profile.name, profile.email, profile.age);
/// }
/// Ok(None) => {
/// println!("User provided no content");
/// }
/// Err(ElicitationError::UserDeclined) => {
/// println!("User explicitly declined to provide information");
/// // Handle explicit decline - perhaps offer alternatives
/// }
/// Err(ElicitationError::UserCancelled) => {
/// println!("User cancelled the request");
/// // Handle cancellation - perhaps prompt again later
/// }
/// Err(ElicitationError::ParseError { error, data }) => {
/// println!("Failed to parse response: {}\nData: {}", error, data);
/// }
/// Err(e) => return Err(e.into()),
/// }
/// # Ok(())
/// # }
/// ```
#[cfg(all(feature = "schemars", feature = "elicitation"))]
pub async fn elicit<T>(&self, message: impl Into<String>) -> Result<Option<T>, ElicitationError>
where
T: ElicitationSafe + for<'de> serde::Deserialize<'de>,
{
self.elicit_with_timeout(message, None).await
}
/// Request typed data from the user with custom timeout.
///
/// Same as `elicit()` but allows specifying a custom timeout for the request.
/// If the user doesn't respond within the timeout, the request will be cancelled.
///
/// # Arguments
/// * `message` - The prompt message for the user
/// * `timeout` - Optional timeout duration. If None, uses default timeout behavior
///
/// # Returns
/// Same as `elicit()` but may also return `ServiceError::Timeout` if timeout expires
///
/// # Example
/// ```rust,no_run
/// # use rmcp::*;
/// # use rmcp::service::ElicitationError;
/// # use serde::{Deserialize, Serialize};
/// # use schemars::JsonSchema;
/// # use std::time::Duration;
/// #
/// #[derive(Debug, Serialize, Deserialize, JsonSchema)]
/// struct QuickResponse {
/// answer: String,
/// }
///
/// // Mark as safe for elicitation
/// rmcp::elicit_safe!(QuickResponse);
///
/// # async fn example(peer: Peer<RoleServer>) -> Result<(), Box<dyn std::error::Error>> {
/// // Give user 30 seconds to respond
/// let timeout = Some(Duration::from_secs(30));
/// match peer.elicit_with_timeout::<QuickResponse>(
/// "Quick question - what's your answer?",
/// timeout
/// ).await {
/// Ok(Some(response)) => println!("Got answer: {}", response.answer),
/// Ok(None) => println!("User provided no content"),
/// Err(ElicitationError::UserDeclined) => {
/// println!("User explicitly declined");
/// // Handle explicit decline
/// }
/// Err(ElicitationError::UserCancelled) => {
/// println!("User cancelled/dismissed");
/// // Handle cancellation
/// }
/// Err(ElicitationError::Service(ServiceError::Timeout { .. })) => {
/// println!("User didn't respond in time");
/// }
/// Err(e) => return Err(e.into()),
/// }
/// # Ok(())
/// # }
/// ```
#[cfg(all(feature = "schemars", feature = "elicitation"))]
pub async fn elicit_with_timeout<T>(
&self,
message: impl Into<String>,
timeout: Option<std::time::Duration>,
) -> Result<Option<T>, ElicitationError>
where
T: ElicitationSafe + for<'de> serde::Deserialize<'de>,
{
// Check if client supports elicitation capability
if !self.supports_elicitation() {
return Err(ElicitationError::CapabilityNotSupported);
}
// Generate schema automatically from type
let schema = crate::handler::server::tool::schema_for_type::<T>();
let response = self
.create_elicitation_with_timeout(
CreateElicitationRequestParam {
message: message.into(),
requested_schema: schema,
},
timeout,
)
.await?;
match response.action {
crate::model::ElicitationAction::Accept => {
if let Some(value) = response.content {
match serde_json::from_value::<T>(value.clone()) {
Ok(parsed) => Ok(Some(parsed)),
Err(error) => Err(ElicitationError::ParseError { error, data: value }),
}
} else {
Err(ElicitationError::NoContent)
}
}
crate::model::ElicitationAction::Decline => Err(ElicitationError::UserDeclined),
crate::model::ElicitationAction::Cancel => Err(ElicitationError::UserCancelled),
}
}
}