// Sampling/Roots/Logging are SEP-2577-deprecated; internal references are expected. #![expect(deprecated)] #[cfg(feature = "elicitation")] use std::collections::HashSet; use std::{borrow::Cow, sync::Arc}; use thiserror::Error; #[cfg(feature = "elicitation")] use url::Url; use super::*; #[cfg(feature = "elicitation")] use crate::model::{ElicitRequest, ElicitRequestParams, ElicitResult, ElicitationAction}; use crate::{ model::{ CancelledNotification, CancelledNotificationParam, ClientInfo, ClientJsonRpcMessage, ClientNotification, ClientRequest, ClientResult, CreateMessageRequest, CreateMessageRequestParams, CreateMessageResult, EmptyResult, ErrorData, ListRootsRequest, ListRootsResult, LoggingMessageNotification, LoggingMessageNotificationParam, ProgressNotification, ProgressNotificationParam, PromptListChangedNotification, ProtocolVersion, ResourceListChangedNotification, ResourceUpdatedNotification, ResourceUpdatedNotificationParam, ServerInfo, ServerNotification, ServerRequest, ServerResult, SubscriptionFilter, SubscriptionsAcknowledgedNotification, SubscriptionsAcknowledgedNotificationParams, ToolListChangedNotification, }, transport::DynamicTransportError, }; #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] #[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")] pub struct RoleServer; impl ServiceRole for RoleServer { type Req = ServerRequest; type Resp = ServerResult; type Not = ServerNotification; type PeerReq = ClientRequest; type PeerResp = ClientResult; type PeerNot = ClientNotification; type Info = ServerInfo; type PeerInfo = ClientInfo; type InitializeError = ServerInitializeError; const IS_CLIENT: bool = false; fn peer_cancelled_params(notification: &Self::PeerNot) -> Option<&CancelledNotificationParam> { match notification { ClientNotification::CancelledNotification(notification) => Some(¬ification.params), _ => None, } } fn enforce_request_association( request: &Self::Req, peer_info: Option<&Self::PeerInfo>, in_request_handler_scope: bool, ) -> Result<(), ServiceError> { let restricted = matches!( request, ServerRequest::CreateMessageRequest(_) | ServerRequest::ListRootsRequest(_) | ServerRequest::ElicitRequest(_) ); if !restricted { return Ok(()); } let strict = peer_info.is_some_and(|info| info.protocol_version >= ProtocolVersion::V_2026_07_28); if strict && !in_request_handler_scope { return Err(ServiceError::McpError(ErrorData::invalid_request( "SEP-2260: server-to-client requests must be associated with an originating client request", None, ))); } Ok(()) } } /// It represents the error that may occur when serving the server. /// /// if you want to handle the error, you can use `serve_server_with_ct` or `serve_server` with `Result, ServerError>` #[derive(Error, Debug)] #[non_exhaustive] pub enum ServerInitializeError { #[error("expect initialized request, but received: {0:?}")] ExpectedInitializeRequest(Option), #[deprecated( since = "1.4.0", note = "The server no longer gates on the initialized notification. This variant is never constructed and will be removed in a future major release." )] #[error("expect initialized notification, but received: {0:?}")] ExpectedInitializedNotification(Option), #[error("connection closed: {0}")] ConnectionClosed(String), #[error("unexpected initialize result: {0:?}")] UnexpectedInitializeResponse(ServerResult), #[error("initialize failed: {0}")] InitializeFailed(ErrorData), #[deprecated( since = "1.8.0", note = "Negotiation now falls back to the server-configured version. This variant is never constructed and will be removed in a future major release." )] #[error("unsupported protocol version: {0}")] UnsupportedProtocolVersion(ProtocolVersion), #[error("Send message error {error}, when {context}")] TransportError { error: DynamicTransportError, context: Cow<'static, str>, }, #[error("Cancelled")] Cancelled, } impl ServerInitializeError { pub fn transport + 'static>( error: T::Error, context: impl Into>, ) -> Self { Self::TransportError { error: DynamicTransportError::new::(error), context: context.into(), } } } pub type ClientSink = Peer; /// Failure to send a notification through a [`SubscriptionSink`]. #[derive(Debug, Error)] #[non_exhaustive] pub enum SubscriptionSendError { #[error("subscription is no longer active")] SubscriptionClosed, #[error("notification is not allowed on a subscription stream: {0}")] UnsupportedNotification(&'static str), #[error("notification was not accepted for this subscription: {0}")] NotificationNotAccepted(&'static str), #[error(transparent)] Service(#[from] ServiceError), } /// A server-side notification sink scoped to one `subscriptions/listen` request. /// /// The sink applies the accepted filter and adds the subscription request ID to /// every notification it sends. #[derive(Debug, Clone)] pub struct SubscriptionSink { peer: Peer, id: RequestId, accepted: Arc, active: CancellationToken, } impl SubscriptionSink { fn new( peer: Peer, id: RequestId, accepted: Arc, active: CancellationToken, ) -> Self { Self { peer, id, accepted, active, } } /// Return the JSON-RPC ID of the originating listen request. pub fn id(&self) -> &RequestId { &self.id } /// Return the filter accepted for this subscription. pub fn accepted(&self) -> &SubscriptionFilter { self.accepted.as_ref() } /// Send an allowed change notification with subscription metadata attached. /// /// # Errors /// /// Returns [`SubscriptionSendError::SubscriptionClosed`] after the request /// ends, a filter error for disallowed notifications, or a transport error. pub async fn send( &self, mut notification: ServerNotification, ) -> Result<(), SubscriptionSendError> { if self.active.is_cancelled() { return Err(SubscriptionSendError::SubscriptionClosed); } match ¬ification { ServerNotification::ToolListChangedNotification(_) => { if self.accepted.tools_list_changed != Some(true) { return Err(SubscriptionSendError::NotificationNotAccepted( "notifications/tools/list_changed", )); } } ServerNotification::PromptListChangedNotification(_) => { if self.accepted.prompts_list_changed != Some(true) { return Err(SubscriptionSendError::NotificationNotAccepted( "notifications/prompts/list_changed", )); } } ServerNotification::ResourceListChangedNotification(_) => { if self.accepted.resources_list_changed != Some(true) { return Err(SubscriptionSendError::NotificationNotAccepted( "notifications/resources/list_changed", )); } } ServerNotification::ResourceUpdatedNotification(update) => { let accepted = self .accepted .resource_subscriptions .as_ref() .is_some_and(|uris| uris.contains(&update.params.uri)); if !accepted { return Err(SubscriptionSendError::NotificationNotAccepted( "notifications/resources/updated", )); } } ServerNotification::SubscriptionsAcknowledgedNotification(_) => { return Err(SubscriptionSendError::UnsupportedNotification( "notifications/subscriptions/acknowledged", )); } ServerNotification::CancelledNotification(_) => { return Err(SubscriptionSendError::UnsupportedNotification( "notifications/cancelled", )); } ServerNotification::ProgressNotification(_) => { return Err(SubscriptionSendError::UnsupportedNotification( "notifications/progress", )); } ServerNotification::LoggingMessageNotification(_) => { return Err(SubscriptionSendError::UnsupportedNotification( "notifications/message", )); } // SEP-2663 task status notifications are not yet routable through // `subscriptions/listen`: `SubscriptionFilter` has no `taskIds` // field yet (the upstream conformance check for this flow is also // still skipped, pending the subscriptions/listen rewrite). // Clients currently observe task state by polling `tasks/get`. ServerNotification::TaskStatusNotification(_) => { return Err(SubscriptionSendError::UnsupportedNotification( "notifications/tasks", )); } ServerNotification::CustomNotification(_) => { return Err(SubscriptionSendError::UnsupportedNotification( "custom notification", )); } } notification .get_meta_mut() .set_subscription_id(self.id.clone()); self.peer.send_notification(notification).await?; Ok(()) } /// Send `notifications/tools/list_changed`. /// /// # Errors /// /// See [`Self::send`]. pub async fn notify_tool_list_changed(&self) -> Result<(), SubscriptionSendError> { self.send(ServerNotification::ToolListChangedNotification( ToolListChangedNotification { method: Default::default(), extensions: Default::default(), }, )) .await } /// Send `notifications/prompts/list_changed`. /// /// # Errors /// /// See [`Self::send`]. pub async fn notify_prompt_list_changed(&self) -> Result<(), SubscriptionSendError> { self.send(ServerNotification::PromptListChangedNotification( PromptListChangedNotification { method: Default::default(), extensions: Default::default(), }, )) .await } /// Send `notifications/resources/list_changed`. /// /// # Errors /// /// See [`Self::send`]. pub async fn notify_resource_list_changed(&self) -> Result<(), SubscriptionSendError> { self.send(ServerNotification::ResourceListChangedNotification( ResourceListChangedNotification { method: Default::default(), extensions: Default::default(), }, )) .await } /// Send `notifications/resources/updated` for an accepted URI. /// /// # Errors /// /// See [`Self::send`]. pub async fn notify_resource_updated( &self, uri: impl Into, ) -> Result<(), SubscriptionSendError> { self.send(ServerNotification::ResourceUpdatedNotification( ResourceUpdatedNotification::new(ResourceUpdatedNotificationParam::new(uri)), )) .await } } /// Context for one established server-side notification subscription. /// /// The acknowledgment has already been sent before this context is handed to /// [`ServerHandler::listen`](crate::ServerHandler::listen). #[derive(Debug)] pub struct SubscriptionContext { request: RequestContext, requested: SubscriptionFilter, accepted: Arc, sink: SubscriptionSink, _active_guard: DropGuard, } impl SubscriptionContext { pub(crate) async fn establish( request: RequestContext, requested: SubscriptionFilter, accepted: SubscriptionFilter, ) -> Result { let active = request.ct.child_token(); let accepted = Arc::new(accepted); let sink = SubscriptionSink::new( request.peer.clone(), request.id.clone(), accepted.clone(), active.clone(), ); let mut acknowledgment = SubscriptionsAcknowledgedNotification::new( SubscriptionsAcknowledgedNotificationParams::new(accepted.as_ref().clone()), ); let mut meta = NotificationMetaObject::new(); meta.set_subscription_id(request.id.clone()); acknowledgment.extensions.insert(meta); request .peer .send_notification(ServerNotification::SubscriptionsAcknowledgedNotification( acknowledgment, )) .await .map_err(|error| { ErrorData::internal_error( format!("failed to acknowledge subscription: {error}"), None, ) })?; Ok(Self { request, requested, accepted, sink, _active_guard: active.drop_guard(), }) } /// Return the filter requested by the client. pub fn requested(&self) -> &SubscriptionFilter { &self.requested } /// Return the subset accepted by the server. pub fn accepted(&self) -> &SubscriptionFilter { self.accepted.as_ref() } /// Return a cloneable, filter-enforcing notification sink. pub fn sink(&self) -> &SubscriptionSink { &self.sink } /// Wait until the subscription request is cancelled. pub async fn cancelled(&self) { self.request.ct.cancelled().await; } /// Access the underlying request context. pub fn request_context(&self) -> &RequestContext { &self.request } } impl> ServiceExt for S { fn serve_with_ct( self, transport: T, ct: CancellationToken, ) -> impl Future, ServerInitializeError>> + MaybeSendFuture where T: IntoTransport, E: std::error::Error + Send + Sync + 'static, Self: Sized, { serve_server_with_ct(self, transport, ct) } } pub async fn serve_server( service: S, transport: T, ) -> Result, ServerInitializeError> where S: Service, T: IntoTransport, E: std::error::Error + Send + Sync + 'static, { serve_server_with_ct(service, transport, CancellationToken::new()).await } /// Helper function to get the next message from the stream async fn expect_next_message( transport: &mut T, context: &str, ) -> Result where T: Transport, { transport .receive() .await .ok_or_else(|| ServerInitializeError::ConnectionClosed(context.to_string())) } pub async fn serve_server_with_ct( service: S, transport: T, ct: CancellationToken, ) -> Result, ServerInitializeError> where S: Service, T: IntoTransport, E: std::error::Error + Send + Sync + 'static, { tokio::select! { result = serve_server_with_ct_inner(service, transport.into_transport(), ct.clone()) => { result } _ = ct.cancelled() => { Err(ServerInitializeError::Cancelled) } } } /// Echoes the client-requested version if known; otherwise returns `server_fallback`. pub(crate) fn negotiate_protocol_version( client_requested: &ProtocolVersion, server_fallback: ProtocolVersion, ) -> ProtocolVersion { if ProtocolVersion::KNOWN_VERSIONS.contains(client_requested) { client_requested.clone() } else { tracing::warn!( client_requested = %client_requested, server_fallback = %server_fallback, "client requested unsupported protocol version; falling back to server default" ); server_fallback } } async fn serve_server_with_ct_inner( service: S, transport: T, ct: CancellationToken, ) -> Result, ServerInitializeError> where S: Service, T: Transport + 'static, { let mut transport = transport.into_transport(); let id_provider = >::default(); // Get initialize request; the MCP spec permits ping before initialize. // See: https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization let (request, id) = loop { let msg = expect_next_message(&mut transport, "initialize request").await?; match msg { ClientJsonRpcMessage::Request(req) if matches!(req.request, ClientRequest::PingRequest(_)) => { transport .send(ServerJsonRpcMessage::response( ServerResult::EmptyResult(EmptyResult {}), req.id, )) .await .map_err(|error| { ServerInitializeError::transport::( error, "sending pre-init ping response", ) })?; } ClientJsonRpcMessage::Request(req) => break (req.request, req.id), other => { return Err(ServerInitializeError::ExpectedInitializeRequest(Some( other, ))); } } }; let initialize_request = match request { ClientRequest::InitializeRequest(request) => request, mut request => { if !request .get_meta() .missing_required_keys(&ProtocolVersion::V_2026_07_28) .is_empty() { return Err(ServerInitializeError::ExpectedInitializeRequest(Some( ClientJsonRpcMessage::request(request, id), ))); } let (peer, peer_rx) = Peer::new(id_provider, None); peer.require_request_metadata(); let context = RequestContext { ct: ct.child_token(), id: id.clone(), meta: std::mem::take(request.get_meta_mut()), extensions: std::mem::take(request.extensions_mut()), peer: peer.clone(), }; let response = match service.handle_request(request, context).await { Ok(result) => ServerJsonRpcMessage::response(result, id), Err(error) => ServerJsonRpcMessage::error(error, Some(id)), }; transport.send(response).await.map_err(|error| { ServerInitializeError::transport::(error, "sending negotiated request response") })?; return Ok(serve_inner(service, transport, peer, peer_rx, ct)); } }; let requested_protocol_version = initialize_request.params.protocol_version.clone(); let mut negotiated_peer_info = initialize_request.params.clone(); let (peer, peer_rx) = Peer::new(id_provider, Some(negotiated_peer_info.clone())); let request = ClientRequest::InitializeRequest(initialize_request); let context = RequestContext { ct: ct.child_token(), id: id.clone(), meta: request.get_meta().clone(), extensions: request.extensions().clone(), peer: peer.clone(), }; // Send initialize response let init_response = service.handle_request(request, context).await; let mut init_response = match init_response { Ok(ServerResult::InitializeResult(init_response)) => init_response, Ok(result) => { return Err(ServerInitializeError::UnexpectedInitializeResponse(result)); } Err(e) => { transport .send(ServerJsonRpcMessage::error(e.clone(), Some(id))) .await .map_err(|error| { ServerInitializeError::transport::(error, "sending error response") })?; return Err(ServerInitializeError::InitializeFailed(e)); } }; init_response.protocol_version = negotiate_protocol_version(&requested_protocol_version, init_response.protocol_version); // Update peer_info so context.protocol_version() reflects the negotiated // version in all subsequent request handlers. negotiated_peer_info.protocol_version = init_response.protocol_version.clone(); peer.set_peer_info(negotiated_peer_info); transport .send(ServerJsonRpcMessage::response( ServerResult::InitializeResult(init_response), id, )) .await .map_err(|error| { ServerInitializeError::transport::(error, "sending initialize response") })?; // Enter the main service loop immediately after sending InitializeResult. // The initialized notification will be handled as a regular notification by serve_inner. // This matches the TypeScript SDK behavior: no init gate, no waiting for initialized. // Streamable HTTP has no ordering guarantee between POSTs, and the MCP spec uses // SHOULD NOT (not MUST NOT) for pre-initialized messages, so any request arriving // before initialized is processed normally. Ok(serve_inner(service, transport, peer, peer_rx, ct)) } macro_rules! method { ($(#[$meta:meta])* peer_req $method:ident $Req:ident() => $Resp: ident ) => { $(#[$meta])* pub async fn $method(&self) -> Result<$Resp, ServiceError> { let result = self .send_request(ServerRequest::$Req($Req { method: Default::default(), extensions: Default::default(), })) .await?; match result { ClientResult::$Resp(result) => Ok(result), _ => Err(ServiceError::UnexpectedResponse), } } }; ($(#[$meta:meta])* peer_req $method:ident $Req:ident($Param: ident) => $Resp: ident ) => { $(#[$meta])* pub async fn $method(&self, params: $Param) -> Result<$Resp, ServiceError> { let result = self .send_request(ServerRequest::$Req($Req { method: Default::default(), params, extensions: Default::default(), })) .await?; match result { ClientResult::$Resp(result) => Ok(result), _ => Err(ServiceError::UnexpectedResponse), } } }; ($(#[$meta:meta])* peer_req $method:ident $Req:ident($Param: ident)) => { $(#[$meta])* pub fn $method( &self, params: $Param, ) -> impl Future> + Send + '_ { async move { let result = self .send_request(ServerRequest::$Req($Req { method: Default::default(), params, })) .await?; match result { ClientResult::EmptyResult(_) => Ok(()), _ => Err(ServiceError::UnexpectedResponse), } } } }; ($(#[$meta:meta])* peer_not $method:ident $Not:ident($Param: ident)) => { $(#[$meta])* pub async fn $method(&self, params: $Param) -> Result<(), ServiceError> { self.send_notification(ServerNotification::$Not($Not { method: Default::default(), params, extensions: Default::default(), })) .await?; Ok(()) } }; ($(#[$meta:meta])* peer_not $method:ident $Not:ident) => { $(#[$meta])* pub async fn $method(&self) -> Result<(), ServiceError> { self.send_notification(ServerNotification::$Not($Not { method: Default::default(), extensions: Default::default(), })) .await?; Ok(()) } }; // Timeout-only variants (base method should be created separately with peer_req) ($(#[$meta:meta])* peer_req_with_timeout $method_with_timeout:ident $Req:ident() => $Resp: ident) => { $(#[$meta])* pub async fn $method_with_timeout( &self, timeout: Option, ) -> Result<$Resp, ServiceError> { let request = ServerRequest::$Req($Req { method: Default::default(), extensions: Default::default(), }); let options = crate::service::PeerRequestOptions { timeout, meta: None, reset_timeout_on_progress: false, max_total_timeout: None, }; let result = self .send_request_with_option(request, options) .await? .await_response() .await?; match result { ClientResult::$Resp(result) => Ok(result), _ => Err(ServiceError::UnexpectedResponse), } } }; ($(#[$meta:meta])* peer_req_with_timeout $method_with_timeout:ident $Req:ident($Param: ident) => $Resp: ident) => { $(#[$meta])* pub async fn $method_with_timeout( &self, params: $Param, timeout: Option, ) -> Result<$Resp, ServiceError> { let request = ServerRequest::$Req($Req { method: Default::default(), params, extensions: Default::default(), }); let options = crate::service::PeerRequestOptions { timeout, meta: None, reset_timeout_on_progress: false, max_total_timeout: None, }; let result = self .send_request_with_option(request, options) .await? .await_response() .await?; match result { ClientResult::$Resp(result) => Ok(result), _ => Err(ServiceError::UnexpectedResponse), } } }; } impl Peer { /// Check if the client supports sampling tools capability. pub fn supports_sampling_tools(&self) -> bool { if let Some(client_info) = self.peer_info() { client_info .capabilities .sampling .as_ref() .and_then(|s| s.tools.as_ref()) .is_some() } else { false } } /// # SEP-2260: request association /// /// From protocol version `2026-07-28` this must be issued while handling a /// client request; see [`OriginatingRequestId`]. #[deprecated( since = "1.8.0", note = "Sampling is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577" )] pub async fn create_message( &self, params: CreateMessageRequestParams, ) -> Result { // MUST throw error when tools/toolChoice provided without capability if (params.tools.is_some() || params.tool_choice.is_some()) && !self.supports_sampling_tools() { return Err(ServiceError::McpError(ErrorData::invalid_params( "tools or toolChoice provided but client does not support sampling tools capability", None, ))); } // Validate message structure params .validate() .map_err(|e| ServiceError::McpError(ErrorData::invalid_params(e, None)))?; let result = self .send_request(ServerRequest::CreateMessageRequest(CreateMessageRequest { method: Default::default(), params, extensions: Default::default(), })) .await?; match result { ClientResult::CreateMessageResult(result) => Ok(*result), _ => Err(ServiceError::UnexpectedResponse), } } method!( /// # SEP-2260: request association /// /// From protocol version `2026-07-28` this must be issued while handling a /// client request; see [`OriginatingRequestId`]. #[deprecated( since = "1.8.0", note = "Roots is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577" )] peer_req list_roots ListRootsRequest() => ListRootsResult ); #[cfg(feature = "elicitation")] method!( /// # SEP-2260: request association /// /// From protocol version `2026-07-28` this must be issued while handling a /// client request; see [`OriginatingRequestId`]. peer_req create_elicitation ElicitRequest(ElicitRequestParams) => ElicitResult ); #[cfg(feature = "elicitation")] method!( /// # SEP-2260: request association /// /// From protocol version `2026-07-28` this must be issued while handling a /// client request; see [`OriginatingRequestId`]. peer_req_with_timeout create_elicitation_with_timeout ElicitRequest(ElicitRequestParams) => ElicitResult ); method!(peer_not notify_cancelled CancelledNotification(CancelledNotificationParam)); method!(peer_not notify_progress ProgressNotification(ProgressNotificationParam)); method!( #[deprecated( since = "1.8.0", note = "Logging is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577" )] peer_not notify_logging_message LoggingMessageNotification(LoggingMessageNotificationParam) ); method!(peer_not notify_resource_updated ResourceUpdatedNotification(ResourceUpdatedNotificationParam)); method!(peer_not notify_resource_list_changed ResourceListChangedNotification); method!(peer_not notify_tool_list_changed ToolListChangedNotification); method!(peer_not notify_prompt_list_changed PromptListChangedNotification); } // ============================================================================= // ELICITATION CONVENIENCE METHODS // These methods are specific to server role and provide typed elicitation functionality // ============================================================================= /// Errors that can occur during typed elicitation operations #[cfg(feature = "elicitation")] #[derive(Error, Debug)] #[non_exhaustive] pub enum ElicitationError { /// The elicitation request failed at the service level #[error("Service error: {0}")] Service(#[from] ServiceError), /// User explicitly declined to provide the requested information /// This indicates a conscious decision by the user to reject the request /// (e.g., clicked "Reject", "Decline", "No", etc.) #[error("User explicitly declined the request")] UserDeclined, /// User dismissed the request without making an explicit choice /// This indicates the user cancelled without explicitly declining /// (e.g., closed dialog, clicked outside, pressed Escape, etc.) #[error("User cancelled/dismissed the request")] UserCancelled, /// The response data could not be parsed into the requested type #[error("Failed to parse response data: {error}\nReceived data: {data}")] ParseError { error: serde_json::Error, data: serde_json::Value, }, /// No response content was provided by the user #[error("No response content provided")] NoContent, /// Client does not support elicitation capability #[error("Client does not support elicitation - capability not declared during initialization")] CapabilityNotSupported, } /// Marker trait to ensure that elicitation types generate object-type JSON schemas. /// /// This trait provides compile-time safety to ensure that types used with /// `elicit()` methods will generate JSON schemas of type "object", which /// aligns with MCP client expectations for structured data input. /// /// # Type Safety Rationale /// /// MCP clients typically expect JSON objects for elicitation schemas to /// provide structured forms and validation. This trait prevents common /// mistakes like: /// /// ```compile_fail /// // These would not compile due to missing ElicitationSafe bound: /// let name: String = server.elicit("Enter name").await?; // Primitive /// let items: Vec = server.elicit("Enter items").await?; // Array /// ``` #[cfg(feature = "elicitation")] pub trait ElicitationSafe: schemars::JsonSchema {} /// Macro to mark types as safe for elicitation by verifying they generate object schemas. /// /// This macro automatically implements the `ElicitationSafe` trait for struct types /// that should be used with `elicit()` methods. /// /// # Example /// /// ```rust /// use rmcp::elicit_safe; /// use schemars::JsonSchema; /// use serde::{Deserialize, Serialize}; /// /// #[derive(Serialize, Deserialize, JsonSchema)] /// struct UserProfile { /// name: String, /// email: String, /// } /// /// elicit_safe!(UserProfile); /// /// // Now safe to use in async context: /// // let profile: UserProfile = server.elicit("Enter profile").await?; /// ``` #[cfg(feature = "elicitation")] #[macro_export] macro_rules! elicit_safe { ($($t:ty),* $(,)?) => { $( impl $crate::service::ElicitationSafe for $t {} )* }; } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum ElicitationMode { Form, Url, } #[cfg(feature = "elicitation")] impl Peer { /// Check if the client supports elicitation capability /// /// Returns true if the client declared elicitation capability during initialization, /// false otherwise. According to MCP 2025-06-18 specification, clients that support /// elicitation MUST declare the capability during initialization. pub fn supported_elicitation_modes(&self) -> HashSet { if let Some(client_info) = self.peer_info() { if let Some(elicit_capability) = &client_info.capabilities.elicitation { let mut modes = HashSet::new(); // Backward compatibility: if neither form nor url is specified, assume form if elicit_capability.form.is_none() && elicit_capability.url.is_none() { modes.insert(ElicitationMode::Form); } else { if elicit_capability.form.is_some() { modes.insert(ElicitationMode::Form); } if elicit_capability.url.is_some() { modes.insert(ElicitationMode::Url); } } modes } else { HashSet::new() } } else { HashSet::new() } } /// Request typed data from the user with automatic schema generation. /// /// This method automatically generates the JSON schema from the Rust type using `schemars`, /// eliminating the need to manually create schemas. The response is automatically parsed /// into the requested type. /// /// **Requires the `elicitation` feature to be enabled.** /// /// # Type Requirements /// The type `T` must implement: /// - `schemars::JsonSchema` - for automatic schema generation /// - `serde::Deserialize` - for parsing the response /// /// # Arguments /// * `message` - The prompt message for the user /// /// # Returns /// * `Ok(Some(data))` if user provided valid data that matches type T /// * `Err(ElicitationError::UserDeclined)` if user explicitly declined the request /// * `Err(ElicitationError::UserCancelled)` if user cancelled/dismissed the request /// * `Err(ElicitationError::ParseError { .. })` if response data couldn't be parsed into type T /// * `Err(ElicitationError::NoContent)` if no response content was provided /// * `Err(ElicitationError::Service(_))` if the underlying service call failed /// /// # Example /// /// Add to your `Cargo.toml`: /// ```toml /// [dependencies] /// rmcp = { version = "0.3", features = ["elicitation"] } /// serde = { version = "1.0", features = ["derive"] } /// schemars = "1.0" /// ``` /// /// ```rust,no_run /// # use rmcp::*; /// # use rmcp::service::ElicitationError; /// # use serde::{Deserialize, Serialize}; /// # use schemars::JsonSchema; /// # /// #[derive(Debug, Serialize, Deserialize, JsonSchema)] /// struct UserProfile { /// #[schemars(description = "Full name")] /// name: String, /// #[schemars(description = "Email address")] /// email: String, /// #[schemars(description = "Age")] /// age: u8, /// } /// /// // Mark as safe for elicitation (generates object schema) /// rmcp::elicit_safe!(UserProfile); /// /// # async fn example(peer: Peer) -> Result<(), Box> { /// match peer.elicit::("Please enter your profile information").await { /// Ok(Some(profile)) => { /// println!("Name: {}, Email: {}, Age: {}", profile.name, profile.email, profile.age); /// } /// Ok(None) => { /// println!("User provided no content"); /// } /// Err(ElicitationError::UserDeclined) => { /// println!("User explicitly declined to provide information"); /// // Handle explicit decline - perhaps offer alternatives /// } /// Err(ElicitationError::UserCancelled) => { /// println!("User cancelled the request"); /// // Handle cancellation - perhaps prompt again later /// } /// Err(ElicitationError::ParseError { error, data }) => { /// println!("Failed to parse response: {}\nData: {}", error, data); /// } /// Err(e) => return Err(e.into()), /// } /// # Ok(()) /// # } /// ``` /// /// # SEP-2260: request association /// /// From protocol version `2026-07-28` this must be issued while handling a /// client request; see [`OriginatingRequestId`]. #[cfg(all(feature = "schemars", feature = "elicitation"))] pub async fn elicit(&self, message: impl Into) -> Result, ElicitationError> where T: ElicitationSafe + for<'de> serde::Deserialize<'de>, { self.elicit_with_timeout(message, None).await } /// Request typed data from the user with custom timeout. /// /// Same as `elicit()` but allows specifying a custom timeout for the request. /// If the user doesn't respond within the timeout, the request will be cancelled. /// /// # Arguments /// * `message` - The prompt message for the user /// * `timeout` - Optional timeout duration. If None, uses default timeout behavior /// /// # Returns /// Same as `elicit()` but may also return `ServiceError::Timeout` if timeout expires /// /// # Example /// ```rust,no_run /// # use rmcp::*; /// # use rmcp::service::ElicitationError; /// # use serde::{Deserialize, Serialize}; /// # use schemars::JsonSchema; /// # use std::time::Duration; /// # /// #[derive(Debug, Serialize, Deserialize, JsonSchema)] /// struct QuickResponse { /// answer: String, /// } /// /// // Mark as safe for elicitation /// rmcp::elicit_safe!(QuickResponse); /// /// # async fn example(peer: Peer) -> Result<(), Box> { /// // Give user 30 seconds to respond /// let timeout = Some(Duration::from_secs(30)); /// match peer.elicit_with_timeout::( /// "Quick question - what's your answer?", /// timeout /// ).await { /// Ok(Some(response)) => println!("Got answer: {}", response.answer), /// Ok(None) => println!("User provided no content"), /// Err(ElicitationError::UserDeclined) => { /// println!("User explicitly declined"); /// // Handle explicit decline /// } /// Err(ElicitationError::UserCancelled) => { /// println!("User cancelled/dismissed"); /// // Handle cancellation /// } /// Err(ElicitationError::Service(ServiceError::Timeout { .. })) => { /// println!("User didn't respond in time"); /// } /// Err(e) => return Err(e.into()), /// } /// # Ok(()) /// # } /// ``` /// /// # SEP-2260: request association /// /// From protocol version `2026-07-28` this must be issued while handling a /// client request; see [`OriginatingRequestId`]. #[cfg(all(feature = "schemars", feature = "elicitation"))] pub async fn elicit_with_timeout( &self, message: impl Into, timeout: Option, ) -> Result, ElicitationError> where T: ElicitationSafe + for<'de> serde::Deserialize<'de>, { // Check if client supports form elicitation capability if !self .supported_elicitation_modes() .contains(&ElicitationMode::Form) { return Err(ElicitationError::CapabilityNotSupported); } // Generate schema automatically from type let schema = crate::model::ElicitationSchema::from_type::().map_err(|e| { ElicitationError::Service(ServiceError::McpError(crate::ErrorData::invalid_params( format!( "Invalid schema for type {}: {}", std::any::type_name::(), e ), None, ))) })?; let response = self .create_elicitation_with_timeout( ElicitRequestParams::FormElicitationParams { meta: None, message: message.into(), requested_schema: schema, }, timeout, ) .await?; match response.action { crate::model::ElicitationAction::Accept => { if let Some(value) = response.content { match serde_json::from_value::(value.clone()) { Ok(parsed) => Ok(Some(parsed)), Err(error) => Err(ElicitationError::ParseError { error, data: value }), } } else { Err(ElicitationError::NoContent) } } crate::model::ElicitationAction::Decline => Err(ElicitationError::UserDeclined), crate::model::ElicitationAction::Cancel => Err(ElicitationError::UserCancelled), } } /// Request the user to visit a URL and confirm completion. /// /// This method sends a URL elicitation request to the client, prompting the user /// to visit the specified URL and confirm completion. It returns the user's action /// (accept/decline/cancel) without any additional data. /// **Requires the `elicitation` feature to be enabled.** /// /// # Arguments /// * `message` - The prompt message for the user /// * `url` - The URL the user is requested to visit /// * `elicitation_id` - A unique identifier for this elicitation request /// # Returns /// * `Ok(action)` indicating the user's response action /// * `Err(ElicitationError::CapabilityNotSupported)` if client does not support elicitation via URL /// * `Err(ElicitationError::Service(_))` if the underlying service call failed /// # Example /// ```rust,no_run /// # use rmcp::*; /// # use rmcp::model::ElicitationAction; /// # use url::Url; /// /// async fn example(peer: Peer) -> Result<(), Box> { /// let elicit_result = peer.elicit_url("Please visit the following URL to complete the action", /// Url::parse("https://example.com/complete_action")?, "elicit_123").await?; /// match elicit_result { /// ElicitationAction::Accept => { /// println!("User accepted and confirmed completion"); /// } /// ElicitationAction::Decline => { /// println!("User declined the request"); /// } /// ElicitationAction::Cancel => { /// println!("User cancelled/dismissed the request"); /// } /// _ => {} /// } /// Ok(()) /// } /// ``` /// /// # SEP-2260: request association /// /// From protocol version `2026-07-28` this must be issued while handling a /// client request; see [`OriginatingRequestId`]. #[cfg(feature = "elicitation")] pub async fn elicit_url( &self, message: impl Into, url: impl Into, elicitation_id: impl Into, ) -> Result { self.elicit_url_with_timeout(message, url, elicitation_id, None) .await } /// Request the user to visit a URL and confirm completion. /// /// Same as `elicit_url()` but allows specifying a custom timeout for the request. /// /// # Arguments /// * `message` - The prompt message for the user /// * `url` - The URL the user is requested to visit /// * `elicitation_id` - A unique identifier for this elicitation request /// * `timeout` - Optional timeout duration. If None, uses default timeout behavior /// # Returns /// * `Ok(action)` indicating the user's response action /// * `Err(ElicitationError::CapabilityNotSupported)` if client does not support elicitation via URL /// * `Err(ElicitationError::Service(_))` if the underlying service call failed /// # Example /// ```rust,no_run /// # use std::time::Duration; /// use rmcp::*; /// # use rmcp::model::ElicitationAction; /// # use url::Url; /// /// async fn example(peer: Peer) -> Result<(), Box> { /// let elicit_result = peer.elicit_url_with_timeout("Please visit the following URL to complete the action", /// Url::parse("https://example.com/complete_action")?, /// "elicit_123", /// Some(Duration::from_secs(30))).await?; /// match elicit_result { /// ElicitationAction::Accept => { /// println!("User accepted and confirmed completion"); /// } /// ElicitationAction::Decline => { /// println!("User declined the request"); /// } /// ElicitationAction::Cancel => { /// println!("User cancelled/dismissed the request"); /// } /// _ => {} /// } /// Ok(()) /// } /// ``` /// /// # SEP-2260: request association /// /// From protocol version `2026-07-28` this must be issued while handling a /// client request; see [`OriginatingRequestId`]. #[cfg(feature = "elicitation")] pub async fn elicit_url_with_timeout( &self, message: impl Into, url: impl Into, elicitation_id: impl Into, timeout: Option, ) -> Result { // Check if client supports url elicitation if !self .supported_elicitation_modes() .contains(&ElicitationMode::Url) { return Err(ElicitationError::CapabilityNotSupported); } let action = self .create_elicitation_with_timeout( ElicitRequestParams::UrlElicitationParams { meta: None, message: message.into(), url: url.into().to_string(), elicitation_id: elicitation_id.into(), }, timeout, ) .await? .action; Ok(action) } }