diff --git a/crates/rmcp/Cargo.toml b/crates/rmcp/Cargo.toml index 6f07f7d3..cdcdddbf 100644 --- a/crates/rmcp/Cargo.toml +++ b/crates/rmcp/Cargo.toml @@ -79,7 +79,7 @@ default = ["base64", "macros", "server"] client = ["dep:tokio-stream"] server = ["transport-async-rw", "dep:schemars", "dep:pastey"] macros = ["dep:rmcp-macros", "dep:pastey"] -elicitation = [] +elicitation = ["dep:url"] # reqwest http client __reqwest = ["dep:reqwest"] diff --git a/crates/rmcp/src/handler/client.rs b/crates/rmcp/src/handler/client.rs index 86539b87..eeb79309 100644 --- a/crates/rmcp/src/handler/client.rs +++ b/crates/rmcp/src/handler/client.rs @@ -62,6 +62,10 @@ impl Service for H { ServerNotification::PromptListChangedNotification(_notification_no_param) => { self.on_prompt_list_changed(context).await } + ServerNotification::ElicitationCompletionNotification(notification) => { + self.on_url_elicitation_notification_complete(notification.params, context) + .await + } ServerNotification::CustomNotification(notification) => { self.on_custom_notification(notification, context).await } @@ -116,6 +120,44 @@ pub trait ClientHandler: Sized + Send + Sync + 'static { /// # Default Behavior /// The default implementation automatically declines all elicitation requests. /// Real clients should override this to provide user interaction. + /// + /// # Example + /// ```rust,ignore + /// use rmcp::model::CreateElicitationRequestParam; + /// use rmcp::{ + /// model::ErrorData as McpError, + /// model::*, + /// service::{NotificationContext, RequestContext, RoleClient, Service, ServiceRole}, + /// }; + /// use rmcp::ClientHandler; + /// + /// impl ClientHandler for MyClient { + /// async fn create_elicitation( + /// &self, + /// request: CreateElicitationRequestParam, + /// context: RequestContext, + /// ) -> Result { + /// match request { + /// CreateElicitationRequestParam::FormElicitationParam {meta, message, requested_schema,} => { + /// // Display message to user and collect input according to requested_schema + /// let user_input = get_user_input(message, requested_schema).await?; + /// Ok(CreateElicitationResult { + /// action: ElicitationAction::Accept, + /// content: Some(user_input), + /// }) + /// } + /// CreateElicitationRequestParam::UrlElicitationParam {meta, message, url, elicitation_id,} => { + /// // Open URL in browser for user to complete elicitation + /// open_url_in_browser(url).await?; + /// Ok(CreateElicitationResult { + /// action: ElicitationAction::Accept, + /// content: None, + /// }) + /// } + /// } + /// } + /// } + /// ``` fn create_elicitation( &self, request: CreateElicitationRequestParams, @@ -189,6 +231,14 @@ pub trait ClientHandler: Sized + Send + Sync + 'static { ) -> impl Future + Send + '_ { std::future::ready(()) } + + fn on_url_elicitation_notification_complete( + &self, + params: ElicitationResponseNotificationParam, + context: NotificationContext, + ) -> impl Future + Send + '_ { + std::future::ready(()) + } fn on_custom_notification( &self, notification: CustomNotification, diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index db6e927e..dc70e181 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -453,6 +453,7 @@ impl ErrorCode { pub const INVALID_PARAMS: Self = Self(-32602); pub const INTERNAL_ERROR: Self = Self(-32603); pub const PARSE_ERROR: Self = Self(-32700); + pub const URL_ELICITATION_REQUIRED: Self = Self(-32042); } /// Error information for JSON-RPC error responses. @@ -504,6 +505,12 @@ impl ErrorData { pub fn internal_error(message: impl Into>, data: Option) -> Self { Self::new(ErrorCode::INTERNAL_ERROR, message, data) } + pub fn url_elicitation_required( + message: impl Into>, + data: Option, + ) -> Self { + Self::new(ErrorCode::URL_ELICITATION_REQUIRED, message, data) + } } /// Represents any JSON-RPC message that can be sent or received. @@ -1891,6 +1898,7 @@ pub type RootsListChangedNotification = NotificationNoParam, + message: String, + requested_schema: ElicitationSchema, + }, + #[serde(rename = "url", rename_all = "camelCase")] + UrlElicitationParam { + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + meta: Option, + message: String, + url: String, + elicitation_id: String, + }, + #[serde(untagged, rename_all = "camelCase")] + FormElicitationParamBackwardsCompat { + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + meta: Option, + message: String, + requested_schema: ElicitationSchema, + }, +} + +impl TryFrom for CreateElicitationRequestParams { + type Error = serde_json::Error; + + fn try_from( + value: CreateElicitationRequestParamDeserializeHelper, + ) -> Result { + match value { + CreateElicitationRequestParamDeserializeHelper::FormElicitationParam { + meta, + message, + requested_schema, + } + | CreateElicitationRequestParamDeserializeHelper::FormElicitationParamBackwardsCompat { + meta, + message, + requested_schema, + } => Ok(CreateElicitationRequestParams::FormElicitationParams { + meta, + message, + requested_schema, + }), + CreateElicitationRequestParamDeserializeHelper::UrlElicitationParam { + meta, + message, + url, + elicitation_id, + } => Ok(CreateElicitationRequestParams::UrlElicitationParams { + meta, + message, + url, + elicitation_id, + }), + } + } +} + /// Parameters for creating an elicitation request to gather user input. /// /// This structure contains everything needed to request interactive input from a user: @@ -1917,12 +1991,12 @@ pub enum ElicitationAction { /// - A type-safe schema defining the expected structure of the response /// /// # Example -/// +/// 1. Form-based elicitation request /// ```rust /// use rmcp::model::*; /// -/// let params = CreateElicitationRequestParams { -/// meta: None, +/// let params = CreateElicitationRequestParams::FormElicitationParams { +/// meta: None, /// message: "Please provide your email".to_string(), /// requested_schema: ElicitationSchema::builder() /// .required_email("email") @@ -1930,31 +2004,68 @@ pub enum ElicitationAction { /// .unwrap(), /// }; /// ``` +/// 2. URL-based elicitation request +/// ```rust +/// use rmcp::model::*; +/// let params = CreateElicitationRequestParams::UrlElicitationParams { +/// meta: None, +/// message: "Please provide your feedback at the following URL".to_string(), +/// url: "https://example.com/feedback".to_string(), +/// elicitation_id: "unique-id-123".to_string(), +/// }; +/// ``` #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct CreateElicitationRequestParams { - /// Protocol-level metadata for this request (SEP-1319) - #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] - pub meta: Option, - - /// 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, - - /// Type-safe schema defining the expected structure and validation rules for the user's response. - /// This enforces the MCP 2025-06-18 specification that elicitation schemas must be objects - /// with primitive-typed properties. - pub requested_schema: ElicitationSchema, +#[serde( + tag = "mode", + try_from = "CreateElicitationRequestParamDeserializeHelper" +)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub enum CreateElicitationRequestParams { + #[serde(rename = "form", rename_all = "camelCase")] + FormElicitationParams { + /// Protocol-level metadata for this request (SEP-1319) + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + meta: Option, + /// 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. + message: String, + + /// Type-safe schema defining the expected structure and validation rules for the user's response. + /// This enforces the MCP 2025-06-18 specification that elicitation schemas must be objects + /// with primitive-typed properties. + requested_schema: ElicitationSchema, + }, + #[serde(rename = "url", rename_all = "camelCase")] + UrlElicitationParams { + /// Protocol-level metadata for this request (SEP-1319) + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + meta: Option, + /// 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. + message: String, + + /// The URL where the user can provide the requested information. + /// The client should direct the user to this URL to complete the elicitation. + url: String, + /// The unique identifier for this elicitation request. + elicitation_id: String, + }, } impl RequestParamsMeta for CreateElicitationRequestParams { fn meta(&self) -> Option<&Meta> { - self.meta.as_ref() + match self { + CreateElicitationRequestParams::FormElicitationParams { meta, .. } => meta.as_ref(), + CreateElicitationRequestParams::UrlElicitationParams { meta, .. } => meta.as_ref(), + } } fn meta_mut(&mut self) -> &mut Option { - &mut self.meta + match self { + CreateElicitationRequestParams::FormElicitationParams { meta, .. } => meta, + CreateElicitationRequestParams::UrlElicitationParams { meta, .. } => meta, + } } } @@ -1984,6 +2095,18 @@ pub struct CreateElicitationResult { pub type CreateElicitationRequest = Request; +/// Notification parameters for an url elicitation completion notification. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct ElicitationResponseNotificationParam { + pub elicitation_id: String, +} + +/// Notification sent when an url elicitation process is completed. +pub type ElicitationCompletionNotification = + Notification; + // ============================================================================= // TOOL EXECUTION RESULTS // ============================================================================= @@ -2488,6 +2611,7 @@ ts_union!( | ResourceListChangedNotification | ToolListChangedNotification | PromptListChangedNotification + | ElicitationCompletionNotification | CustomNotification; ); @@ -2993,4 +3117,142 @@ mod tests { assert_eq!(json["serverInfo"]["icons"][0]["sizes"][0], "48x48"); assert_eq!(json["serverInfo"]["websiteUrl"], "https://docs.example.com"); } + + #[test] + fn test_elicitation_deserialization_untagged() { + // Test deserialization without the "type" field (should default to FormElicitationParam) + let json_data_without_tag = json!({ + "message": "Please provide more details.", + "requestedSchema": { + "title": "User Details", + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "integer" } + }, + "required": ["name", "age"] + } + }); + let elicitation: CreateElicitationRequestParams = + serde_json::from_value(json_data_without_tag).expect("Deserialization failed"); + if let CreateElicitationRequestParams::FormElicitationParams { + meta, + message, + requested_schema, + } = elicitation + { + assert_eq!(meta, None); + assert_eq!(message, "Please provide more details."); + assert_eq!(requested_schema.title, Some(Cow::from("User Details"))); + assert_eq!(requested_schema.type_, ObjectTypeConst); + } else { + panic!("Expected FormElicitationParam"); + } + } + + #[test] + fn test_elicitation_deserialization() { + let json_data_form = json!({ + "_meta": { "meta_form_key_1": "meta form value 1" }, + "mode": "form", + "message": "Please provide more details.", + "requestedSchema": { + "title": "User Details", + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "integer" } + }, + "required": ["name", "age"] + } + }); + let elicitation_form: CreateElicitationRequestParams = + serde_json::from_value(json_data_form).expect("Deserialization failed"); + if let CreateElicitationRequestParams::FormElicitationParams { + meta, + message, + requested_schema, + } = elicitation_form + { + assert_eq!( + meta, + Some(Meta(object!({ "meta_form_key_1": "meta form value 1" }))) + ); + assert_eq!(message, "Please provide more details."); + assert_eq!(requested_schema.title, Some(Cow::from("User Details"))); + assert_eq!(requested_schema.type_, ObjectTypeConst); + } else { + panic!("Expected FormElicitationParam"); + } + + let json_data_url = json!({ + "_meta": { "meta_url_key_1": "meta url value 1" }, + "mode": "url", + "message": "Please fill out the form at the following URL.", + "url": "https://example.com/form", + "elicitationId": "elicitation-123" + }); + let elicitation_url: CreateElicitationRequestParams = + serde_json::from_value(json_data_url).expect("Deserialization failed"); + if let CreateElicitationRequestParams::UrlElicitationParams { + meta, + message, + url, + elicitation_id, + } = elicitation_url + { + assert_eq!( + meta, + Some(Meta(object!({ "meta_url_key_1": "meta url value 1" }))) + ); + assert_eq!(message, "Please fill out the form at the following URL."); + assert_eq!(url, "https://example.com/form"); + assert_eq!(elicitation_id, "elicitation-123"); + } else { + panic!("Expected UrlElicitationParam"); + } + } + + #[test] + fn test_elicitation_serialization() { + let form_elicitation = CreateElicitationRequestParams::FormElicitationParams { + meta: Some(Meta(object!({ "meta_form_key_1": "meta form value 1" }))), + message: "Please provide more details.".to_string(), + requested_schema: ElicitationSchema::builder() + .title("User Details") + .string_property("name", |s| s) + .build() + .expect("Valid schema"), + }; + let json_form = serde_json::to_value(&form_elicitation).expect("Serialization failed"); + let expected_form_json = json!({ + "_meta": { "meta_form_key_1": "meta form value 1" }, + "mode": "form", + "message": "Please provide more details.", + "requestedSchema": { + "title":"User Details", + "type":"object", + "properties":{ + "name": { "type": "string" }, + }, + } + }); + assert_eq!(json_form, expected_form_json); + + let url_elicitation = CreateElicitationRequestParams::UrlElicitationParams { + meta: Some(Meta(object!({ "meta_url_key_1": "meta url value 1" }))), + message: "Please fill out the form at the following URL.".to_string(), + url: "https://example.com/form".to_string(), + elicitation_id: "elicitation-123".to_string(), + }; + let json_url = serde_json::to_value(&url_elicitation).expect("Serialization failed"); + let expected_url_json = json!({ + "_meta": { "meta_url_key_1": "meta url value 1" }, + "mode": "url", + "message": "Please fill out the form at the following URL.", + "url": "https://example.com/form", + "elicitationId": "elicitation-123" + }); + assert_eq!(json_url, expected_url_json); + } } diff --git a/crates/rmcp/src/model/capabilities.rs b/crates/rmcp/src/model/capabilities.rs index 6f4e0648..d0f8e1b2 100644 --- a/crates/rmcp/src/model/capabilities.rs +++ b/crates/rmcp/src/model/capabilities.rs @@ -179,14 +179,15 @@ impl TasksCapability { } /// 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. +/// +/// Capability for form mode elicitation. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -pub struct ElicitationCapability { +pub struct FormElicitationCapability { /// 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. @@ -194,6 +195,26 @@ pub struct ElicitationCapability { pub schema_validation: Option, } +/// Capability for URL mode elicitation. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct UrlElicitationCapability {} + +/// 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 client supports form-based elicitation. + #[serde(skip_serializing_if = "Option::is_none")] + pub form: Option, + /// Whether client supports URL-based elicitation. + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, +} + /// Sampling capability with optional sub-capabilities (SEP-1577). #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] #[serde(rename_all = "camelCase")] @@ -504,12 +525,14 @@ impl ClientCapabilitiesBuilder> { - /// Enable JSON Schema validation for elicitation responses. + /// Enable JSON Schema validation for elicitation responses in form mode. /// 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); + c.form = Some(FormElicitationCapability { + schema_validation: Some(true), + }); } self } diff --git a/crates/rmcp/src/model/meta.rs b/crates/rmcp/src/model/meta.rs index c979318d..c60762a3 100644 --- a/crates/rmcp/src/model/meta.rs +++ b/crates/rmcp/src/model/meta.rs @@ -188,6 +188,7 @@ variant_extension! { ResourceListChangedNotification ToolListChangedNotification PromptListChangedNotification + ElicitationCompletionNotification CustomNotification } } diff --git a/crates/rmcp/src/service/server.rs b/crates/rmcp/src/service/server.rs index 1ba578b7..351976f2 100644 --- a/crates/rmcp/src/service/server.rs +++ b/crates/rmcp/src/service/server.rs @@ -1,11 +1,16 @@ use std::borrow::Cow; +#[cfg(feature = "elicitation")] +use std::collections::HashSet; use thiserror::Error; +#[cfg(feature = "elicitation")] +use url::Url; use super::*; #[cfg(feature = "elicitation")] use crate::model::{ CreateElicitationRequest, CreateElicitationRequestParams, CreateElicitationResult, + ElicitationAction, ElicitationCompletionNotification, ElicitationResponseNotificationParam, }; use crate::{ model::{ @@ -405,6 +410,8 @@ impl Peer { method!(peer_req create_elicitation CreateElicitationRequest(CreateElicitationRequestParams) => CreateElicitationResult); #[cfg(feature = "elicitation")] method!(peer_req_with_timeout create_elicitation_with_timeout CreateElicitationRequest(CreateElicitationRequestParams) => CreateElicitationResult); + #[cfg(feature = "elicitation")] + method!(peer_not notify_url_elicitation_completed ElicitationCompletionNotification(ElicitationResponseNotificationParam)); method!(peer_not notify_cancelled CancelledNotification(CancelledNotificationParam)); method!(peer_not notify_progress ProgressNotification(ProgressNotificationParam)); @@ -509,6 +516,12 @@ macro_rules! elicit_safe { }; } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ElicitationMode { + Form, + Url, +} + #[cfg(feature = "elicitation")] impl Peer { /// Check if the client supports elicitation capability @@ -516,11 +529,27 @@ impl Peer { /// 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 { + pub fn supported_elicitation_modes(&self) -> HashSet { if let Some(client_info) = self.peer_info() { - client_info.capabilities.elicitation.is_some() + if let Some(elicit_capability) = &client_info.capabilities.elicitation { + let mut modes = HashSet::new(); + // Backward compatibility: if neither form nor url is specified, assume form + if elicit_capability.form.is_none() && elicit_capability.url.is_none() { + modes.insert(ElicitationMode::Form); + } else { + if elicit_capability.form.is_some() { + modes.insert(ElicitationMode::Form); + } + if elicit_capability.url.is_some() { + modes.insert(ElicitationMode::Url); + } + } + modes + } else { + HashSet::new() + } } else { - false + HashSet::new() } } @@ -671,8 +700,11 @@ impl Peer { where T: ElicitationSafe + for<'de> serde::Deserialize<'de>, { - // Check if client supports elicitation capability - if !self.supports_elicitation() { + // Check if client supports form elicitation capability + if !self + .supported_elicitation_modes() + .contains(&ElicitationMode::Form) + { return Err(ElicitationError::CapabilityNotSupported); } @@ -690,7 +722,7 @@ impl Peer { let response = self .create_elicitation_with_timeout( - CreateElicitationRequestParams { + CreateElicitationRequestParams::FormElicitationParams { meta: None, message: message.into(), requested_schema: schema, @@ -714,4 +746,123 @@ impl Peer { crate::model::ElicitationAction::Cancel => Err(ElicitationError::UserCancelled), } } + + /// Request the user to visit a URL and confirm completion. + /// + /// This method sends a URL elicitation request to the client, prompting the user + /// to visit the specified URL and confirm completion. It returns the user's action + /// (accept/decline/cancel) without any additional data. + /// **Requires the `elicitation` feature to be enabled.** + /// + /// # Arguments + /// * `message` - The prompt message for the user + /// * `url` - The URL the user is requested to visit + /// * `elicitation_id` - A unique identifier for this elicitation request + /// # Returns + /// * `Ok(action)` indicating the user's response action + /// * `Err(ElicitationError::CapabilityNotSupported)` if client does not support elicitation via URL + /// * `Err(ElicitationError::Service(_))` if the underlying service call failed + /// # Example + /// ```rust,no_run + /// # use rmcp::*; + /// # use rmcp::model::ElicitationAction; + /// # use url::Url; + /// + /// async fn example(peer: Peer) -> Result<(), Box> { + /// let elicit_result = peer.elicit_url("Please visit the following URL to complete the action", + /// Url::parse("https://example.com/complete_action")?, "elicit_123").await?; + /// match elicit_result { + /// ElicitationAction::Accept => { + /// println!("User accepted and confirmed completion"); + /// } + /// ElicitationAction::Decline => { + /// println!("User declined the request"); + /// } + /// ElicitationAction::Cancel => { + /// println!("User cancelled/dismissed the request"); + /// } + /// } + /// Ok(()) + /// } + /// ``` + #[cfg(feature = "elicitation")] + pub async fn elicit_url( + &self, + message: impl Into, + url: impl Into, + elicitation_id: impl Into, + ) -> Result { + self.elicit_url_with_timeout(message, url, elicitation_id, None) + .await + } + + /// Request the user to visit a URL and confirm completion. + /// + /// Same as `elicit_url()` but allows specifying a custom timeout for the request. + /// + /// # Arguments + /// * `message` - The prompt message for the user + /// * `url` - The URL the user is requested to visit + /// * `elicitation_id` - A unique identifier for this elicitation request + /// * `timeout` - Optional timeout duration. If None, uses default timeout behavior + /// # Returns + /// * `Ok(action)` indicating the user's response action + /// * `Err(ElicitationError::CapabilityNotSupported)` if client does not support elicitation via URL + /// * `Err(ElicitationError::Service(_))` if the underlying service call failed + /// # Example + /// ```rust,no_run + /// # use std::time::Duration; + /// use rmcp::*; + /// # use rmcp::model::ElicitationAction; + /// # use url::Url; + /// + /// async fn example(peer: Peer) -> Result<(), Box> { + /// let elicit_result = peer.elicit_url_with_timeout("Please visit the following URL to complete the action", + /// Url::parse("https://example.com/complete_action")?, + /// "elicit_123", + /// Some(Duration::from_secs(30))).await?; + /// match elicit_result { + /// ElicitationAction::Accept => { + /// println!("User accepted and confirmed completion"); + /// } + /// ElicitationAction::Decline => { + /// println!("User declined the request"); + /// } + /// ElicitationAction::Cancel => { + /// println!("User cancelled/dismissed the request"); + /// } + /// } + /// Ok(()) + /// } + /// ``` + #[cfg(feature = "elicitation")] + pub async fn elicit_url_with_timeout( + &self, + message: impl Into, + url: impl Into, + elicitation_id: impl Into, + timeout: Option, + ) -> Result { + // Check if client supports url elicitation + if !self + .supported_elicitation_modes() + .contains(&ElicitationMode::Url) + { + return Err(ElicitationError::CapabilityNotSupported); + } + + let action = self + .create_elicitation_with_timeout( + CreateElicitationRequestParams::UrlElicitationParams { + meta: None, + message: message.into(), + url: url.into().to_string(), + elicitation_id: elicitation_id.into(), + }, + timeout, + ) + .await? + .action; + Ok(action) + } } diff --git a/crates/rmcp/tests/test_elicitation.rs b/crates/rmcp/tests/test_elicitation.rs index 65f21e23..3cc3c0d2 100644 --- a/crates/rmcp/tests/test_elicitation.rs +++ b/crates/rmcp/tests/test_elicitation.rs @@ -44,7 +44,7 @@ async fn test_elicitation_request_param_serialization() { .build() .unwrap(); - let request_param = CreateElicitationRequestParams { + let request_param = CreateElicitationRequestParams::FormElicitationParams { meta: None, message: "Please provide your email address".to_string(), requested_schema: schema, @@ -53,6 +53,7 @@ async fn test_elicitation_request_param_serialization() { // Test serialization let json = serde_json::to_value(&request_param).unwrap(); let expected = json!({ + "mode": "form", "message": "Please provide your email address", "requestedSchema": { "type": "object", @@ -70,11 +71,24 @@ async fn test_elicitation_request_param_serialization() { // Test deserialization let deserialized: CreateElicitationRequestParams = serde_json::from_value(expected).unwrap(); - assert_eq!(deserialized.message, request_param.message); - assert_eq!( - deserialized.requested_schema, - request_param.requested_schema - ); + match (&deserialized, &request_param) { + ( + CreateElicitationRequestParams::FormElicitationParams { + meta: None, + message: msg1, + requested_schema: schema1, + }, + CreateElicitationRequestParams::FormElicitationParams { + meta: None, + message: msg2, + requested_schema: schema2, + }, + ) => { + assert_eq!(msg1, msg2); + assert_eq!(schema1, schema2); + } + _ => panic!("Expected FormElicitationParam variant"), + } } /// Test CreateElicitationResult structure with different action types @@ -129,7 +143,7 @@ async fn test_elicitation_json_rpc_protocol() { id: RequestId::Number(1), request: CreateElicitationRequest { method: ElicitationCreateRequestMethod, - params: CreateElicitationRequestParams { + params: CreateElicitationRequestParams::FormElicitationParams { meta: None, message: "Do you want to continue?".to_string(), requested_schema: schema, @@ -149,10 +163,12 @@ async fn test_elicitation_json_rpc_protocol() { let deserialized: JsonRpcRequest = serde_json::from_value(json).unwrap(); assert_eq!(deserialized.id, RequestId::Number(1)); - assert_eq!( - deserialized.request.params.message, - "Do you want to continue?" - ); + match &deserialized.request.params { + CreateElicitationRequestParams::FormElicitationParams { message, .. } => { + assert_eq!(message, "Do you want to continue?"); + } + _ => panic!("Expected FormElicitationParam variant"), + } } /// Test elicitation action types and their expected behavior @@ -214,7 +230,7 @@ async fn test_elicitation_spec_compliance() { #[tokio::test] async fn test_elicitation_error_handling() { // Test minimal schema handling (empty properties is technically valid) - let minimal_schema_request = CreateElicitationRequestParams { + let minimal_schema_request = CreateElicitationRequestParams::FormElicitationParams { meta: None, message: "Test message".to_string(), requested_schema: ElicitationSchema::builder().build().unwrap(), @@ -224,7 +240,7 @@ async fn test_elicitation_error_handling() { let _json = serde_json::to_value(&minimal_schema_request).unwrap(); // Test empty message - let empty_message_request = CreateElicitationRequestParams { + let empty_message_request = CreateElicitationRequestParams::FormElicitationParams { meta: None, message: "".to_string(), requested_schema: ElicitationSchema::builder() @@ -250,7 +266,7 @@ async fn test_elicitation_performance() { .build() .unwrap(); - let request = CreateElicitationRequestParams { + let request = CreateElicitationRequestParams::FormElicitationParams { meta: None, message: "Performance test message".to_string(), requested_schema: schema, @@ -286,19 +302,25 @@ async fn test_elicitation_capabilities() { // Test basic elicitation capability let mut elicitation_cap = ElicitationCapability::default(); - assert_eq!(elicitation_cap.schema_validation, None); + assert_eq!(elicitation_cap.form, None); + assert_eq!(elicitation_cap.url, None); // Test with schema validation enabled - elicitation_cap.schema_validation = Some(true); + elicitation_cap.form = Some(FormElicitationCapability { + schema_validation: Some(true), + }); // Test serialization let json = serde_json::to_value(&elicitation_cap).unwrap(); - let expected = json!({"schemaValidation": true}); + let expected = json!({"form":{"schemaValidation": true}}); assert_eq!(json, expected); // Test deserialization let deserialized: ElicitationCapability = serde_json::from_value(expected).unwrap(); - assert_eq!(deserialized.schema_validation, Some(true)); + assert_eq!( + deserialized.form.as_ref().unwrap().schema_validation, + Some(true) + ); // Test ClientCapabilities builder with elicitation let client_caps = ClientCapabilities::builder() @@ -308,14 +330,21 @@ async fn test_elicitation_capabilities() { assert!(client_caps.elicitation.is_some()); assert_eq!( - client_caps.elicitation.as_ref().unwrap().schema_validation, + client_caps + .elicitation + .as_ref() + .unwrap() + .form + .as_ref() + .unwrap() + .schema_validation, Some(true) ); // Test full client capabilities serialization let json = serde_json::to_value(&client_caps).unwrap(); assert!( - json["elicitation"]["schemaValidation"] + json["elicitation"]["form"]["schemaValidation"] .as_bool() .unwrap_or(false) ); @@ -374,8 +403,8 @@ async fn test_elicitation_convenience_methods() { .contains("Option A") ); - // Test that CreateElicitationRequestParams can be created with type-safe schemas - let confirmation_request = CreateElicitationRequestParams { + // Test that CreateElicitationRequestParam can be created with type-safe schemas + let confirmation_request = CreateElicitationRequestParams::FormElicitationParams { meta: None, message: "Test confirmation".to_string(), requested_schema: ElicitationSchema::builder() @@ -418,7 +447,7 @@ async fn test_elicitation_structured_schemas() { .build() .unwrap(); - let request = CreateElicitationRequestParams { + let request = CreateElicitationRequestParams::FormElicitationParams { meta: None, message: "Please provide your user information".to_string(), requested_schema: schema, @@ -428,42 +457,31 @@ async fn test_elicitation_structured_schemas() { let json = serde_json::to_value(&request).unwrap(); let deserialized: CreateElicitationRequestParams = serde_json::from_value(json).unwrap(); - assert_eq!(deserialized.message, "Please provide your user information"); - assert_eq!(deserialized.requested_schema.properties.len(), 5); - assert!( - deserialized - .requested_schema - .properties - .contains_key("name") - ); - assert!( - deserialized - .requested_schema - .properties - .contains_key("email") - ); - assert!(deserialized.requested_schema.properties.contains_key("age")); - assert!( - deserialized - .requested_schema - .properties - .contains_key("newsletter") - ); - assert!( - deserialized - .requested_schema - .properties - .contains_key("country") - ); - assert_eq!( - deserialized.requested_schema.required, - Some(vec![ - "name".to_string(), - "email".to_string(), - "age".to_string(), - "country".to_string() - ]) - ); + match deserialized { + CreateElicitationRequestParams::FormElicitationParams { + message, + requested_schema, + .. + } => { + assert_eq!(message, "Please provide your user information"); + assert_eq!(requested_schema.properties.len(), 5); + assert!(requested_schema.properties.contains_key("name")); + assert!(requested_schema.properties.contains_key("email")); + assert!(requested_schema.properties.contains_key("age")); + assert!(requested_schema.properties.contains_key("newsletter")); + assert!(requested_schema.properties.contains_key("country")); + assert_eq!( + requested_schema.required, + Some(vec![ + "name".to_string(), + "email".to_string(), + "age".to_string(), + "country".to_string() + ]) + ); + } + _ => panic!("Expected FormElicitationParam variant"), + } } // Typed elicitation tests using the API with schemars @@ -661,7 +679,7 @@ async fn test_elicitation_multi_select_enum() { .build() .unwrap(); - let request = CreateElicitationRequestParams { + let request = CreateElicitationRequestParams::FormElicitationParams { meta: None, message: "Please provide your user information".to_string(), requested_schema: schema, @@ -671,58 +689,56 @@ async fn test_elicitation_multi_select_enum() { let json = serde_json::to_value(&request).unwrap(); let deserialized: CreateElicitationRequestParams = serde_json::from_value(json).unwrap(); - assert_eq!(deserialized.message, "Please provide your user information"); - assert_eq!(deserialized.requested_schema.properties.len(), 1); - assert!( - deserialized - .requested_schema - .properties - .contains_key("choices") - ); - assert_eq!( - deserialized.requested_schema.required, - Some(vec!["choices".to_string()]) - ); - - assert!(matches!( - deserialized - .requested_schema - .properties - .get("choices") - .unwrap(), - PrimitiveSchema::Enum(EnumSchema::Multi(_)) - )); - - if let Some(PrimitiveSchema::Enum(schema)) = - deserialized.requested_schema.properties.get("choices") - { - assert_eq!( - schema, - &EnumSchema::Multi(MultiSelectEnumSchema::Titled(TitledMultiSelectEnumSchema { - type_: ArrayTypeConst, - title: None, - description: None, - min_items: Some(1), - max_items: Some(2), - items: TitledItems { - any_of: vec![ - ConstTitle { - const_: "A".to_string(), - title: "A name".to_string() - }, - ConstTitle { - const_: "B".to_string(), - title: "B name".to_string() - }, - ConstTitle { - const_: "C".to_string(), - title: "C name".to_string() + match deserialized { + CreateElicitationRequestParams::FormElicitationParams { + message, + requested_schema, + .. + } => { + assert_eq!(message, "Please provide your user information"); + assert_eq!(requested_schema.properties.len(), 1); + assert!(requested_schema.properties.contains_key("choices")); + assert_eq!(requested_schema.required, Some(vec!["choices".to_string()])); + + assert!(matches!( + requested_schema.properties.get("choices").unwrap(), + PrimitiveSchema::Enum(EnumSchema::Multi(_)) + )); + + if let Some(PrimitiveSchema::Enum(schema)) = requested_schema.properties.get("choices") + { + assert_eq!( + schema, + &EnumSchema::Multi(MultiSelectEnumSchema::Titled( + TitledMultiSelectEnumSchema { + type_: ArrayTypeConst, + title: None, + description: None, + min_items: Some(1), + max_items: Some(2), + items: TitledItems { + any_of: vec![ + ConstTitle { + const_: "A".to_string(), + title: "A name".to_string() + }, + ConstTitle { + const_: "B".to_string(), + title: "B name".to_string() + }, + ConstTitle { + const_: "C".to_string(), + title: "C name".to_string() + } + ], + }, + default: None } - ], - }, - default: None - })) - ) + )) + ) + } + } + _ => panic!("Expected FormElicitationParam variant"), } } @@ -743,7 +759,7 @@ async fn test_elicitation_single_select_enum() { .build() .unwrap(); - let request = CreateElicitationRequestParams { + let request = CreateElicitationRequestParams::FormElicitationParams { meta: None, message: "Please provide your user information".to_string(), requested_schema: schema, @@ -752,55 +768,52 @@ async fn test_elicitation_single_select_enum() { // Test that complex schemas serialize/deserialize correctly let json = serde_json::to_value(&request).unwrap(); let deserialized: CreateElicitationRequestParams = serde_json::from_value(json).unwrap(); - assert_eq!(deserialized.message, "Please provide your user information"); - assert_eq!(deserialized.requested_schema.properties.len(), 1); - assert!( - deserialized - .requested_schema - .properties - .contains_key("choices") - ); - assert_eq!( - deserialized.requested_schema.required, - Some(vec!["choices".to_string()]) - ); - assert!(matches!( - deserialized - .requested_schema - .properties - .get("choices") - .unwrap(), - PrimitiveSchema::Enum(EnumSchema::Single(_)) - )); - - if let Some(PrimitiveSchema::Enum(schema)) = - deserialized.requested_schema.properties.get("choices") - { - assert_eq!( - schema, - &EnumSchema::Single(SingleSelectEnumSchema::Titled( - TitledSingleSelectEnumSchema { - type_: StringTypeConst, - title: None, - description: None, - one_of: vec![ - ConstTitle { - const_: "A".to_string(), - title: "A name".to_string() - }, - ConstTitle { - const_: "B".to_string(), - title: "B name".to_string() - }, - ConstTitle { - const_: "C".to_string(), - title: "C name".to_string() + + match deserialized { + CreateElicitationRequestParams::FormElicitationParams { + message, + requested_schema, + .. + } => { + assert_eq!(message, "Please provide your user information"); + assert_eq!(requested_schema.properties.len(), 1); + assert!(requested_schema.properties.contains_key("choices")); + assert_eq!(requested_schema.required, Some(vec!["choices".to_string()])); + assert!(matches!( + requested_schema.properties.get("choices").unwrap(), + PrimitiveSchema::Enum(EnumSchema::Single(_)) + )); + + if let Some(PrimitiveSchema::Enum(schema)) = requested_schema.properties.get("choices") + { + assert_eq!( + schema, + &EnumSchema::Single(SingleSelectEnumSchema::Titled( + TitledSingleSelectEnumSchema { + type_: StringTypeConst, + title: None, + description: None, + one_of: vec![ + ConstTitle { + const_: "A".to_string(), + title: "A name".to_string() + }, + ConstTitle { + const_: "B".to_string(), + title: "B name".to_string() + }, + ConstTitle { + const_: "C".to_string(), + title: "C name".to_string() + } + ], + default: None } - ], - default: None - } - )) - ) + )) + ) + } + } + _ => panic!("Expected FormElicitationParam variant"), } } @@ -825,7 +838,7 @@ async fn test_elicitation_direction_server_to_client() { .build() .unwrap(); - let elicitation_request = CreateElicitationRequestParams { + let elicitation_request = CreateElicitationRequestParams::FormElicitationParams { meta: None, message: "Please enter your name".to_string(), requested_schema: schema, @@ -878,7 +891,7 @@ async fn test_elicitation_json_rpc_direction() { let server_request = ServerJsonRpcMessage::request( ServerRequest::CreateElicitationRequest(CreateElicitationRequest { method: ElicitationCreateRequestMethod, - params: CreateElicitationRequestParams { + params: CreateElicitationRequestParams::FormElicitationParams { meta: None, message: "Do you want to continue?".to_string(), requested_schema: schema, @@ -984,32 +997,54 @@ async fn test_elicitation_result_in_client_result() { async fn test_elicitation_capability_structure() { // Test default ElicitationCapability let default_cap = ElicitationCapability::default(); - assert!(default_cap.schema_validation.is_none()); + assert!(default_cap.form.is_none()); + assert!(default_cap.url.is_none()); // Test ElicitationCapability with schema validation enabled let cap_with_validation = ElicitationCapability { - schema_validation: Some(true), + form: Some(FormElicitationCapability { + schema_validation: Some(true), + }), + url: None, }; - assert_eq!(cap_with_validation.schema_validation, Some(true)); + assert_eq!( + cap_with_validation.form.as_ref().unwrap().schema_validation, + Some(true) + ); // Test ElicitationCapability with schema validation disabled let cap_without_validation = ElicitationCapability { - schema_validation: Some(false), + form: Some(FormElicitationCapability { + schema_validation: Some(false), + }), + url: None, }; - assert_eq!(cap_without_validation.schema_validation, Some(false)); + assert_eq!( + cap_without_validation + .form + .as_ref() + .unwrap() + .schema_validation, + Some(false) + ); // Test JSON serialization let json = serde_json::to_value(&cap_with_validation).unwrap(); assert_eq!( json, serde_json::json!({ - "schemaValidation": true + "form": { + "schemaValidation": true + } }) ); // Test JSON deserialization let deserialized: ElicitationCapability = serde_json::from_value(json).unwrap(); - assert_eq!(deserialized.schema_validation, Some(true)); + assert_eq!( + deserialized.form.as_ref().unwrap().schema_validation, + Some(true) + ); } /// Test ClientCapabilities with elicitation capability @@ -1018,7 +1053,10 @@ async fn test_client_capabilities_with_elicitation() { // Test ClientCapabilities with elicitation capability let capabilities = ClientCapabilities { elicitation: Some(ElicitationCapability { - schema_validation: Some(true), + form: Some(FormElicitationCapability { + schema_validation: Some(true), + }), + url: None, }), ..Default::default() }; @@ -1026,14 +1064,21 @@ async fn test_client_capabilities_with_elicitation() { // Verify elicitation capability is present assert!(capabilities.elicitation.is_some()); assert_eq!( - capabilities.elicitation.as_ref().unwrap().schema_validation, + capabilities + .elicitation + .as_ref() + .unwrap() + .form + .as_ref() + .unwrap() + .schema_validation, Some(true) ); // Test JSON serialization let json = serde_json::to_value(&capabilities).unwrap(); assert!( - json["elicitation"]["schemaValidation"] + json["elicitation"]["form"]["schemaValidation"] .as_bool() .unwrap_or(false) ); @@ -1050,13 +1095,16 @@ async fn test_client_capabilities_with_elicitation() { /// Test InitializeRequestParam with elicitation capability #[tokio::test] async fn test_initialize_request_with_elicitation() { - // Test InitializeRequestParams with elicitation capability + // Test InitializeRequestParam with elicitation capability let init_param = InitializeRequestParams { meta: None, protocol_version: ProtocolVersion::LATEST, capabilities: ClientCapabilities { elicitation: Some(ElicitationCapability { - schema_validation: Some(true), + form: Some(FormElicitationCapability { + schema_validation: Some(true), + }), + url: None, }), ..Default::default() }, @@ -1077,6 +1125,9 @@ async fn test_initialize_request_with_elicitation() { .elicitation .as_ref() .unwrap() + .form + .as_ref() + .unwrap() .schema_validation, Some(true) ); @@ -1084,7 +1135,7 @@ async fn test_initialize_request_with_elicitation() { // Test JSON serialization let json = serde_json::to_value(&init_param).unwrap(); assert!( - json["capabilities"]["elicitation"]["schemaValidation"] + json["capabilities"]["elicitation"]["form"]["schemaValidation"] .as_bool() .unwrap_or(false) ); @@ -1101,7 +1152,10 @@ async fn test_capability_checking_logic() { protocol_version: ProtocolVersion::LATEST, capabilities: ClientCapabilities { elicitation: Some(ElicitationCapability { - schema_validation: Some(true), + form: Some(FormElicitationCapability { + schema_validation: Some(true), + }), + url: None, }), ..Default::default() }, @@ -1230,37 +1284,47 @@ async fn test_elicitation_capability_serialization() { // Test capability with schema validation enabled let cap_with_validation = ElicitationCapability { - schema_validation: Some(true), + form: Some(FormElicitationCapability { + schema_validation: Some(true), + }), + url: None, }; let json = serde_json::to_value(&cap_with_validation).unwrap(); assert_eq!( json, serde_json::json!({ - "schemaValidation": true + "form": { + "schemaValidation": true + } }) ); // Test capability with schema validation disabled let cap_without_validation = ElicitationCapability { - schema_validation: Some(false), + form: Some(FormElicitationCapability { + schema_validation: Some(false), + }), + url: None, }; let json = serde_json::to_value(&cap_without_validation).unwrap(); assert_eq!( json, serde_json::json!({ - "schemaValidation": false + "form": { + "schemaValidation": false + } }) ); // Test deserialization let deserialized: ElicitationCapability = serde_json::from_value(serde_json::json!({ - "schemaValidation": true + "form":{"schemaValidation": true} })) .unwrap(); - assert_eq!(deserialized.schema_validation, Some(true)); + assert_eq!(deserialized.form.unwrap().schema_validation, Some(true)); } /// Test ClientCapabilities builder with elicitation capability methods @@ -1272,7 +1336,7 @@ async fn test_client_capabilities_elicitation_builder() { let caps = ClientCapabilities::builder().enable_elicitation().build(); assert!(caps.elicitation.is_some()); - assert_eq!(caps.elicitation.as_ref().unwrap().schema_validation, None); + assert_eq!(caps.elicitation.as_ref().unwrap().form, None); // Test enabling elicitation with schema validation let caps_with_validation = ClientCapabilities::builder() @@ -1286,13 +1350,19 @@ async fn test_client_capabilities_elicitation_builder() { .elicitation .as_ref() .unwrap() + .form + .as_ref() + .unwrap() .schema_validation, Some(true) ); // Test enabling elicitation with custom capability let custom_elicitation = ElicitationCapability { - schema_validation: Some(false), + form: Some(FormElicitationCapability { + schema_validation: Some(false), + }), + url: None, }; let caps_custom = ClientCapabilities::builder() @@ -1322,7 +1392,7 @@ async fn test_create_elicitation_with_timeout_basic() { .build() .unwrap(); - let _params = CreateElicitationRequestParams { + let _params = CreateElicitationRequestParams::FormElicitationParams { meta: None, message: "Enter your details".to_string(), requested_schema: schema, @@ -1784,3 +1854,421 @@ async fn test_required_typed_property_methods() { assert!(required.contains(&"age".to_string())); assert!(required.contains(&"active".to_string())); } + +// ============================================================================= +// URL ELICITATION TESTS +// ============================================================================= + +/// Test URL elicitation request parameter serialization/deserialization +#[tokio::test] +async fn test_url_elicitation_request_param_serialization() { + let request_param = CreateElicitationRequestParams::UrlElicitationParams { + meta: None, + message: "Please visit the following URL to complete verification".to_string(), + url: "https://example.com/verify".to_string(), + elicitation_id: "elicit-123".to_string(), + }; + + // Test serialization + let json = serde_json::to_value(&request_param).unwrap(); + let expected = json!({ + "mode": "url", + "message": "Please visit the following URL to complete verification", + "url": "https://example.com/verify", + "elicitationId": "elicit-123" + }); + + assert_eq!(json, expected); + + // Test deserialization + let deserialized: CreateElicitationRequestParams = serde_json::from_value(expected).unwrap(); + match deserialized { + CreateElicitationRequestParams::UrlElicitationParams { + message, + url, + elicitation_id, + .. + } => { + assert_eq!( + message, + "Please visit the following URL to complete verification" + ); + assert_eq!(url, "https://example.com/verify"); + assert_eq!(elicitation_id, "elicit-123"); + } + _ => panic!("Expected UrlElicitationParam variant"), + } +} + +/// Test URL elicitation request in JSON-RPC protocol +#[tokio::test] +async fn test_url_elicitation_json_rpc_protocol() { + // Create a complete JSON-RPC request for URL elicitation + let request = JsonRpcRequest { + jsonrpc: JsonRpcVersion2_0, + id: RequestId::Number(1), + request: CreateElicitationRequest { + method: ElicitationCreateRequestMethod, + params: CreateElicitationRequestParams::UrlElicitationParams { + meta: None, + message: "Please authorize this action at the following URL".to_string(), + url: "https://auth.example.com/authorize/abc123".to_string(), + elicitation_id: "auth-request-456".to_string(), + }, + extensions: Default::default(), + }, + }; + + // Test serialization of complete request + let json = serde_json::to_value(&request).unwrap(); + assert_eq!(json["jsonrpc"], "2.0"); + assert_eq!(json["id"], 1); + assert_eq!(json["method"], "elicitation/create"); + assert_eq!(json["params"]["mode"], "url"); + assert_eq!( + json["params"]["message"], + "Please authorize this action at the following URL" + ); + assert_eq!( + json["params"]["url"], + "https://auth.example.com/authorize/abc123" + ); + assert_eq!(json["params"]["elicitationId"], "auth-request-456"); + + // Test deserialization + let deserialized: JsonRpcRequest = + serde_json::from_value(json).unwrap(); + assert_eq!(deserialized.id, RequestId::Number(1)); + match &deserialized.request.params { + CreateElicitationRequestParams::UrlElicitationParams { + message, + url, + elicitation_id, + .. + } => { + assert_eq!(message, "Please authorize this action at the following URL"); + assert_eq!(url, "https://auth.example.com/authorize/abc123"); + assert_eq!(elicitation_id, "auth-request-456"); + } + _ => panic!("Expected UrlElicitationParam variant"), + } +} + +/// Test ElicitationCompletionNotification serialization/deserialization +#[tokio::test] +async fn test_elicitation_completion_notification() { + let notification_params = ElicitationResponseNotificationParam { + elicitation_id: "elicit-789".to_string(), + }; + + // Test serialization + let json = serde_json::to_value(¬ification_params).unwrap(); + let expected = json!({ + "elicitationId": "elicit-789" + }); + assert_eq!(json, expected); + + // Test deserialization + let deserialized: ElicitationResponseNotificationParam = + serde_json::from_value(expected).unwrap(); + assert_eq!(deserialized.elicitation_id, "elicit-789"); + + // Test complete notification structure + let notification = ElicitationCompletionNotification { + method: ElicitationCompletionNotificationMethod, + params: notification_params, + extensions: Default::default(), + }; + + let json = serde_json::to_value(¬ification).unwrap(); + assert_eq!(json["method"], "notifications/elicitation/complete"); + assert_eq!(json["params"]["elicitationId"], "elicit-789"); +} + +/// Test UrlElicitationCapability structure and serialization +#[tokio::test] +async fn test_url_elicitation_capability() { + // Test default UrlElicitationCapability + let url_cap = UrlElicitationCapability::default(); + + // Test serialization - should be empty object + let json = serde_json::to_value(&url_cap).unwrap(); + assert_eq!(json, json!({})); + + // Test deserialization + let deserialized: UrlElicitationCapability = serde_json::from_value(json!({})).unwrap(); + assert_eq!(deserialized, url_cap); + + // Test ElicitationCapability with URL mode enabled + let elicitation_cap = ElicitationCapability { + form: None, + url: Some(UrlElicitationCapability::default()), + }; + + let json = serde_json::to_value(&elicitation_cap).unwrap(); + assert_eq!( + json, + json!({ + "url": {} + }) + ); + + // Test ElicitationCapability with both form and URL modes + let both_cap = ElicitationCapability { + form: Some(FormElicitationCapability { + schema_validation: Some(true), + }), + url: Some(UrlElicitationCapability::default()), + }; + + let json = serde_json::to_value(&both_cap).unwrap(); + assert_eq!( + json, + json!({ + "form": { + "schemaValidation": true + }, + "url": {} + }) + ); +} + +/// Test backward compatibility: CreateElicitationRequestParam without mode tag +#[tokio::test] +async fn test_elicitation_backward_compatibility_no_mode() { + // JSON without "mode" field should deserialize as FormElicitationParam + let json_without_mode = json!({ + "message": "Please enter your details", + "requestedSchema": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"] + } + }); + + let deserialized: CreateElicitationRequestParams = + serde_json::from_value(json_without_mode).unwrap(); + + match deserialized { + CreateElicitationRequestParams::FormElicitationParams { + message, + requested_schema, + .. + } => { + assert_eq!(message, "Please enter your details"); + assert_eq!(requested_schema.properties.len(), 1); + assert!(requested_schema.properties.contains_key("name")); + } + _ => panic!("Expected FormElicitationParam for backward compatibility"), + } +} + +/// Test both form and URL elicitation modes in the same test +#[tokio::test] +async fn test_elicitation_both_modes() { + // Form mode + let form_schema = ElicitationSchema::builder() + .required_property("email", PrimitiveSchema::String(StringSchema::email())) + .build() + .unwrap(); + + let form_request = CreateElicitationRequestParams::FormElicitationParams { + meta: None, + message: "Enter email".to_string(), + requested_schema: form_schema, + }; + + let form_json = serde_json::to_value(&form_request).unwrap(); + assert_eq!(form_json["mode"], "form"); + assert!(form_json.get("requestedSchema").is_some()); + assert!(form_json.get("url").is_none()); + + // URL mode + let url_request = CreateElicitationRequestParams::UrlElicitationParams { + meta: None, + message: "Visit URL".to_string(), + url: "https://example.com".to_string(), + elicitation_id: "id-123".to_string(), + }; + + let url_json = serde_json::to_value(&url_request).unwrap(); + assert_eq!(url_json["mode"], "url"); + assert!(url_json.get("url").is_some()); + assert!(url_json.get("elicitationId").is_some()); + assert!(url_json.get("requestedSchema").is_none()); +} + +/// Test URL_ELICITATION_REQUIRED error code +#[tokio::test] +async fn test_url_elicitation_required_error_code() { + // Test the error code constant + assert_eq!(ErrorCode::URL_ELICITATION_REQUIRED.0, -32042); + + // Test creating error data with URL_ELICITATION_REQUIRED + let error_data = ErrorData::url_elicitation_required( + "URL elicitation is required for this operation", + Some(json!({ + "url": "https://example.com/complete", + "elicitationId": "elicit-999" + })), + ); + + assert_eq!(error_data.code, ErrorCode::URL_ELICITATION_REQUIRED); + assert_eq!( + error_data.message, + "URL elicitation is required for this operation" + ); + assert!(error_data.data.is_some()); + + // Test serialization + let json = serde_json::to_value(&error_data).unwrap(); + assert_eq!(json["code"], -32042); + assert_eq!( + json["message"], + "URL elicitation is required for this operation" + ); + assert_eq!(json["data"]["url"], "https://example.com/complete"); + assert_eq!(json["data"]["elicitationId"], "elicit-999"); +} + +/// Test ClientCapabilities with different elicitation mode combinations +#[tokio::test] +async fn test_client_capabilities_elicitation_modes() { + // Test with form-only capability + let form_only_caps = ClientCapabilities { + elicitation: Some(ElicitationCapability { + form: Some(FormElicitationCapability { + schema_validation: Some(true), + }), + url: None, + }), + ..Default::default() + }; + + let json = serde_json::to_value(&form_only_caps).unwrap(); + assert!(json["elicitation"]["form"].is_object()); + assert!( + json["elicitation"]["url"].is_null() + || !json["elicitation"].as_object().unwrap().contains_key("url") + ); + + // Test with URL-only capability + let url_only_caps = ClientCapabilities { + elicitation: Some(ElicitationCapability { + form: None, + url: Some(UrlElicitationCapability::default()), + }), + ..Default::default() + }; + + let json = serde_json::to_value(&url_only_caps).unwrap(); + assert!(json["elicitation"]["url"].is_object()); + assert!( + json["elicitation"]["form"].is_null() + || !json["elicitation"] + .as_object() + .unwrap() + .contains_key("form") + ); + + // Test with both capabilities + let both_caps = ClientCapabilities { + elicitation: Some(ElicitationCapability { + form: Some(FormElicitationCapability { + schema_validation: Some(false), + }), + url: Some(UrlElicitationCapability::default()), + }), + ..Default::default() + }; + + let json = serde_json::to_value(&both_caps).unwrap(); + assert!(json["elicitation"]["form"].is_object()); + assert!(json["elicitation"]["url"].is_object()); +} + +/// Test ElicitationCompletionNotification in ServerNotification enum +#[tokio::test] +async fn test_elicitation_completion_in_server_notification() { + let notification_param = ElicitationResponseNotificationParam { + elicitation_id: "notify-123".to_string(), + }; + + let completion_notification = ElicitationCompletionNotification { + method: ElicitationCompletionNotificationMethod, + params: notification_param.clone(), + extensions: Default::default(), + }; + + // Test that it's part of ServerNotification + let server_notification = + ServerNotification::ElicitationCompletionNotification(completion_notification); + + // Test serialization + let json = serde_json::to_value(&server_notification).unwrap(); + assert_eq!(json["method"], "notifications/elicitation/complete"); + assert_eq!(json["params"]["elicitationId"], "notify-123"); + + // Test deserialization + let deserialized: ServerNotification = serde_json::from_value(json).unwrap(); + match deserialized { + ServerNotification::ElicitationCompletionNotification(notif) => { + assert_eq!(notif.params.elicitation_id, "notify-123"); + } + _ => panic!("Expected ElicitationCompletionNotification variant"), + } +} + +/// Test ElicitationAction with URL elicitation workflow +#[tokio::test] +async fn test_url_elicitation_action_workflow() { + // Test Accept action for URL elicitation (user visited URL and confirmed) + let accept_result = CreateElicitationResult { + action: ElicitationAction::Accept, + content: None, // URL elicitation doesn't return content, just confirmation + }; + + let json = serde_json::to_value(&accept_result).unwrap(); + assert_eq!(json["action"], "accept"); + // content should be omitted when None + assert!(json.get("content").is_none() || json["content"].is_null()); + + // Test Decline action for URL elicitation + let decline_result = CreateElicitationResult { + action: ElicitationAction::Decline, + content: None, + }; + + let json = serde_json::to_value(&decline_result).unwrap(); + assert_eq!(json["action"], "decline"); + + // Test Cancel action for URL elicitation + let cancel_result = CreateElicitationResult { + action: ElicitationAction::Cancel, + content: None, + }; + + let json = serde_json::to_value(&cancel_result).unwrap(); + assert_eq!(json["action"], "cancel"); +} + +/// Test method constants for URL elicitation +#[tokio::test] +async fn test_elicitation_method_constants() { + // Test existing methods + assert_eq!(ElicitationCreateRequestMethod::VALUE, "elicitation/create"); + assert_eq!( + ElicitationResponseNotificationMethod::VALUE, + "notifications/elicitation/response" + ); + + // Test new completion notification method + assert_eq!( + ElicitationCompletionNotificationMethod::VALUE, + "notifications/elicitation/complete" + ); +} diff --git a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json index c9d7dab0..c7a2092a 100644 --- a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json +++ b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema.json @@ -529,14 +529,29 @@ ] }, "ElicitationCapability": { - "description": "Capability for handling elicitation requests from servers.\n\nElicitation allows servers to request interactive input from users during tool execution.\nThis capability indicates that a client can handle elicitation requests and present\nappropriate UI to users for collecting the requested information.", + "description": "Elicitation allows servers to request interactive input from users during tool execution.\nThis capability indicates that a client can handle elicitation requests and present\nappropriate UI to users for collecting the requested information.", "type": "object", "properties": { - "schemaValidation": { - "description": "Whether the client supports JSON Schema validation for elicitation responses.\nWhen true, the client will validate user input against the requested_schema\nbefore sending the response back to the server.", - "type": [ - "boolean", - "null" + "form": { + "description": "Whether client supports form-based elicitation.", + "anyOf": [ + { + "$ref": "#/definitions/FormElicitationCapability" + }, + { + "type": "null" + } + ] + }, + "url": { + "description": "Whether client supports URL-based elicitation.", + "anyOf": [ + { + "$ref": "#/definitions/UrlElicitationCapability" + }, + { + "type": "null" + } ] } } @@ -588,6 +603,19 @@ "message" ] }, + "FormElicitationCapability": { + "description": "Capability for handling elicitation requests from servers.\nElicitation allows servers to request interactive input from users during tool execution.\nThis capability indicates that a client can handle elicitation requests and present\nappropriate UI to users for collecting the requested information.\n\nCapability for form mode elicitation.", + "type": "object", + "properties": { + "schemaValidation": { + "description": "Whether the client supports JSON Schema validation for elicitation responses.\nWhen true, the client will validate user input against the requested_schema\nbefore sending the response back to the server.", + "type": [ + "boolean", + "null" + ] + } + } + }, "GetPromptRequestMethod": { "type": "string", "format": "const", @@ -2126,6 +2154,10 @@ "required": [ "uri" ] + }, + "UrlElicitationCapability": { + "description": "Capability for URL mode elicitation.", + "type": "object" } } } \ No newline at end of file diff --git a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json index c9d7dab0..c7a2092a 100644 --- a/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json +++ b/crates/rmcp/tests/test_message_schema/client_json_rpc_message_schema_current.json @@ -529,14 +529,29 @@ ] }, "ElicitationCapability": { - "description": "Capability for handling elicitation requests from servers.\n\nElicitation allows servers to request interactive input from users during tool execution.\nThis capability indicates that a client can handle elicitation requests and present\nappropriate UI to users for collecting the requested information.", + "description": "Elicitation allows servers to request interactive input from users during tool execution.\nThis capability indicates that a client can handle elicitation requests and present\nappropriate UI to users for collecting the requested information.", "type": "object", "properties": { - "schemaValidation": { - "description": "Whether the client supports JSON Schema validation for elicitation responses.\nWhen true, the client will validate user input against the requested_schema\nbefore sending the response back to the server.", - "type": [ - "boolean", - "null" + "form": { + "description": "Whether client supports form-based elicitation.", + "anyOf": [ + { + "$ref": "#/definitions/FormElicitationCapability" + }, + { + "type": "null" + } + ] + }, + "url": { + "description": "Whether client supports URL-based elicitation.", + "anyOf": [ + { + "$ref": "#/definitions/UrlElicitationCapability" + }, + { + "type": "null" + } ] } } @@ -588,6 +603,19 @@ "message" ] }, + "FormElicitationCapability": { + "description": "Capability for handling elicitation requests from servers.\nElicitation allows servers to request interactive input from users during tool execution.\nThis capability indicates that a client can handle elicitation requests and present\nappropriate UI to users for collecting the requested information.\n\nCapability for form mode elicitation.", + "type": "object", + "properties": { + "schemaValidation": { + "description": "Whether the client supports JSON Schema validation for elicitation responses.\nWhen true, the client will validate user input against the requested_schema\nbefore sending the response back to the server.", + "type": [ + "boolean", + "null" + ] + } + } + }, "GetPromptRequestMethod": { "type": "string", "format": "const", @@ -2126,6 +2154,10 @@ "required": [ "uri" ] + }, + "UrlElicitationCapability": { + "description": "Capability for URL mode elicitation.", + "type": "object" } } } \ No newline at end of file diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json index 7bd25060..2986077e 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema.json @@ -505,33 +505,88 @@ ] }, "CreateElicitationRequestParams": { - "description": "Parameters for creating an elicitation request to gather user input.\n\nThis structure contains everything needed to request interactive input from a user:\n- A human-readable message explaining what information is needed\n- A type-safe schema defining the expected structure of the response\n\n# Example\n\n```rust\nuse rmcp::model::*;\n\nlet params = CreateElicitationRequestParams {\n meta: None,\n message: \"Please provide your email\".to_string(),\n requested_schema: ElicitationSchema::builder()\n .required_email(\"email\")\n .build()\n .unwrap(),\n};\n```", - "type": "object", - "properties": { - "_meta": { - "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "description": "Parameters for creating an elicitation request to gather user input.\n\nThis structure contains everything needed to request interactive input from a user:\n- A human-readable message explaining what information is needed\n- A type-safe schema defining the expected structure of the response\n\n# Example\n1. Form-based elicitation request\n```rust\nuse rmcp::model::*;\n\nlet params = CreateElicitationRequestParams::FormElicitationParams {\n meta: None,\n message: \"Please provide your email\".to_string(),\n requested_schema: ElicitationSchema::builder()\n .required_email(\"email\")\n .build()\n .unwrap(),\n};\n```\n2. URL-based elicitation request\n```rust\nuse rmcp::model::*;\nlet params = CreateElicitationRequestParams::UrlElicitationParams {\n meta: None,\n message: \"Please provide your feedback at the following URL\".to_string(),\n url: \"https://example.com/feedback\".to_string(),\n elicitation_id: \"unique-id-123\".to_string(),\n};\n```", + "anyOf": [ + { + "type": "object", + "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "message": { + "type": "string" + }, + "mode": { + "type": "string", + "const": "form" + }, + "requestedSchema": { + "$ref": "#/definitions/ElicitationSchema" + } + }, + "required": [ + "mode", + "message", + "requestedSchema" + ] }, - "message": { - "description": "Human-readable message explaining what input is needed from the user.\nThis should be clear and provide sufficient context for the user to understand\nwhat information they need to provide.", - "type": "string" + { + "type": "object", + "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "elicitationId": { + "type": "string" + }, + "message": { + "type": "string" + }, + "mode": { + "type": "string", + "const": "url" + }, + "url": { + "type": "string" + } + }, + "required": [ + "mode", + "message", + "url", + "elicitationId" + ] }, - "requestedSchema": { - "description": "Type-safe schema defining the expected structure and validation rules for the user's response.\nThis enforces the MCP 2025-06-18 specification that elicitation schemas must be objects\nwith primitive-typed properties.", - "allOf": [ - { + { + "type": "object", + "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "message": { + "type": "string" + }, + "requestedSchema": { "$ref": "#/definitions/ElicitationSchema" } + }, + "required": [ + "message", + "requestedSchema" ] } - }, - "required": [ - "message", - "requestedSchema" ] }, "CreateElicitationResult": { @@ -730,11 +785,28 @@ } ] }, + "ElicitationCompletionNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/elicitation/complete" + }, "ElicitationCreateRequestMethod": { "type": "string", "format": "const", "const": "elicitation/create" }, + "ElicitationResponseNotificationParam": { + "description": "Notification parameters for an url elicitation completion notification.", + "type": "object", + "properties": { + "elicitationId": { + "type": "string" + } + }, + "required": [ + "elicitationId" + ] + }, "ElicitationSchema": { "description": "Type-safe elicitation schema for requesting structured user input.\n\nThis enforces the MCP 2025-06-18 specification that elicitation schemas\nmust be objects with primitive-typed properties.\n\n# Example\n\n```rust\nuse rmcp::model::*;\n\nlet schema = ElicitationSchema::builder()\n .required_email(\"email\")\n .required_integer(\"age\", 0, 150)\n .optional_bool(\"newsletter\", false)\n .build();\n```", "type": "object", @@ -1089,6 +1161,9 @@ { "$ref": "#/definitions/NotificationNoParam3" }, + { + "$ref": "#/definitions/Notification5" + }, { "$ref": "#/definitions/CustomNotification" } @@ -1505,6 +1580,21 @@ "params" ] }, + "Notification5": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/ElicitationCompletionNotificationMethod" + }, + "params": { + "$ref": "#/definitions/ElicitationResponseNotificationParam" + } + }, + "required": [ + "method", + "params" + ] + }, "NotificationNoParam": { "type": "object", "properties": { diff --git a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json index 7bd25060..2986077e 100644 --- a/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json +++ b/crates/rmcp/tests/test_message_schema/server_json_rpc_message_schema_current.json @@ -505,33 +505,88 @@ ] }, "CreateElicitationRequestParams": { - "description": "Parameters for creating an elicitation request to gather user input.\n\nThis structure contains everything needed to request interactive input from a user:\n- A human-readable message explaining what information is needed\n- A type-safe schema defining the expected structure of the response\n\n# Example\n\n```rust\nuse rmcp::model::*;\n\nlet params = CreateElicitationRequestParams {\n meta: None,\n message: \"Please provide your email\".to_string(),\n requested_schema: ElicitationSchema::builder()\n .required_email(\"email\")\n .build()\n .unwrap(),\n};\n```", - "type": "object", - "properties": { - "_meta": { - "description": "Protocol-level metadata for this request (SEP-1319)", - "type": [ - "object", - "null" - ], - "additionalProperties": true + "description": "Parameters for creating an elicitation request to gather user input.\n\nThis structure contains everything needed to request interactive input from a user:\n- A human-readable message explaining what information is needed\n- A type-safe schema defining the expected structure of the response\n\n# Example\n1. Form-based elicitation request\n```rust\nuse rmcp::model::*;\n\nlet params = CreateElicitationRequestParams::FormElicitationParams {\n meta: None,\n message: \"Please provide your email\".to_string(),\n requested_schema: ElicitationSchema::builder()\n .required_email(\"email\")\n .build()\n .unwrap(),\n};\n```\n2. URL-based elicitation request\n```rust\nuse rmcp::model::*;\nlet params = CreateElicitationRequestParams::UrlElicitationParams {\n meta: None,\n message: \"Please provide your feedback at the following URL\".to_string(),\n url: \"https://example.com/feedback\".to_string(),\n elicitation_id: \"unique-id-123\".to_string(),\n};\n```", + "anyOf": [ + { + "type": "object", + "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "message": { + "type": "string" + }, + "mode": { + "type": "string", + "const": "form" + }, + "requestedSchema": { + "$ref": "#/definitions/ElicitationSchema" + } + }, + "required": [ + "mode", + "message", + "requestedSchema" + ] }, - "message": { - "description": "Human-readable message explaining what input is needed from the user.\nThis should be clear and provide sufficient context for the user to understand\nwhat information they need to provide.", - "type": "string" + { + "type": "object", + "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "elicitationId": { + "type": "string" + }, + "message": { + "type": "string" + }, + "mode": { + "type": "string", + "const": "url" + }, + "url": { + "type": "string" + } + }, + "required": [ + "mode", + "message", + "url", + "elicitationId" + ] }, - "requestedSchema": { - "description": "Type-safe schema defining the expected structure and validation rules for the user's response.\nThis enforces the MCP 2025-06-18 specification that elicitation schemas must be objects\nwith primitive-typed properties.", - "allOf": [ - { + { + "type": "object", + "properties": { + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + }, + "message": { + "type": "string" + }, + "requestedSchema": { "$ref": "#/definitions/ElicitationSchema" } + }, + "required": [ + "message", + "requestedSchema" ] } - }, - "required": [ - "message", - "requestedSchema" ] }, "CreateElicitationResult": { @@ -730,11 +785,28 @@ } ] }, + "ElicitationCompletionNotificationMethod": { + "type": "string", + "format": "const", + "const": "notifications/elicitation/complete" + }, "ElicitationCreateRequestMethod": { "type": "string", "format": "const", "const": "elicitation/create" }, + "ElicitationResponseNotificationParam": { + "description": "Notification parameters for an url elicitation completion notification.", + "type": "object", + "properties": { + "elicitationId": { + "type": "string" + } + }, + "required": [ + "elicitationId" + ] + }, "ElicitationSchema": { "description": "Type-safe elicitation schema for requesting structured user input.\n\nThis enforces the MCP 2025-06-18 specification that elicitation schemas\nmust be objects with primitive-typed properties.\n\n# Example\n\n```rust\nuse rmcp::model::*;\n\nlet schema = ElicitationSchema::builder()\n .required_email(\"email\")\n .required_integer(\"age\", 0, 150)\n .optional_bool(\"newsletter\", false)\n .build();\n```", "type": "object", @@ -1089,6 +1161,9 @@ { "$ref": "#/definitions/NotificationNoParam3" }, + { + "$ref": "#/definitions/Notification5" + }, { "$ref": "#/definitions/CustomNotification" } @@ -1505,6 +1580,21 @@ "params" ] }, + "Notification5": { + "type": "object", + "properties": { + "method": { + "$ref": "#/definitions/ElicitationCompletionNotificationMethod" + }, + "params": { + "$ref": "#/definitions/ElicitationResponseNotificationParam" + } + }, + "required": [ + "method", + "params" + ] + }, "NotificationNoParam": { "type": "object", "properties": { diff --git a/examples/servers/src/elicitation_stdio.rs b/examples/servers/src/elicitation_stdio.rs index 10ee6611..82f8d696 100644 --- a/examples/servers/src/elicitation_stdio.rs +++ b/examples/servers/src/elicitation_stdio.rs @@ -17,6 +17,7 @@ use rmcp::{ use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; use tracing_subscriber::{self, EnvFilter}; +use url::Url; /// User information request #[derive(Debug, Serialize, Deserialize, JsonSchema)] @@ -106,6 +107,48 @@ impl ElicitationServer { "User name reset. Next greeting will ask for name again.".to_string(), )])) } + + #[tool(description = "Example of URL elicitation")] + pub async fn secure_tool_call( + &self, + context: RequestContext, + ) -> std::result::Result { + let elicit_result = context + .peer + .elicit_url( + "User must visit the following URL to complete tool call", + Url::parse("https://example.com/complete_tool").expect("valid URL"), + "elicit_123", + ) + .await + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Url elicitation has failed: {}", e), + None, + ) + })?; + match elicit_result { + ElicitationAction::Accept => { + // Mock notifying completion + let _ = context + .peer + .notify_url_elicitation_completed(ElicitationResponseNotificationParam { + elicitation_id: "elicit_123".to_string(), + }) + .await; + Ok(CallToolResult::success(vec![Content::text( + "Elicitation via URL successful".to_string(), + )])) + } + ElicitationAction::Cancel => Ok(CallToolResult::success(vec![Content::text( + "Elicitation via URL cancelled by user".to_string(), + )])), + ElicitationAction::Decline => Ok(CallToolResult::error(vec![Content::text( + "Elicitation via URL declined by user".to_string(), + )])), + } + } } #[tool_handler]