feat(elicitation): add support URL elicitation. SEP-1036 (#605)
This commit is contained in:
parent
edd5b1d7e9
commit
187597bf7e
12 changed files with 1533 additions and 271 deletions
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ variant_extension! {
|
|||
ResourceListChangedNotification
|
||||
ToolListChangedNotification
|
||||
PromptListChangedNotification
|
||||
ElicitationCompletionNotification
|
||||
CustomNotification
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue