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"]
|
client = ["dep:tokio-stream"]
|
||||||
server = ["transport-async-rw", "dep:schemars", "dep:pastey"]
|
server = ["transport-async-rw", "dep:schemars", "dep:pastey"]
|
||||||
macros = ["dep:rmcp-macros", "dep:pastey"]
|
macros = ["dep:rmcp-macros", "dep:pastey"]
|
||||||
elicitation = []
|
elicitation = ["dep:url"]
|
||||||
|
|
||||||
# reqwest http client
|
# reqwest http client
|
||||||
__reqwest = ["dep:reqwest"]
|
__reqwest = ["dep:reqwest"]
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,10 @@ impl<H: ClientHandler> Service<RoleClient> for H {
|
||||||
ServerNotification::PromptListChangedNotification(_notification_no_param) => {
|
ServerNotification::PromptListChangedNotification(_notification_no_param) => {
|
||||||
self.on_prompt_list_changed(context).await
|
self.on_prompt_list_changed(context).await
|
||||||
}
|
}
|
||||||
|
ServerNotification::ElicitationCompletionNotification(notification) => {
|
||||||
|
self.on_url_elicitation_notification_complete(notification.params, context)
|
||||||
|
.await
|
||||||
|
}
|
||||||
ServerNotification::CustomNotification(notification) => {
|
ServerNotification::CustomNotification(notification) => {
|
||||||
self.on_custom_notification(notification, context).await
|
self.on_custom_notification(notification, context).await
|
||||||
}
|
}
|
||||||
|
|
@ -116,6 +120,44 @@ pub trait ClientHandler: Sized + Send + Sync + 'static {
|
||||||
/// # Default Behavior
|
/// # Default Behavior
|
||||||
/// The default implementation automatically declines all elicitation requests.
|
/// The default implementation automatically declines all elicitation requests.
|
||||||
/// Real clients should override this to provide user interaction.
|
/// 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(
|
fn create_elicitation(
|
||||||
&self,
|
&self,
|
||||||
request: CreateElicitationRequestParams,
|
request: CreateElicitationRequestParams,
|
||||||
|
|
@ -189,6 +231,14 @@ pub trait ClientHandler: Sized + Send + Sync + 'static {
|
||||||
) -> impl Future<Output = ()> + Send + '_ {
|
) -> impl Future<Output = ()> + Send + '_ {
|
||||||
std::future::ready(())
|
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(
|
fn on_custom_notification(
|
||||||
&self,
|
&self,
|
||||||
notification: CustomNotification,
|
notification: CustomNotification,
|
||||||
|
|
|
||||||
|
|
@ -453,6 +453,7 @@ impl ErrorCode {
|
||||||
pub const INVALID_PARAMS: Self = Self(-32602);
|
pub const INVALID_PARAMS: Self = Self(-32602);
|
||||||
pub const INTERNAL_ERROR: Self = Self(-32603);
|
pub const INTERNAL_ERROR: Self = Self(-32603);
|
||||||
pub const PARSE_ERROR: Self = Self(-32700);
|
pub const PARSE_ERROR: Self = Self(-32700);
|
||||||
|
pub const URL_ELICITATION_REQUIRED: Self = Self(-32042);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error information for JSON-RPC error responses.
|
/// 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 {
|
pub fn internal_error(message: impl Into<Cow<'static, str>>, data: Option<Value>) -> Self {
|
||||||
Self::new(ErrorCode::INTERNAL_ERROR, message, data)
|
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.
|
/// 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.
|
// Elicitation allows servers to request interactive input from users during tool execution.
|
||||||
const_string!(ElicitationCreateRequestMethod = "elicitation/create");
|
const_string!(ElicitationCreateRequestMethod = "elicitation/create");
|
||||||
const_string!(ElicitationResponseNotificationMethod = "notifications/elicitation/response");
|
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.
|
/// Represents the possible actions a user can take in response to an elicitation request.
|
||||||
///
|
///
|
||||||
|
|
@ -1989,6 +1997,72 @@ pub enum ElicitationAction {
|
||||||
Cancel,
|
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.
|
/// Parameters for creating an elicitation request to gather user input.
|
||||||
///
|
///
|
||||||
/// This structure contains everything needed to request interactive input from a user:
|
/// 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
|
/// - A type-safe schema defining the expected structure of the response
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
/// 1. Form-based elicitation request
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use rmcp::model::*;
|
/// use rmcp::model::*;
|
||||||
///
|
///
|
||||||
/// let params = CreateElicitationRequestParams {
|
/// let params = CreateElicitationRequestParams::FormElicitationParams {
|
||||||
/// meta: None,
|
/// meta: None,
|
||||||
/// message: "Please provide your email".to_string(),
|
/// message: "Please provide your email".to_string(),
|
||||||
/// requested_schema: ElicitationSchema::builder()
|
/// requested_schema: ElicitationSchema::builder()
|
||||||
/// .required_email("email")
|
/// .required_email("email")
|
||||||
|
|
@ -2009,31 +2083,68 @@ pub enum ElicitationAction {
|
||||||
/// .unwrap(),
|
/// .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)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(
|
||||||
|
tag = "mode",
|
||||||
|
try_from = "CreateElicitationRequestParamDeserializeHelper"
|
||||||
|
)]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct CreateElicitationRequestParams {
|
pub enum CreateElicitationRequestParams {
|
||||||
/// Protocol-level metadata for this request (SEP-1319)
|
#[serde(rename = "form", rename_all = "camelCase")]
|
||||||
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
|
FormElicitationParams {
|
||||||
pub meta: Option<Meta>,
|
/// 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.
|
/// Type-safe schema defining the expected structure and validation rules for the user's response.
|
||||||
/// This should be clear and provide sufficient context for the user to understand
|
/// This enforces the MCP 2025-06-18 specification that elicitation schemas must be objects
|
||||||
/// what information they need to provide.
|
/// with primitive-typed properties.
|
||||||
pub message: String,
|
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.
|
/// The URL where the user can provide the requested information.
|
||||||
/// This enforces the MCP 2025-06-18 specification that elicitation schemas must be objects
|
/// The client should direct the user to this URL to complete the elicitation.
|
||||||
/// with primitive-typed properties.
|
url: String,
|
||||||
pub requested_schema: ElicitationSchema,
|
/// The unique identifier for this elicitation request.
|
||||||
|
elicitation_id: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequestParamsMeta for CreateElicitationRequestParams {
|
impl RequestParamsMeta for CreateElicitationRequestParams {
|
||||||
fn meta(&self) -> Option<&Meta> {
|
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> {
|
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 =
|
pub type CreateElicitationRequest =
|
||||||
Request<ElicitationCreateRequestMethod, CreateElicitationRequestParams>;
|
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
|
// TOOL EXECUTION RESULTS
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -2575,6 +2698,7 @@ ts_union!(
|
||||||
| ResourceListChangedNotification
|
| ResourceListChangedNotification
|
||||||
| ToolListChangedNotification
|
| ToolListChangedNotification
|
||||||
| PromptListChangedNotification
|
| PromptListChangedNotification
|
||||||
|
| ElicitationCompletionNotification
|
||||||
| CustomNotification;
|
| CustomNotification;
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -3080,4 +3204,142 @@ mod tests {
|
||||||
assert_eq!(json["serverInfo"]["icons"][0]["sizes"][0], "48x48");
|
assert_eq!(json["serverInfo"]["icons"][0]["sizes"][0], "48x48");
|
||||||
assert_eq!(json["serverInfo"]["websiteUrl"], "https://docs.example.com");
|
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.
|
/// 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.
|
/// Elicitation allows servers to request interactive input from users during tool execution.
|
||||||
/// This capability indicates that a client can handle elicitation requests and present
|
/// This capability indicates that a client can handle elicitation requests and present
|
||||||
/// appropriate UI to users for collecting the requested information.
|
/// appropriate UI to users for collecting the requested information.
|
||||||
|
|
@ -187,11 +207,12 @@ impl TasksCapability {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct ElicitationCapability {
|
pub struct ElicitationCapability {
|
||||||
/// Whether the client supports JSON Schema validation for elicitation responses.
|
/// Whether client supports form-based elicitation.
|
||||||
/// 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")]
|
#[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).
|
/// 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>
|
impl<const E: bool, const EXT: bool, const R: bool, const S: bool, const TASKS: bool>
|
||||||
ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, EXT, R, S, true, TASKS>>
|
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
|
/// When enabled, the client will validate user input against the requested_schema
|
||||||
/// before sending responses back to the server.
|
/// before sending responses back to the server.
|
||||||
pub fn enable_elicitation_schema_validation(mut self) -> Self {
|
pub fn enable_elicitation_schema_validation(mut self) -> Self {
|
||||||
if let Some(c) = self.elicitation.as_mut() {
|
if let Some(c) = self.elicitation.as_mut() {
|
||||||
c.schema_validation = Some(true);
|
c.form = Some(FormElicitationCapability {
|
||||||
|
schema_validation: Some(true),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,7 @@ variant_extension! {
|
||||||
ResourceListChangedNotification
|
ResourceListChangedNotification
|
||||||
ToolListChangedNotification
|
ToolListChangedNotification
|
||||||
PromptListChangedNotification
|
PromptListChangedNotification
|
||||||
|
ElicitationCompletionNotification
|
||||||
CustomNotification
|
CustomNotification
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
#[cfg(feature = "elicitation")]
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
#[cfg(feature = "elicitation")]
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
#[cfg(feature = "elicitation")]
|
#[cfg(feature = "elicitation")]
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
CreateElicitationRequest, CreateElicitationRequestParams, CreateElicitationResult,
|
CreateElicitationRequest, CreateElicitationRequestParams, CreateElicitationResult,
|
||||||
|
ElicitationAction, ElicitationCompletionNotification, ElicitationResponseNotificationParam,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
model::{
|
model::{
|
||||||
|
|
@ -432,6 +437,8 @@ impl Peer<RoleServer> {
|
||||||
method!(peer_req create_elicitation CreateElicitationRequest(CreateElicitationRequestParams) => CreateElicitationResult);
|
method!(peer_req create_elicitation CreateElicitationRequest(CreateElicitationRequestParams) => CreateElicitationResult);
|
||||||
#[cfg(feature = "elicitation")]
|
#[cfg(feature = "elicitation")]
|
||||||
method!(peer_req_with_timeout create_elicitation_with_timeout CreateElicitationRequest(CreateElicitationRequestParams) => CreateElicitationResult);
|
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_cancelled CancelledNotification(CancelledNotificationParam));
|
||||||
method!(peer_not notify_progress ProgressNotification(ProgressNotificationParam));
|
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")]
|
#[cfg(feature = "elicitation")]
|
||||||
impl Peer<RoleServer> {
|
impl Peer<RoleServer> {
|
||||||
/// Check if the client supports elicitation capability
|
/// Check if the client supports elicitation capability
|
||||||
|
|
@ -543,11 +556,27 @@ impl Peer<RoleServer> {
|
||||||
/// Returns true if the client declared elicitation capability during initialization,
|
/// Returns true if the client declared elicitation capability during initialization,
|
||||||
/// false otherwise. According to MCP 2025-06-18 specification, clients that support
|
/// false otherwise. According to MCP 2025-06-18 specification, clients that support
|
||||||
/// elicitation MUST declare the capability during initialization.
|
/// 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() {
|
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 {
|
} else {
|
||||||
false
|
HashSet::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -698,8 +727,11 @@ impl Peer<RoleServer> {
|
||||||
where
|
where
|
||||||
T: ElicitationSafe + for<'de> serde::Deserialize<'de>,
|
T: ElicitationSafe + for<'de> serde::Deserialize<'de>,
|
||||||
{
|
{
|
||||||
// Check if client supports elicitation capability
|
// Check if client supports form elicitation capability
|
||||||
if !self.supports_elicitation() {
|
if !self
|
||||||
|
.supported_elicitation_modes()
|
||||||
|
.contains(&ElicitationMode::Form)
|
||||||
|
{
|
||||||
return Err(ElicitationError::CapabilityNotSupported);
|
return Err(ElicitationError::CapabilityNotSupported);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -717,7 +749,7 @@ impl Peer<RoleServer> {
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
.create_elicitation_with_timeout(
|
.create_elicitation_with_timeout(
|
||||||
CreateElicitationRequestParams {
|
CreateElicitationRequestParams::FormElicitationParams {
|
||||||
meta: None,
|
meta: None,
|
||||||
message: message.into(),
|
message: message.into(),
|
||||||
requested_schema: schema,
|
requested_schema: schema,
|
||||||
|
|
@ -741,4 +773,123 @@ impl Peer<RoleServer> {
|
||||||
crate::model::ElicitationAction::Cancel => Err(ElicitationError::UserCancelled),
|
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": {
|
"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",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"schemaValidation": {
|
"form": {
|
||||||
"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.",
|
"description": "Whether client supports form-based elicitation.",
|
||||||
"type": [
|
"anyOf": [
|
||||||
"boolean",
|
{
|
||||||
"null"
|
"$ref": "#/definitions/FormElicitationCapability"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"description": "Whether client supports URL-based elicitation.",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/UrlElicitationCapability"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -588,6 +603,19 @@
|
||||||
"message"
|
"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": {
|
"GetPromptRequestMethod": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "const",
|
"format": "const",
|
||||||
|
|
@ -2126,6 +2154,10 @@
|
||||||
"required": [
|
"required": [
|
||||||
"uri"
|
"uri"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"UrlElicitationCapability": {
|
||||||
|
"description": "Capability for URL mode elicitation.",
|
||||||
|
"type": "object"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -529,14 +529,29 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ElicitationCapability": {
|
"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",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"schemaValidation": {
|
"form": {
|
||||||
"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.",
|
"description": "Whether client supports form-based elicitation.",
|
||||||
"type": [
|
"anyOf": [
|
||||||
"boolean",
|
{
|
||||||
"null"
|
"$ref": "#/definitions/FormElicitationCapability"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"description": "Whether client supports URL-based elicitation.",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/UrlElicitationCapability"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -588,6 +603,19 @@
|
||||||
"message"
|
"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": {
|
"GetPromptRequestMethod": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "const",
|
"format": "const",
|
||||||
|
|
@ -2126,6 +2154,10 @@
|
||||||
"required": [
|
"required": [
|
||||||
"uri"
|
"uri"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"UrlElicitationCapability": {
|
||||||
|
"description": "Capability for URL mode elicitation.",
|
||||||
|
"type": "object"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -505,33 +505,88 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"CreateElicitationRequestParams": {
|
"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```",
|
"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```",
|
||||||
"type": "object",
|
"anyOf": [
|
||||||
"properties": {
|
{
|
||||||
"_meta": {
|
"type": "object",
|
||||||
"description": "Protocol-level metadata for this request (SEP-1319)",
|
"properties": {
|
||||||
"type": [
|
"_meta": {
|
||||||
"object",
|
"type": [
|
||||||
"null"
|
"object",
|
||||||
],
|
"null"
|
||||||
"additionalProperties": true
|
],
|
||||||
},
|
"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.",
|
"message": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"requestedSchema": {
|
"mode": {
|
||||||
"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.",
|
"type": "string",
|
||||||
"allOf": [
|
"const": "form"
|
||||||
{
|
},
|
||||||
|
"requestedSchema": {
|
||||||
"$ref": "#/definitions/ElicitationSchema"
|
"$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": {
|
"CreateElicitationResult": {
|
||||||
|
|
@ -730,11 +785,28 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ElicitationCompletionNotificationMethod": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "const",
|
||||||
|
"const": "notifications/elicitation/complete"
|
||||||
|
},
|
||||||
"ElicitationCreateRequestMethod": {
|
"ElicitationCreateRequestMethod": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "const",
|
"format": "const",
|
||||||
"const": "elicitation/create"
|
"const": "elicitation/create"
|
||||||
},
|
},
|
||||||
|
"ElicitationResponseNotificationParam": {
|
||||||
|
"description": "Notification parameters for an url elicitation completion notification.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"elicitationId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"elicitationId"
|
||||||
|
]
|
||||||
|
},
|
||||||
"ElicitationSchema": {
|
"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```",
|
"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",
|
"type": "object",
|
||||||
|
|
@ -1089,6 +1161,9 @@
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/NotificationNoParam3"
|
"$ref": "#/definitions/NotificationNoParam3"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/Notification5"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/CustomNotification"
|
"$ref": "#/definitions/CustomNotification"
|
||||||
}
|
}
|
||||||
|
|
@ -1505,6 +1580,21 @@
|
||||||
"params"
|
"params"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"Notification5": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"method": {
|
||||||
|
"$ref": "#/definitions/ElicitationCompletionNotificationMethod"
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"$ref": "#/definitions/ElicitationResponseNotificationParam"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"method",
|
||||||
|
"params"
|
||||||
|
]
|
||||||
|
},
|
||||||
"NotificationNoParam": {
|
"NotificationNoParam": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
||||||
|
|
@ -505,33 +505,88 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"CreateElicitationRequestParams": {
|
"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```",
|
"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```",
|
||||||
"type": "object",
|
"anyOf": [
|
||||||
"properties": {
|
{
|
||||||
"_meta": {
|
"type": "object",
|
||||||
"description": "Protocol-level metadata for this request (SEP-1319)",
|
"properties": {
|
||||||
"type": [
|
"_meta": {
|
||||||
"object",
|
"type": [
|
||||||
"null"
|
"object",
|
||||||
],
|
"null"
|
||||||
"additionalProperties": true
|
],
|
||||||
},
|
"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.",
|
"message": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"requestedSchema": {
|
"mode": {
|
||||||
"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.",
|
"type": "string",
|
||||||
"allOf": [
|
"const": "form"
|
||||||
{
|
},
|
||||||
|
"requestedSchema": {
|
||||||
"$ref": "#/definitions/ElicitationSchema"
|
"$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": {
|
"CreateElicitationResult": {
|
||||||
|
|
@ -730,11 +785,28 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"ElicitationCompletionNotificationMethod": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "const",
|
||||||
|
"const": "notifications/elicitation/complete"
|
||||||
|
},
|
||||||
"ElicitationCreateRequestMethod": {
|
"ElicitationCreateRequestMethod": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "const",
|
"format": "const",
|
||||||
"const": "elicitation/create"
|
"const": "elicitation/create"
|
||||||
},
|
},
|
||||||
|
"ElicitationResponseNotificationParam": {
|
||||||
|
"description": "Notification parameters for an url elicitation completion notification.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"elicitationId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"elicitationId"
|
||||||
|
]
|
||||||
|
},
|
||||||
"ElicitationSchema": {
|
"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```",
|
"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",
|
"type": "object",
|
||||||
|
|
@ -1089,6 +1161,9 @@
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/NotificationNoParam3"
|
"$ref": "#/definitions/NotificationNoParam3"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/Notification5"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/definitions/CustomNotification"
|
"$ref": "#/definitions/CustomNotification"
|
||||||
}
|
}
|
||||||
|
|
@ -1505,6 +1580,21 @@
|
||||||
"params"
|
"params"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"Notification5": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"method": {
|
||||||
|
"$ref": "#/definitions/ElicitationCompletionNotificationMethod"
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"$ref": "#/definitions/ElicitationResponseNotificationParam"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"method",
|
||||||
|
"params"
|
||||||
|
]
|
||||||
|
},
|
||||||
"NotificationNoParam": {
|
"NotificationNoParam": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ use rmcp::{
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tracing_subscriber::{self, EnvFilter};
|
use tracing_subscriber::{self, EnvFilter};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
/// User information request
|
/// User information request
|
||||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
|
@ -106,6 +107,48 @@ impl ElicitationServer {
|
||||||
"User name reset. Next greeting will ask for name again.".to_string(),
|
"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]
|
#[tool_handler]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue