Skip to content

Commit 4acf13c

Browse files
committed
feat: implement MCP elicitation support for interactive user input
Adds comprehensive elicitation functionality according to MCP 2025-06-18 specification: Core Features: - ElicitationAction enum (Accept, Decline, Cancel) - CreateElicitationRequestParam and CreateElicitationResult structures - Protocol version V_2025_06_18 with elicitation methods - Full JSON-RPC integration with method constants Capabilities Integration: - ElicitationCapability with schema validation support - ClientCapabilities builder pattern integration - enable_elicitation() and enable_elicitation_schema_validation() methods Handler Support: - create_elicitation method in ClientHandler and ServerHandler traits - Integration with existing request/response union types - Async/await compatible implementation Service Layer: - Basic create_elicitation method via macro expansion - Four convenience methods for common scenarios: * elicit_confirmation() - yes/no questions * elicit_text_input() - string input with optional requirements * elicit_choice() - selection from multiple options * elicit_structured_input() - complex data via JSON Schema Comprehensive Testing: - 11 test cases covering all functionality aspects - JSON serialization/deserialization validation - MCP specification compliance verification - Error handling and edge cases - Performance benchmarks - Capabilities integration tests All tests pass and code follows project standards.
1 parent ac40e00 commit 4acf13c

File tree

7 files changed

+893
-4
lines changed

7 files changed

+893
-4
lines changed

crates/rmcp/src/handler/client.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ impl<H: ClientHandler> Service<RoleClient> for H {
2121
.list_roots(context)
2222
.await
2323
.map(ClientResult::ListRootsResult),
24+
ServerRequest::CreateElicitationRequest(request) => self
25+
.create_elicitation(request.params, context)
26+
.await
27+
.map(ClientResult::CreateElicitationResult),
2428
}
2529
}
2630

@@ -86,6 +90,35 @@ pub trait ClientHandler: Sized + Send + Sync + 'static {
8690
std::future::ready(Ok(ListRootsResult::default()))
8791
}
8892

93+
/// Handle an elicitation request from a server asking for user input.
94+
///
95+
/// This method is called when a server needs interactive input from the user
96+
/// during tool execution. Implementations should present the message to the user,
97+
/// collect their input according to the requested schema, and return the result.
98+
///
99+
/// # Arguments
100+
/// * `request` - The elicitation request with message and schema
101+
/// * `context` - The request context
102+
///
103+
/// # Returns
104+
/// The user's response including action (accept/decline/cancel) and optional data
105+
///
106+
/// # Default Behavior
107+
/// The default implementation automatically declines all elicitation requests.
108+
/// Real clients should override this to provide user interaction.
109+
fn create_elicitation(
110+
&self,
111+
request: CreateElicitationRequestParam,
112+
context: RequestContext<RoleClient>,
113+
) -> impl Future<Output = Result<CreateElicitationResult, McpError>> + Send + '_ {
114+
// Default implementation declines all requests - real clients should override this
115+
let _ = (request, context);
116+
std::future::ready(Ok(CreateElicitationResult {
117+
action: ElicitationAction::Decline,
118+
content: None,
119+
}))
120+
}
121+
89122
fn on_cancelled(
90123
&self,
91124
params: CancelledNotificationParam,

crates/rmcp/src/handler/server.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ impl<H: ServerHandler> Service<RoleServer> for H {
6666
.list_tools(request.params, context)
6767
.await
6868
.map(ServerResult::ListToolsResult),
69+
ClientRequest::CreateElicitationRequest(request) => self
70+
.create_elicitation(request.params, context)
71+
.await
72+
.map(ServerResult::CreateElicitationResult),
6973
}
7074
}
7175

@@ -195,6 +199,31 @@ pub trait ServerHandler: Sized + Send + Sync + 'static {
195199
std::future::ready(Ok(ListToolsResult::default()))
196200
}
197201

202+
/// Handle an elicitation request to gather interactive user input.
203+
///
204+
/// This method is typically implemented by server applications that need to
205+
/// request information from users during tool execution. The default implementation
206+
/// declines all elicitation requests.
207+
///
208+
/// # Arguments
209+
/// * `request` - The elicitation request parameters containing the message and schema
210+
/// * `context` - The request context for this elicitation
211+
///
212+
/// # Returns
213+
/// A result containing the user's response (accept/decline/cancel) and optional data
214+
fn create_elicitation(
215+
&self,
216+
request: CreateElicitationRequestParam,
217+
context: RequestContext<RoleServer>,
218+
) -> impl Future<Output = Result<CreateElicitationResult, McpError>> + Send + '_ {
219+
// Default implementation declines all elicitation requests
220+
let _ = (request, context);
221+
std::future::ready(Ok(CreateElicitationResult {
222+
action: ElicitationAction::Decline,
223+
content: None,
224+
}))
225+
}
226+
198227
fn on_cancelled(
199228
&self,
200229
notification: CancelledNotificationParam,

crates/rmcp/src/model.rs

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,10 @@ impl std::fmt::Display for ProtocolVersion {
143143
}
144144

145145
impl ProtocolVersion {
146+
pub const V_2025_06_18: Self = Self(Cow::Borrowed("2025-06-18"));
146147
pub const V_2025_03_26: Self = Self(Cow::Borrowed("2025-03-26"));
147148
pub const V_2024_11_05: Self = Self(Cow::Borrowed("2024-11-05"));
148-
pub const LATEST: Self = Self::V_2025_03_26;
149+
pub const LATEST: Self = Self::V_2025_06_18;
149150
}
150151

151152
impl Serialize for ProtocolVersion {
@@ -167,6 +168,7 @@ impl<'de> Deserialize<'de> for ProtocolVersion {
167168
match s.as_str() {
168169
"2024-11-05" => return Ok(ProtocolVersion::V_2024_11_05),
169170
"2025-03-26" => return Ok(ProtocolVersion::V_2025_03_26),
171+
"2025-06-18" => return Ok(ProtocolVersion::V_2025_06_18),
170172
_ => {}
171173
}
172174
Ok(ProtocolVersion(Cow::Owned(s)))
@@ -1173,6 +1175,75 @@ pub struct ListRootsResult {
11731175
const_string!(RootsListChangedNotificationMethod = "notifications/roots/list_changed");
11741176
pub type RootsListChangedNotification = NotificationNoParam<RootsListChangedNotificationMethod>;
11751177

1178+
// =============================================================================
1179+
// ELICITATION (INTERACTIVE USER INPUT)
1180+
// =============================================================================
1181+
1182+
// Method constants for elicitation operations.
1183+
// Elicitation allows servers to request interactive input from users during tool execution.
1184+
const_string!(ElicitationCreateRequestMethod = "elicitation/create");
1185+
const_string!(ElicitationResponseNotificationMethod = "notifications/elicitation/response");
1186+
1187+
/// Represents the possible actions a user can take in response to an elicitation request.
1188+
///
1189+
/// When a server requests user input through elicitation, the user can:
1190+
/// - Accept: Provide the requested information and continue
1191+
/// - Decline: Refuse to provide the information but continue the operation
1192+
/// - Cancel: Stop the entire operation
1193+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
1194+
#[serde(rename_all = "lowercase")]
1195+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1196+
pub enum ElicitationAction {
1197+
/// User accepts the request and provides the requested information
1198+
Accept,
1199+
/// User declines to provide the information but allows the operation to continue
1200+
Decline,
1201+
/// User cancels the entire operation
1202+
Cancel,
1203+
}
1204+
1205+
/// Parameters for creating an elicitation request to gather user input.
1206+
///
1207+
/// This structure contains everything needed to request interactive input from a user:
1208+
/// - A human-readable message explaining what information is needed
1209+
/// - A JSON schema defining the expected structure of the response
1210+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1211+
#[serde(rename_all = "camelCase")]
1212+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1213+
pub struct CreateElicitationRequestParam {
1214+
/// Human-readable message explaining what input is needed from the user.
1215+
/// This should be clear and provide sufficient context for the user to understand
1216+
/// what information they need to provide.
1217+
pub message: String,
1218+
1219+
/// JSON Schema defining the expected structure and validation rules for the user's response.
1220+
/// This allows clients to validate input and provide appropriate UI controls.
1221+
/// Must be a valid JSON Schema Draft 2020-12 object.
1222+
pub requested_schema: JsonObject,
1223+
}
1224+
1225+
/// The result returned by a client in response to an elicitation request.
1226+
///
1227+
/// Contains the user's decision (accept/decline/cancel) and optionally their input data
1228+
/// if they chose to accept the request.
1229+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1230+
#[serde(rename_all = "camelCase")]
1231+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1232+
pub struct CreateElicitationResult {
1233+
/// The user's decision on how to handle the elicitation request
1234+
pub action: ElicitationAction,
1235+
1236+
/// The actual data provided by the user, if they accepted the request.
1237+
/// Must conform to the JSON schema specified in the original request.
1238+
/// Only present when action is Accept.
1239+
#[serde(skip_serializing_if = "Option::is_none")]
1240+
pub content: Option<Value>,
1241+
}
1242+
1243+
/// Request type for creating an elicitation to gather user input
1244+
pub type CreateElicitationRequest =
1245+
Request<ElicitationCreateRequestMethod, CreateElicitationRequestParam>;
1246+
11761247
// =============================================================================
11771248
// TOOL EXECUTION RESULTS
11781249
// =============================================================================
@@ -1303,7 +1374,8 @@ ts_union!(
13031374
| SubscribeRequest
13041375
| UnsubscribeRequest
13051376
| CallToolRequest
1306-
| ListToolsRequest;
1377+
| ListToolsRequest
1378+
| CreateElicitationRequest;
13071379
);
13081380

13091381
ts_union!(
@@ -1315,7 +1387,7 @@ ts_union!(
13151387
);
13161388

13171389
ts_union!(
1318-
export type ClientResult = CreateMessageResult | ListRootsResult | EmptyResult;
1390+
export type ClientResult = CreateMessageResult | ListRootsResult | CreateElicitationResult | EmptyResult;
13191391
);
13201392

13211393
impl ClientResult {
@@ -1330,7 +1402,8 @@ ts_union!(
13301402
export type ServerRequest =
13311403
| PingRequest
13321404
| CreateMessageRequest
1333-
| ListRootsRequest;
1405+
| ListRootsRequest
1406+
| CreateElicitationRequest;
13341407
);
13351408

13361409
ts_union!(
@@ -1355,6 +1428,7 @@ ts_union!(
13551428
| ReadResourceResult
13561429
| CallToolResult
13571430
| ListToolsResult
1431+
| CreateElicitationResult
13581432
| EmptyResult
13591433
;
13601434
);

crates/rmcp/src/model/capabilities.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,22 @@ pub struct RootsCapabilities {
3939
pub list_changed: Option<bool>,
4040
}
4141

42+
/// Capability for handling elicitation requests from servers.
43+
///
44+
/// Elicitation allows servers to request interactive input from users during tool execution.
45+
/// This capability indicates that a client can handle elicitation requests and present
46+
/// appropriate UI to users for collecting the requested information.
47+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
48+
#[serde(rename_all = "camelCase")]
49+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
50+
pub struct ElicitationCapability {
51+
/// Whether the client supports JSON Schema validation for elicitation responses.
52+
/// When true, the client will validate user input against the requested_schema
53+
/// before sending the response back to the server.
54+
#[serde(skip_serializing_if = "Option::is_none")]
55+
pub schema_validation: Option<bool>,
56+
}
57+
4258
///
4359
/// # Builder
4460
/// ```rust
@@ -58,6 +74,9 @@ pub struct ClientCapabilities {
5874
pub roots: Option<RootsCapabilities>,
5975
#[serde(skip_serializing_if = "Option::is_none")]
6076
pub sampling: Option<JsonObject>,
77+
/// Capability to handle elicitation requests from servers for interactive user input
78+
#[serde(skip_serializing_if = "Option::is_none")]
79+
pub elicitation: Option<ElicitationCapability>,
6180
}
6281

6382
///
@@ -252,6 +271,7 @@ builder! {
252271
experimental: ExperimentalCapabilities,
253272
roots: RootsCapabilities,
254273
sampling: JsonObject,
274+
elicitation: ElicitationCapability,
255275
}
256276
}
257277

@@ -266,6 +286,20 @@ impl<const E: bool, const S: bool>
266286
}
267287
}
268288

289+
impl<const E: bool, const R: bool, const S: bool>
290+
ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, R, S, true>>
291+
{
292+
/// Enable JSON Schema validation for elicitation responses.
293+
/// When enabled, the client will validate user input against the requested_schema
294+
/// before sending responses back to the server.
295+
pub fn enable_elicitation_schema_validation(mut self) -> Self {
296+
if let Some(c) = self.elicitation.as_mut() {
297+
c.schema_validation = Some(true);
298+
}
299+
self
300+
}
301+
}
302+
269303
#[cfg(test)]
270304
mod test {
271305
use super::*;

crates/rmcp/src/model/meta.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ variant_extension! {
6666
UnsubscribeRequest
6767
CallToolRequest
6868
ListToolsRequest
69+
CreateElicitationRequest
6970
}
7071
}
7172

@@ -74,6 +75,7 @@ variant_extension! {
7475
PingRequest
7576
CreateMessageRequest
7677
ListRootsRequest
78+
CreateElicitationRequest
7779
}
7880
}
7981

0 commit comments

Comments
 (0)