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 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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>,
}

///
/// # 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