feat(elicitation): add support URL elicitation. SEP-1036 (#605)
This commit is contained in:
parent
edd5b1d7e9
commit
187597bf7e
1 changed files with 1533 additions and 271 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue