feat(elicitation): add support URL elicitation. SEP-1036 (#605)

This commit is contained in:
Pavel Bezglasny 2026-02-08 03:39:30 +01:00 • committed by GitHub
commit 187597bf7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1533 additions and 271 deletions

View file

@ -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"]

View file

@ -62,6 +62,10 @@ impl<H: ClientHandler> Service<RoleClient> 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<RoleClient>,
/// ) -> Result<CreateElicitationResult, McpError> {
/// 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<Output = ()> + Send + '_ {
std::future::ready(())
}
fn on_url_elicitation_notification_complete(
&self,
params: ElicitationResponseNotificationParam,
context: NotificationContext<RoleClient>,
) -> impl Future<Output = ()> + Send + '_ {
std::future::ready(())
}
fn on_custom_notification(
&self,
notification: CustomNotification,

View file

@ -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<Cow<'static, str>>, data: Option<Value>) -> Self {
Self::new(ErrorCode::INTERNAL_ERROR, message, data)
}
pub fn url_elicitation_required(
message: impl Into<Cow<'static, str>>,
data: Option<Value>,
) -> Self {
Self::new(ErrorCode::URL_ELICITATION_REQUIRED, message, data)
}
}
/// Represents any JSON-RPC message that can be sent or received.
@ -1970,6 +1977,7 @@ pub type RootsListChangedNotification = NotificationNoParam<RootsListChangedNoti
// Elicitation allows servers to request interactive input from users during tool execution.
const_string!(ElicitationCreateRequestMethod = "elicitation/create");
const_string!(ElicitationResponseNotificationMethod = "notifications/elicitation/response");
const_string!(ElicitationCompletionNotificationMethod = "notifications/elicitation/complete");
/// Represents the possible actions a user can take in response to an elicitation request.
///
@ -1989,6 +1997,72 @@ pub enum ElicitationAction {
Cancel,
}
/// Helper enum for deserializing CreateElicitationRequestParam with backward compatibility.
/// When mode is missing, it defaults to FormElicitationParam.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "mode")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
enum CreateElicitationRequestParamDeserializeHelper {
#[serde(rename = "form", rename_all = "camelCase")]
FormElicitationParam {
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
meta: Option<Meta>,
message: String,
requested_schema: ElicitationSchema,
},
#[serde(rename = "url", rename_all = "camelCase")]
UrlElicitationParam {
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
meta: Option<Meta>,
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<Meta>,
message: String,
requested_schema: ElicitationSchema,
},
}
impl TryFrom<CreateElicitationRequestParamDeserializeHelper> for CreateElicitationRequestParams {
type Error = serde_json::Error;
fn try_from(
value: CreateElicitationRequestParamDeserializeHelper,
) -> Result<Self, Self::Error> {
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:
@ -1996,12 +2070,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")
@ -2009,31 +2083,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")]
#[serde(
tag = "mode",
try_from = "CreateElicitationRequestParamDeserializeHelper"
)]
#[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<Meta>,
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<Meta>,
/// 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,
/// 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.
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<Meta>,
/// 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.
pub requested_schema: ElicitationSchema,
/// 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<Meta> {
&mut self.meta
match self {
CreateElicitationRequestParams::FormElicitationParams { meta, .. } => meta,
CreateElicitationRequestParams::UrlElicitationParams { meta, .. } => meta,
}
}
}
@ -2063,6 +2174,18 @@ pub struct CreateElicitationResult {
pub type CreateElicitationRequest =
Request<ElicitationCreateRequestMethod, CreateElicitationRequestParams>;
/// 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<ElicitationCompletionNotificationMethod, ElicitationResponseNotificationParam>;
// =============================================================================
// TOOL EXECUTION RESULTS
// =============================================================================
@ -2575,6 +2698,7 @@ ts_union!(
| ResourceListChangedNotification
| ToolListChangedNotification
| PromptListChangedNotification
| ElicitationCompletionNotification
| CustomNotification;
);
@ -3080,4 +3204,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);
}
}

View file

@ -179,7 +179,27 @@ 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 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.
#[serde(skip_serializing_if = "Option::is_none")]
pub schema_validation: Option<bool>,
}
/// 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.
@ -187,11 +207,12 @@ impl TasksCapability {
#[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.
/// Whether client supports form-based elicitation.
#[serde(skip_serializing_if = "Option::is_none")]
pub schema_validation: Option<bool>,
pub form: Option<FormElicitationCapability>,
/// Whether client supports URL-based elicitation.
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<UrlElicitationCapability>,
}
/// Sampling capability with optional sub-capabilities (SEP-1577).
@ -504,12 +525,14 @@ impl<const E: bool, const EXT: bool, const R: bool, const EL: bool, const TASKS:
impl<const E: bool, const EXT: bool, const R: bool, const S: bool, const TASKS: bool>
ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, EXT, R, S, true, TASKS>>
{
/// 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
}

View file

@ -188,6 +188,7 @@ variant_extension! {
ResourceListChangedNotification
ToolListChangedNotification
PromptListChangedNotification
ElicitationCompletionNotification
CustomNotification
}
}

View file

@ -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::{
@ -432,6 +437,8 @@ impl Peer<RoleServer> {
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));
@ -536,6 +543,12 @@ macro_rules! elicit_safe {
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ElicitationMode {
Form,
Url,
}
#[cfg(feature = "elicitation")]
impl Peer<RoleServer> {
/// Check if the client supports elicitation capability
@ -543,11 +556,27 @@ impl Peer<RoleServer> {
/// 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<ElicitationMode> {
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()
}
}
@ -698,8 +727,11 @@ impl Peer<RoleServer> {
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);
}
@ -717,7 +749,7 @@ impl Peer<RoleServer> {
let response = self
.create_elicitation_with_timeout(
CreateElicitationRequestParams {
CreateElicitationRequestParams::FormElicitationParams {
meta: None,
message: message.into(),
requested_schema: schema,
@ -741,4 +773,123 @@ impl Peer<RoleServer> {
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<RoleServer>) -> Result<(), Box<dyn std::error::Error>> {
/// 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<String>,
url: impl Into<Url>,
elicitation_id: impl Into<String>,
) -> Result<ElicitationAction, ElicitationError> {
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<RoleServer>) -> Result<(), Box<dyn std::error::Error>> {
/// 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<String>,
url: impl Into<Url>,
elicitation_id: impl Into<String>,
timeout: Option<std::time::Duration>,
) -> Result<ElicitationAction, ElicitationError> {
// 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)
}
}

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}
}

View file

@ -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"
}
}
}

View file

@ -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
},
"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"
},
"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": [
{
"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"
]
},
{
"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"
]
},
{
"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": {

View file

@ -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
},
"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"
},
"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": [
{
"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"
]
},
{
"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"
]
},
{
"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": {

View file

@ -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<RoleServer>,
) -> std::result::Result<CallToolResult, McpError> {
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]