Skip to content

feat: Add MCP Elicitation support #332

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4acf13c
feat: implement MCP elicitation support for interactive user input
bug-ops Jul 23, 2025
ce4a03a
feat: add typed elicitation API with enhanced error handling
bug-ops Jul 23, 2025
e1a995c
fix: correct elicitation direction to comply with MCP 2025-06-18
bug-ops Jul 23, 2025
174af7e
feat: add elicitation capability checking for server methods
bug-ops Jul 23, 2025
f4819a5
fix: json rpc message schema
bug-ops Jul 24, 2025
c5fb31c
fix: doc tests
bug-ops Jul 24, 2025
242a8ab
fix: cargo nightly fmt checks
bug-ops Jul 24, 2025
c080d40
fix: clippy
bug-ops Jul 24, 2025
6867d5c
refactor: separate elicitation methods into dedicated impl block for …
bug-ops Jul 24, 2025
01dd3e8
revert: rollback LATEST protocol version to V_2025_03_26
bug-ops Jul 30, 2025
c649de1
fix: remove protocol version assertions
bug-ops Jul 31, 2025
0be52a1
fix: fmt checks
bug-ops Aug 1, 2025
3b18efc
feat: add timeout support for elicitation methods
bug-ops Aug 1, 2025
8296242
feat: add timeout validation to prevent DoS attacks
bug-ops Aug 12, 2025
3d6a9d5
feat: separate UserDeclined and UserCancelled elicitation errors
bug-ops Aug 12, 2025
5be0f43
feat: add compile-time type safety for elicitation methods
bug-ops Aug 12, 2025
edc5bed
Revert "feat: add timeout validation to prevent DoS attacks"
bug-ops Aug 12, 2025
a8c9b5e
fix: correct doctest example in elicit_safe macro documentation
bug-ops Aug 12, 2025
3c98177
refactor: remove redundant elicitation direction tests
bug-ops Aug 14, 2025
fd54781
feat: add elicitation example with user name collection
bug-ops Aug 16, 2025
a7211f3
Merge branch 'main' into feature/elicitation
bug-ops Aug 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions crates/rmcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ default = ["base64", "macros", "server"]
client = ["dep:tokio-stream"]
server = ["transport-async-rw", "dep:schemars"]
macros = ["dep:rmcp-macros", "dep:paste"]
elicitation = []

# reqwest http client
__reqwest = ["dep:reqwest"]
Expand Down Expand Up @@ -201,3 +202,8 @@ path = "tests/test_message_schema.rs"
name = "test_progress_subscriber"
required-features = ["server", "client", "macros"]
path = "tests/test_progress_subscriber.rs"

[[test]]
name = "test_elicitation"
required-features = ["elicitation", "client", "server"]
path = "tests/test_elicitation.rs"
33 changes: 33 additions & 0 deletions crates/rmcp/src/handler/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ impl<H: ClientHandler> Service<RoleClient> for H {
.list_roots(context)
.await
.map(ClientResult::ListRootsResult),
ServerRequest::CreateElicitationRequest(request) => self
.create_elicitation(request.params, context)
.await
.map(ClientResult::CreateElicitationResult),
}
}

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

/// Handle an elicitation request from a server asking for user input.
///
/// This method is called when a server needs interactive input from the user
/// during tool execution. Implementations should present the message to the user,
/// collect their input according to the requested schema, and return the result.
///
/// # Arguments
/// * `request` - The elicitation request with message and schema
/// * `context` - The request context
///
/// # Returns
/// The user's response including action (accept/decline/cancel) and optional data
///
/// # Default Behavior
/// The default implementation automatically declines all elicitation requests.
/// Real clients should override this to provide user interaction.
fn create_elicitation(
&self,
request: CreateElicitationRequestParam,
context: RequestContext<RoleClient>,
) -> impl Future<Output = Result<CreateElicitationResult, McpError>> + Send + '_ {
// Default implementation declines all requests - real clients should override this
let _ = (request, context);
std::future::ready(Ok(CreateElicitationResult {
action: ElicitationAction::Decline,
content: None,
}))
}

fn on_cancelled(
&self,
params: CancelledNotificationParam,
Expand Down
77 changes: 75 additions & 2 deletions crates/rmcp/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ impl std::fmt::Display for ProtocolVersion {
}

impl ProtocolVersion {
pub const V_2025_06_18: Self = Self(Cow::Borrowed("2025-06-18"));
pub const V_2025_03_26: Self = Self(Cow::Borrowed("2025-03-26"));
pub const V_2024_11_05: Self = Self(Cow::Borrowed("2024-11-05"));
pub const LATEST: Self = Self::V_2025_03_26;
Expand All @@ -167,6 +168,7 @@ impl<'de> Deserialize<'de> for ProtocolVersion {
match s.as_str() {
"2024-11-05" => return Ok(ProtocolVersion::V_2024_11_05),
"2025-03-26" => return Ok(ProtocolVersion::V_2025_03_26),
"2025-06-18" => return Ok(ProtocolVersion::V_2025_06_18),
_ => {}
}
Ok(ProtocolVersion(Cow::Owned(s)))
Expand Down Expand Up @@ -1173,6 +1175,75 @@ pub struct ListRootsResult {
const_string!(RootsListChangedNotificationMethod = "notifications/roots/list_changed");
pub type RootsListChangedNotification = NotificationNoParam<RootsListChangedNotificationMethod>;

// =============================================================================
// ELICITATION (INTERACTIVE USER INPUT)
// =============================================================================

// Method constants for elicitation operations.
// Elicitation allows servers to request interactive input from users during tool execution.
const_string!(ElicitationCreateRequestMethod = "elicitation/create");
const_string!(ElicitationResponseNotificationMethod = "notifications/elicitation/response");

/// Represents the possible actions a user can take in response to an elicitation request.
///
/// When a server requests user input through elicitation, the user can:
/// - Accept: Provide the requested information and continue
/// - Decline: Refuse to provide the information but continue the operation
/// - Cancel: Stop the entire operation
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum ElicitationAction {
/// User accepts the request and provides the requested information
Accept,
/// User declines to provide the information but allows the operation to continue
Decline,
/// User cancels the entire operation
Cancel,
}

/// Parameters for creating an elicitation request to gather user input.
///
/// This structure contains everything needed to request interactive input from a user:
/// - A human-readable message explaining what information is needed
/// - A JSON schema defining the expected structure of the response
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CreateElicitationRequestParam {
/// 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,

/// JSON Schema defining the expected structure and validation rules for the user's response.
/// This allows clients to validate input and provide appropriate UI controls.
/// Must be a valid JSON Schema Draft 2020-12 object.
pub requested_schema: JsonObject,
}

/// The result returned by a client in response to an elicitation request.
///
/// Contains the user's decision (accept/decline/cancel) and optionally their input data
/// if they chose to accept the request.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CreateElicitationResult {
/// The user's decision on how to handle the elicitation request
pub action: ElicitationAction,

/// The actual data provided by the user, if they accepted the request.
/// Must conform to the JSON schema specified in the original request.
/// Only present when action is Accept.
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<Value>,
}

/// Request type for creating an elicitation to gather user input
pub type CreateElicitationRequest =
Request<ElicitationCreateRequestMethod, CreateElicitationRequestParam>;

// =============================================================================
// TOOL EXECUTION RESULTS
// =============================================================================
Expand Down Expand Up @@ -1315,7 +1386,7 @@ ts_union!(
);

ts_union!(
export type ClientResult = CreateMessageResult | ListRootsResult | EmptyResult;
export type ClientResult = CreateMessageResult | ListRootsResult | CreateElicitationResult | EmptyResult;
);

impl ClientResult {
Expand All @@ -1330,7 +1401,8 @@ ts_union!(
export type ServerRequest =
| PingRequest
| CreateMessageRequest
| ListRootsRequest;
| ListRootsRequest
| CreateElicitationRequest;
);

ts_union!(
Expand All @@ -1355,6 +1427,7 @@ ts_union!(
| ReadResourceResult
| CallToolResult
| ListToolsResult
| CreateElicitationResult
| EmptyResult
;
);
Expand Down
35 changes: 35 additions & 0 deletions crates/rmcp/src/model/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ pub struct RootsCapabilities {
pub list_changed: Option<bool>,
}

/// 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.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ElicitationCapability {
/// 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>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

didn't see this option in the spec -- what's the motivation for adding it?

only related bit I found in the spec was

Both parties SHOULD validate elicitation content against the provided schema

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @jamadeo, the optional nature of schema_validation is actually aligned with how the MCP specification uses the SHOULD keyword. According to RFC 2119, SHOULD means:

"there may exist valid reasons in particular circumstances to ignore a particular item, but the full implications must be understood and carefully weighed before choosing a different course."

The MCP 2025-06-18 specification states:

"Both parties SHOULD validate elicitation content against the provided schema"

This deliberate use of SHOULD (rather than MUST) suggests the protocol designers anticipated scenarios where validation might be bypassed for valid reasons.
Valid use cases for disabling schema validation:

  • Performance-critical environments
  • High-throughput scenarios
  • Legacy integrations where validation happens elsewhere in the stack
  • Trusted internal networks with simplified security models
  • Progressive enhancement for clients with limited JSON Schema support

However, the implementation should:

  • Default to true (following the SHOULD recommendation)
  • Clearly document the security and reliability implications
  • Ensure servers can handle potentially invalid data gracefully
  • Provide explicit opt-out rather than accidental disabling

This approach balances protocol compliance with real-world flexibility, which seems to align with MCP's pragmatic design philosophy.
What do you think? Does this rationale make sense for the optional capability?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think that's the right interpretation of should vs must. Mostly I just meant is it valid to put it in the capabilities object itself. (I'm not familiar enough with what's permitted there.)

}

///
/// # Builder
/// ```rust
Expand All @@ -58,6 +74,9 @@ pub struct ClientCapabilities {
pub roots: Option<RootsCapabilities>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sampling: Option<JsonObject>,
/// Capability to handle elicitation requests from servers for interactive user input
#[serde(skip_serializing_if = "Option::is_none")]
pub elicitation: Option<ElicitationCapability>,
}

///
Expand Down Expand Up @@ -252,6 +271,7 @@ builder! {
experimental: ExperimentalCapabilities,
roots: RootsCapabilities,
sampling: JsonObject,
elicitation: ElicitationCapability,
}
}

Expand All @@ -266,6 +286,21 @@ impl<const E: bool, const S: bool>
}
}

#[cfg(feature = "elicitation")]
impl<const E: bool, const R: bool, const S: bool>
ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, R, S, true>>
{
/// Enable JSON Schema validation for elicitation responses.
/// When enabled, the client will validate user input against the requested_schema
/// before sending responses back to the server.
pub fn enable_elicitation_schema_validation(mut self) -> Self {
if let Some(c) = self.elicitation.as_mut() {
c.schema_validation = Some(true);
}
self
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
1 change: 1 addition & 0 deletions crates/rmcp/src/model/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ variant_extension! {
PingRequest
CreateMessageRequest
ListRootsRequest
CreateElicitationRequest
}
}

Expand Down
Loading