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

This commit is contained in:
Pavel Bezglasny 2026-02-08 03:39:30 +01:00 • committed by GitHub
commit 187597bf7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -453,6 +453,7 @@ impl ErrorCode {
pub const INVALID_PARAMS: Self = Self(-32602);
pub const INTERNAL_ERROR: Self = Self(-32603);
pub const PARSE_ERROR: Self = Self(-32700);
pub const URL_ELICITATION_REQUIRED: Self = Self(-32042);
}
/// Error information for JSON-RPC error responses.
@ -504,6 +505,12 @@ impl ErrorData {
pub fn internal_error(message: impl Into<Cow<'static, str>>, data: Option<Value>) -> Self {
Self::new(ErrorCode::INTERNAL_ERROR, message, data)
}
pub fn url_elicitation_required(
message: impl Into<Cow<'static, str>>,
data: Option<Value>,
) -> Self {
Self::new(ErrorCode::URL_ELICITATION_REQUIRED, message, data)
}
}
/// Represents any JSON-RPC message that can be sent or received.
@ -1970,6 +1977,7 @@ pub type RootsListChangedNotification = NotificationNoParam<RootsListChangedNoti
// Elicitation allows servers to request interactive input from users during tool execution.
const_string!(ElicitationCreateRequestMethod = "elicitation/create");
const_string!(ElicitationResponseNotificationMethod = "notifications/elicitation/response");
const_string!(ElicitationCompletionNotificationMethod = "notifications/elicitation/complete");
/// Represents the possible actions a user can take in response to an elicitation request.
///
@ -1989,6 +1997,72 @@ pub enum ElicitationAction {
Cancel,
}
/// Helper enum for deserializing CreateElicitationRequestParam with backward compatibility.
/// When mode is missing, it defaults to FormElicitationParam.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(tag = "mode")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
enum CreateElicitationRequestParamDeserializeHelper {
#[serde(rename = "form", rename_all = "camelCase")]
FormElicitationParam {
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
meta: Option<Meta>,
message: String,
requested_schema: ElicitationSchema,
},
#[serde(rename = "url", rename_all = "camelCase")]
UrlElicitationParam {
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
meta: Option<Meta>,
message: String,
url: String,
elicitation_id: String,
},
#[serde(untagged, rename_all = "camelCase")]
FormElicitationParamBackwardsCompat {
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
meta: Option<Meta>,
message: String,
requested_schema: ElicitationSchema,
},
}
impl TryFrom<CreateElicitationRequestParamDeserializeHelper> for CreateElicitationRequestParams {
type Error = serde_json::Error;
fn try_from(
value: CreateElicitationRequestParamDeserializeHelper,
) -> Result<Self, Self::Error> {
match value {
CreateElicitationRequestParamDeserializeHelper::FormElicitationParam {
meta,
message,
requested_schema,
}
| CreateElicitationRequestParamDeserializeHelper::FormElicitationParamBackwardsCompat {
meta,
message,
requested_schema,
} => Ok(CreateElicitationRequestParams::FormElicitationParams {
meta,
message,
requested_schema,
}),
CreateElicitationRequestParamDeserializeHelper::UrlElicitationParam {
meta,
message,
url,
elicitation_id,
} => Ok(CreateElicitationRequestParams::UrlElicitationParams {
meta,
message,
url,
elicitation_id,
}),
}
}
}
/// Parameters for creating an elicitation request to gather user input.
///
/// This structure contains everything needed to request interactive input from a user:
@ -1996,12 +2070,12 @@ pub enum ElicitationAction {
/// - A type-safe schema defining the expected structure of the response
///
/// # Example
///
/// 1. Form-based elicitation request
/// ```rust
/// use rmcp::model::*;
///
/// let params = CreateElicitationRequestParams {
/// meta: None,
/// let params = CreateElicitationRequestParams::FormElicitationParams {
/// meta: None,
/// message: "Please provide your email".to_string(),
/// requested_schema: ElicitationSchema::builder()
/// .required_email("email")
@ -2009,31 +2083,68 @@ pub enum ElicitationAction {
/// .unwrap(),
/// };
/// ```
/// 2. URL-based elicitation request
/// ```rust
/// use rmcp::model::*;
/// let params = CreateElicitationRequestParams::UrlElicitationParams {
/// meta: None,
/// message: "Please provide your feedback at the following URL".to_string(),
/// url: "https://example.com/feedback".to_string(),
/// elicitation_id: "unique-id-123".to_string(),
/// };
/// ```
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[serde(
tag = "mode",
try_from = "CreateElicitationRequestParamDeserializeHelper"
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CreateElicitationRequestParams {
/// Protocol-level metadata for this request (SEP-1319)
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<Meta>,
pub enum CreateElicitationRequestParams {
#[serde(rename = "form", rename_all = "camelCase")]
FormElicitationParams {
/// Protocol-level metadata for this request (SEP-1319)
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
meta: Option<Meta>,
/// Human-readable message explaining what input is needed from the user.
/// This should be clear and provide sufficient context for the user to understand
/// what information they need to provide.
message: String,
/// Human-readable message explaining what input is needed from the user.
/// This should be clear and provide sufficient context for the user to understand
/// what information they need to provide.
pub message: String,
/// Type-safe schema defining the expected structure and validation rules for the user's response.
/// This enforces the MCP 2025-06-18 specification that elicitation schemas must be objects
/// with primitive-typed properties.
requested_schema: ElicitationSchema,
},
#[serde(rename = "url", rename_all = "camelCase")]
UrlElicitationParams {
/// Protocol-level metadata for this request (SEP-1319)
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
meta: Option<Meta>,
/// Human-readable message explaining what input is needed from the user.
/// This should be clear and provide sufficient context for the user to understand
/// what information they need to provide.
message: String,
/// Type-safe schema defining the expected structure and validation rules for the user's response.
/// This enforces the MCP 2025-06-18 specification that elicitation schemas must be objects
/// with primitive-typed properties.
pub requested_schema: ElicitationSchema,
/// The URL where the user can provide the requested information.
/// The client should direct the user to this URL to complete the elicitation.
url: String,
/// The unique identifier for this elicitation request.
elicitation_id: String,
},
}
impl RequestParamsMeta for CreateElicitationRequestParams {
fn meta(&self) -> Option<&Meta> {
self.meta.as_ref()
match self {
CreateElicitationRequestParams::FormElicitationParams { meta, .. } => meta.as_ref(),
CreateElicitationRequestParams::UrlElicitationParams { meta, .. } => meta.as_ref(),
}
}
fn meta_mut(&mut self) -> &mut Option<Meta> {
&mut self.meta
match self {
CreateElicitationRequestParams::FormElicitationParams { meta, .. } => meta,
CreateElicitationRequestParams::UrlElicitationParams { meta, .. } => meta,
}
}
}
@ -2063,6 +2174,18 @@ pub struct CreateElicitationResult {
pub type CreateElicitationRequest =
Request<ElicitationCreateRequestMethod, CreateElicitationRequestParams>;
/// Notification parameters for an url elicitation completion notification.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ElicitationResponseNotificationParam {
pub elicitation_id: String,
}
/// Notification sent when an url elicitation process is completed.
pub type ElicitationCompletionNotification =
Notification<ElicitationCompletionNotificationMethod, ElicitationResponseNotificationParam>;
// =============================================================================
// TOOL EXECUTION RESULTS
// =============================================================================
@ -2575,6 +2698,7 @@ ts_union!(
| ResourceListChangedNotification
| ToolListChangedNotification
| PromptListChangedNotification
| ElicitationCompletionNotification
| CustomNotification;
);
@ -3080,4 +3204,142 @@ mod tests {
assert_eq!(json["serverInfo"]["icons"][0]["sizes"][0], "48x48");
assert_eq!(json["serverInfo"]["websiteUrl"], "https://docs.example.com");
}
#[test]
fn test_elicitation_deserialization_untagged() {
// Test deserialization without the "type" field (should default to FormElicitationParam)
let json_data_without_tag = json!({
"message": "Please provide more details.",
"requestedSchema": {
"title": "User Details",
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" }
},
"required": ["name", "age"]
}
});
let elicitation: CreateElicitationRequestParams =
serde_json::from_value(json_data_without_tag).expect("Deserialization failed");
if let CreateElicitationRequestParams::FormElicitationParams {
meta,
message,
requested_schema,
} = elicitation
{
assert_eq!(meta, None);
assert_eq!(message, "Please provide more details.");
assert_eq!(requested_schema.title, Some(Cow::from("User Details")));
assert_eq!(requested_schema.type_, ObjectTypeConst);
} else {
panic!("Expected FormElicitationParam");
}
}
#[test]
fn test_elicitation_deserialization() {
let json_data_form = json!({
"_meta": { "meta_form_key_1": "meta form value 1" },
"mode": "form",
"message": "Please provide more details.",
"requestedSchema": {
"title": "User Details",
"type": "object",
"properties": {
"name": { "type": "string" },
"age": { "type": "integer" }
},
"required": ["name", "age"]
}
});
let elicitation_form: CreateElicitationRequestParams =
serde_json::from_value(json_data_form).expect("Deserialization failed");
if let CreateElicitationRequestParams::FormElicitationParams {
meta,
message,
requested_schema,
} = elicitation_form
{
assert_eq!(
meta,
Some(Meta(object!({ "meta_form_key_1": "meta form value 1" })))
);
assert_eq!(message, "Please provide more details.");
assert_eq!(requested_schema.title, Some(Cow::from("User Details")));
assert_eq!(requested_schema.type_, ObjectTypeConst);
} else {
panic!("Expected FormElicitationParam");
}
let json_data_url = json!({
"_meta": { "meta_url_key_1": "meta url value 1" },
"mode": "url",
"message": "Please fill out the form at the following URL.",
"url": "https://example.com/form",
"elicitationId": "elicitation-123"
});
let elicitation_url: CreateElicitationRequestParams =
serde_json::from_value(json_data_url).expect("Deserialization failed");
if let CreateElicitationRequestParams::UrlElicitationParams {
meta,
message,
url,
elicitation_id,
} = elicitation_url
{
assert_eq!(
meta,
Some(Meta(object!({ "meta_url_key_1": "meta url value 1" })))
);
assert_eq!(message, "Please fill out the form at the following URL.");
assert_eq!(url, "https://example.com/form");
assert_eq!(elicitation_id, "elicitation-123");
} else {
panic!("Expected UrlElicitationParam");
}
}
#[test]
fn test_elicitation_serialization() {
let form_elicitation = CreateElicitationRequestParams::FormElicitationParams {
meta: Some(Meta(object!({ "meta_form_key_1": "meta form value 1" }))),
message: "Please provide more details.".to_string(),
requested_schema: ElicitationSchema::builder()
.title("User Details")
.string_property("name", |s| s)
.build()
.expect("Valid schema"),
};
let json_form = serde_json::to_value(&form_elicitation).expect("Serialization failed");
let expected_form_json = json!({
"_meta": { "meta_form_key_1": "meta form value 1" },
"mode": "form",
"message": "Please provide more details.",
"requestedSchema": {
"title":"User Details",
"type":"object",
"properties":{
"name": { "type": "string" },
},
}
});
assert_eq!(json_form, expected_form_json);
let url_elicitation = CreateElicitationRequestParams::UrlElicitationParams {
meta: Some(Meta(object!({ "meta_url_key_1": "meta url value 1" }))),
message: "Please fill out the form at the following URL.".to_string(),
url: "https://example.com/form".to_string(),
elicitation_id: "elicitation-123".to_string(),
};
let json_url = serde_json::to_value(&url_elicitation).expect("Serialization failed");
let expected_url_json = json!({
"_meta": { "meta_url_key_1": "meta url value 1" },
"mode": "url",
"message": "Please fill out the form at the following URL.",
"url": "https://example.com/form",
"elicitationId": "elicitation-123"
});
assert_eq!(json_url, expected_url_json);
}
}