Skip to content

Commit ffbb19f

Browse files
committed
feat(elicitation): add support URL elicitation. SEP-1036
1 parent e9029cc commit ffbb19f

File tree

12 files changed

+1400
-256
lines changed

12 files changed

+1400
-256
lines changed

crates/rmcp/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ default = ["base64", "macros", "server"]
7979
client = ["dep:tokio-stream"]
8080
server = ["transport-async-rw", "dep:schemars"]
8181
macros = ["dep:rmcp-macros", "dep:pastey"]
82-
elicitation = []
82+
elicitation = ["dep:url"]
8383

8484
# reqwest http client
8585
__reqwest = ["dep:reqwest"]

crates/rmcp/src/handler/client.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ impl<H: ClientHandler> Service<RoleClient> for H {
6060
ServerNotification::PromptListChangedNotification(_notification_no_param) => {
6161
self.on_prompt_list_changed(context).await
6262
}
63+
ServerNotification::ElicitationCompletionNotification(notification) => {
64+
self.on_url_elicitation_notification_complete(notification.params, context)
65+
.await
66+
}
6367
ServerNotification::CustomNotification(notification) => {
6468
self.on_custom_notification(notification, context).await
6569
}
@@ -187,6 +191,14 @@ pub trait ClientHandler: Sized + Send + Sync + 'static {
187191
) -> impl Future<Output = ()> + Send + '_ {
188192
std::future::ready(())
189193
}
194+
195+
fn on_url_elicitation_notification_complete(
196+
&self,
197+
params: ElicitationResponseNotificationParam,
198+
context: NotificationContext<RoleClient>,
199+
) -> impl Future<Output = ()> + Send + '_ {
200+
std::future::ready(())
201+
}
190202
fn on_custom_notification(
191203
&self,
192204
notification: CustomNotification,

crates/rmcp/src/model.rs

Lines changed: 237 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,7 @@ impl ErrorCode {
453453
pub const INVALID_PARAMS: Self = Self(-32602);
454454
pub const INTERNAL_ERROR: Self = Self(-32603);
455455
pub const PARSE_ERROR: Self = Self(-32700);
456+
pub const URL_ELICITATION_REQUIRED: Self = Self(-32042);
456457
}
457458

458459
/// Error information for JSON-RPC error responses.
@@ -504,6 +505,12 @@ impl ErrorData {
504505
pub fn internal_error(message: impl Into<Cow<'static, str>>, data: Option<Value>) -> Self {
505506
Self::new(ErrorCode::INTERNAL_ERROR, message, data)
506507
}
508+
pub fn url_elicitation_required(
509+
message: impl Into<Cow<'static, str>>,
510+
data: Option<Value>,
511+
) -> Self {
512+
Self::new(ErrorCode::URL_ELICITATION_REQUIRED, message, data)
513+
}
507514
}
508515

509516
/// Represents any JSON-RPC message that can be sent or received.
@@ -1447,6 +1454,7 @@ pub type RootsListChangedNotification = NotificationNoParam<RootsListChangedNoti
14471454
// Elicitation allows servers to request interactive input from users during tool execution.
14481455
const_string!(ElicitationCreateRequestMethod = "elicitation/create");
14491456
const_string!(ElicitationResponseNotificationMethod = "notifications/elicitation/response");
1457+
const_string!(ElicitationCompletionNotificationMethod = "notifications/elicitation/complete");
14501458

14511459
/// Represents the possible actions a user can take in response to an elicitation request.
14521460
///
@@ -1466,38 +1474,121 @@ pub enum ElicitationAction {
14661474
Cancel,
14671475
}
14681476

1477+
/// Helper enum for deserializing CreateElicitationRequestParam with backward compatibility.
1478+
/// When mode is missing, it defaults to FormElicitationParam.
1479+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1480+
#[serde(tag = "mode")]
1481+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1482+
enum CreateElicitationRequestParamDeserializeHelper {
1483+
#[serde(rename = "form", rename_all = "camelCase")]
1484+
FormElicitationParam {
1485+
message: String,
1486+
requested_schema: ElicitationSchema,
1487+
},
1488+
#[serde(rename = "url", rename_all = "camelCase")]
1489+
UrlElicitationParam {
1490+
message: String,
1491+
url: String,
1492+
elicitation_id: String,
1493+
},
1494+
#[serde(untagged, rename_all = "camelCase")]
1495+
FormElicitationParamBackwardsCompat {
1496+
message: String,
1497+
requested_schema: ElicitationSchema,
1498+
},
1499+
}
1500+
1501+
impl TryFrom<CreateElicitationRequestParamDeserializeHelper> for CreateElicitationRequestParam {
1502+
type Error = serde_json::Error;
1503+
1504+
fn try_from(
1505+
value: CreateElicitationRequestParamDeserializeHelper,
1506+
) -> Result<Self, Self::Error> {
1507+
match value {
1508+
CreateElicitationRequestParamDeserializeHelper::FormElicitationParam {
1509+
message,
1510+
requested_schema,
1511+
}
1512+
| CreateElicitationRequestParamDeserializeHelper::FormElicitationParamBackwardsCompat {
1513+
message,
1514+
requested_schema,
1515+
} => Ok(CreateElicitationRequestParam::FormElicitationParam {
1516+
message,
1517+
requested_schema,
1518+
}),
1519+
CreateElicitationRequestParamDeserializeHelper::UrlElicitationParam {
1520+
message,
1521+
url,
1522+
elicitation_id,
1523+
} => Ok(CreateElicitationRequestParam::UrlElicitationParam {
1524+
message,
1525+
url,
1526+
elicitation_id,
1527+
}),
1528+
}
1529+
}
1530+
}
1531+
14691532
/// Parameters for creating an elicitation request to gather user input.
14701533
///
14711534
/// This structure contains everything needed to request interactive input from a user:
14721535
/// - A human-readable message explaining what information is needed
14731536
/// - A type-safe schema defining the expected structure of the response
14741537
///
14751538
/// # Example
1476-
///
1539+
/// 1. Form-based elicitation request
14771540
/// ```rust
14781541
/// use rmcp::model::*;
14791542
///
1480-
/// let params = CreateElicitationRequestParam {
1543+
/// let params = CreateElicitationRequestParam::FormElicitationParam {
14811544
/// message: "Please provide your email".to_string(),
14821545
/// requested_schema: ElicitationSchema::builder()
14831546
/// .required_email("email")
14841547
/// .build()
14851548
/// .unwrap(),
14861549
/// };
14871550
/// ```
1551+
/// 2. URL-based elicitation request
1552+
/// ```rust
1553+
/// use rmcp::model::*;
1554+
/// let params = CreateElicitationRequestParam::UrlElicitationParam {
1555+
/// message: "Please provide your feedback at the following URL".to_string(),
1556+
/// url: "https://example.com/feedback".to_string(),
1557+
/// elicitation_id: "unique-id-123".to_string(),
1558+
/// };
1559+
/// ```
14881560
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1489-
#[serde(rename_all = "camelCase")]
1561+
#[serde(
1562+
tag = "mode",
1563+
try_from = "CreateElicitationRequestParamDeserializeHelper"
1564+
)]
14901565
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1491-
pub struct CreateElicitationRequestParam {
1492-
/// Human-readable message explaining what input is needed from the user.
1493-
/// This should be clear and provide sufficient context for the user to understand
1494-
/// what information they need to provide.
1495-
pub message: String,
1496-
1497-
/// Type-safe schema defining the expected structure and validation rules for the user's response.
1498-
/// This enforces the MCP 2025-06-18 specification that elicitation schemas must be objects
1499-
/// with primitive-typed properties.
1500-
pub requested_schema: ElicitationSchema,
1566+
pub enum CreateElicitationRequestParam {
1567+
#[serde(rename = "form", rename_all = "camelCase")]
1568+
FormElicitationParam {
1569+
/// Human-readable message explaining what input is needed from the user.
1570+
/// This should be clear and provide sufficient context for the user to understand
1571+
/// what information they need to provide.
1572+
message: String,
1573+
1574+
/// Type-safe schema defining the expected structure and validation rules for the user's response.
1575+
/// This enforces the MCP 2025-06-18 specification that elicitation schemas must be objects
1576+
/// with primitive-typed properties.
1577+
requested_schema: ElicitationSchema,
1578+
},
1579+
#[serde(rename = "url", rename_all = "camelCase")]
1580+
UrlElicitationParam {
1581+
/// Human-readable message explaining what input is needed from the user.
1582+
/// This should be clear and provide sufficient context for the user to understand
1583+
/// what information they need to provide.
1584+
message: String,
1585+
1586+
/// The URL where the user can provide the requested information.
1587+
/// The client should direct the user to this URL to complete the elicitation.
1588+
url: String,
1589+
/// The unique identifier for this elicitation request.
1590+
elicitation_id: String,
1591+
},
15011592
}
15021593

15031594
/// The result returned by a client in response to an elicitation request.
@@ -1522,6 +1613,18 @@ pub struct CreateElicitationResult {
15221613
pub type CreateElicitationRequest =
15231614
Request<ElicitationCreateRequestMethod, CreateElicitationRequestParam>;
15241615

1616+
/// Notification parameters for an url elicitation completion notification.
1617+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1618+
#[serde(rename_all = "camelCase")]
1619+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1620+
pub struct ElicitationResponseNotificationParam {
1621+
pub elicitation_id: String,
1622+
}
1623+
1624+
/// Notification sent when an url elicitation process is completed.
1625+
pub type ElicitationCompletionNotification =
1626+
Notification<ElicitationCompletionNotificationMethod, ElicitationResponseNotificationParam>;
1627+
15251628
// =============================================================================
15261629
// TOOL EXECUTION RESULTS
15271630
// =============================================================================
@@ -1945,6 +2048,7 @@ ts_union!(
19452048
| ResourceListChangedNotification
19462049
| ToolListChangedNotification
19472050
| PromptListChangedNotification
2051+
| ElicitationCompletionNotification
19482052
| CustomNotification;
19492053
);
19502054

@@ -2446,4 +2550,124 @@ mod tests {
24462550
assert_eq!(json["serverInfo"]["icons"][0]["sizes"][0], "48x48");
24472551
assert_eq!(json["serverInfo"]["websiteUrl"], "https://docs.example.com");
24482552
}
2553+
2554+
#[test]
2555+
fn test_elicitation_deserialization_untagged() {
2556+
// Test deserialization without the "type" field (should default to FormElicitationParam)
2557+
let json_data_without_tag = json!({
2558+
"message": "Please provide more details.",
2559+
"requestedSchema": {
2560+
"title": "User Details",
2561+
"type": "object",
2562+
"properties": {
2563+
"name": { "type": "string" },
2564+
"age": { "type": "integer" }
2565+
},
2566+
"required": ["name", "age"]
2567+
}
2568+
});
2569+
let elicitation: CreateElicitationRequestParam =
2570+
serde_json::from_value(json_data_without_tag).expect("Deserialization failed");
2571+
if let CreateElicitationRequestParam::FormElicitationParam {
2572+
message,
2573+
requested_schema,
2574+
} = elicitation
2575+
{
2576+
assert_eq!(message, "Please provide more details.");
2577+
assert_eq!(requested_schema.title, Some(Cow::from("User Details")));
2578+
assert_eq!(requested_schema.type_, ObjectTypeConst);
2579+
} else {
2580+
panic!("Expected FormElicitationParam");
2581+
}
2582+
}
2583+
2584+
#[test]
2585+
fn test_elicitation_deserialization() {
2586+
let json_data_form = json!({
2587+
"mode": "form",
2588+
"message": "Please provide more details.",
2589+
"requestedSchema": {
2590+
"title": "User Details",
2591+
"type": "object",
2592+
"properties": {
2593+
"name": { "type": "string" },
2594+
"age": { "type": "integer" }
2595+
},
2596+
"required": ["name", "age"]
2597+
}
2598+
});
2599+
let elicitation_form: CreateElicitationRequestParam =
2600+
serde_json::from_value(json_data_form).expect("Deserialization failed");
2601+
if let CreateElicitationRequestParam::FormElicitationParam {
2602+
message,
2603+
requested_schema,
2604+
} = elicitation_form
2605+
{
2606+
assert_eq!(message, "Please provide more details.");
2607+
assert_eq!(requested_schema.title, Some(Cow::from("User Details")));
2608+
assert_eq!(requested_schema.type_, ObjectTypeConst);
2609+
} else {
2610+
panic!("Expected FormElicitationParam");
2611+
}
2612+
2613+
let json_data_url = json!({
2614+
"mode": "url",
2615+
"message": "Please fill out the form at the following URL.",
2616+
"url": "https://example.com/form",
2617+
"elicitationId": "elicitation-123"
2618+
});
2619+
let elicitation_url: CreateElicitationRequestParam =
2620+
serde_json::from_value(json_data_url).expect("Deserialization failed");
2621+
if let CreateElicitationRequestParam::UrlElicitationParam {
2622+
message,
2623+
url,
2624+
elicitation_id,
2625+
} = elicitation_url
2626+
{
2627+
assert_eq!(message, "Please fill out the form at the following URL.");
2628+
assert_eq!(url, "https://example.com/form");
2629+
assert_eq!(elicitation_id, "elicitation-123");
2630+
} else {
2631+
panic!("Expected UrlElicitationParam");
2632+
}
2633+
}
2634+
2635+
#[test]
2636+
fn test_elicitation_serialization() {
2637+
let form_elicitation = CreateElicitationRequestParam::FormElicitationParam {
2638+
message: "Please provide more details.".to_string(),
2639+
requested_schema: ElicitationSchema::builder()
2640+
.title("User Details")
2641+
.string_property("name", |s| s)
2642+
.build()
2643+
.expect("Valid schema"),
2644+
};
2645+
let json_form = serde_json::to_value(&form_elicitation).expect("Serialization failed");
2646+
let expected_form_json = json!({
2647+
"mode": "form",
2648+
"message": "Please provide more details.",
2649+
"requestedSchema": {
2650+
"title":"User Details",
2651+
"type":"object",
2652+
"properties":{
2653+
"name": { "type": "string" },
2654+
},
2655+
}
2656+
});
2657+
assert_eq!(json_form, expected_form_json);
2658+
2659+
let url_elicitation = CreateElicitationRequestParam::UrlElicitationParam {
2660+
message: "Please fill out the form at the following URL.".to_string(),
2661+
url: "https://example.com/form".to_string(),
2662+
elicitation_id: "elicitation-123".to_string(),
2663+
};
2664+
let json_url = serde_json::to_value(&url_elicitation).expect("Serialization failed");
2665+
let expected_url_json = json!({
2666+
"mode": "url",
2667+
"message": "Please fill out the form at the following URL.",
2668+
"url": "https://example.com/form",
2669+
"elicitationId": "elicitation-123"
2670+
});
2671+
assert_eq!(json_url, expected_url_json);
2672+
}
24492673
}

0 commit comments

Comments
 (0)