rmcp_soddygo/service/server.rs
1use std::borrow::Cow;
2#[cfg(feature = "elicitation")]
3use std::collections::HashSet;
4
5use thiserror::Error;
6#[cfg(feature = "elicitation")]
7use url::Url;
8
9use super::*;
10#[cfg(feature = "elicitation")]
11use crate::model::{
12 CreateElicitationRequest, CreateElicitationRequestParams, CreateElicitationResult,
13 ElicitationAction, ElicitationCompletionNotification, ElicitationResponseNotificationParam,
14};
15use crate::{
16 model::{
17 CancelledNotification, CancelledNotificationParam, ClientInfo, ClientJsonRpcMessage,
18 ClientNotification, ClientRequest, ClientResult, CreateMessageRequest,
19 CreateMessageRequestParams, CreateMessageResult, EmptyResult, ErrorData, ListRootsRequest,
20 ListRootsResult, LoggingMessageNotification, LoggingMessageNotificationParam,
21 ProgressNotification, ProgressNotificationParam, PromptListChangedNotification,
22 ProtocolVersion, ResourceListChangedNotification, ResourceUpdatedNotification,
23 ResourceUpdatedNotificationParam, ServerInfo, ServerNotification, ServerRequest,
24 ServerResult, ToolListChangedNotification,
25 },
26 transport::DynamicTransportError,
27};
28
29#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
30#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
31pub struct RoleServer;
32
33impl ServiceRole for RoleServer {
34 type Req = ServerRequest;
35 type Resp = ServerResult;
36 type Not = ServerNotification;
37 type PeerReq = ClientRequest;
38 type PeerResp = ClientResult;
39 type PeerNot = ClientNotification;
40 type Info = ServerInfo;
41 type PeerInfo = ClientInfo;
42
43 type InitializeError = ServerInitializeError;
44 const IS_CLIENT: bool = false;
45}
46
47/// It represents the error that may occur when serving the server.
48///
49/// if you want to handle the error, you can use `serve_server_with_ct` or `serve_server` with `Result<RunningService<RoleServer, S>, ServerError>`
50#[derive(Error, Debug)]
51#[non_exhaustive]
52pub enum ServerInitializeError {
53 #[error("expect initialized request, but received: {0:?}")]
54 ExpectedInitializeRequest(Option<ClientJsonRpcMessage>),
55
56 #[deprecated(
57 since = "1.4.0",
58 note = "The server no longer gates on the initialized notification. This variant is never constructed and will be removed in a future major release."
59 )]
60 #[error("expect initialized notification, but received: {0:?}")]
61 ExpectedInitializedNotification(Option<ClientJsonRpcMessage>),
62
63 #[error("connection closed: {0}")]
64 ConnectionClosed(String),
65
66 #[error("unexpected initialize result: {0:?}")]
67 UnexpectedInitializeResponse(ServerResult),
68
69 #[error("initialize failed: {0}")]
70 InitializeFailed(ErrorData),
71
72 #[error("unsupported protocol version: {0}")]
73 UnsupportedProtocolVersion(ProtocolVersion),
74
75 #[error("Send message error {error}, when {context}")]
76 TransportError {
77 error: DynamicTransportError,
78 context: Cow<'static, str>,
79 },
80
81 #[error("Cancelled")]
82 Cancelled,
83}
84
85impl ServerInitializeError {
86 pub fn transport<T: Transport<RoleServer> + 'static>(
87 error: T::Error,
88 context: impl Into<Cow<'static, str>>,
89 ) -> Self {
90 Self::TransportError {
91 error: DynamicTransportError::new::<T, _>(error),
92 context: context.into(),
93 }
94 }
95}
96pub type ClientSink = Peer<RoleServer>;
97
98impl<S: Service<RoleServer>> ServiceExt<RoleServer> for S {
99 fn serve_with_ct<T, E, A>(
100 self,
101 transport: T,
102 ct: CancellationToken,
103 ) -> impl Future<Output = Result<RunningService<RoleServer, Self>, ServerInitializeError>>
104 + MaybeSendFuture
105 where
106 T: IntoTransport<RoleServer, E, A>,
107 E: std::error::Error + Send + Sync + 'static,
108 Self: Sized,
109 {
110 serve_server_with_ct(self, transport, ct)
111 }
112}
113
114pub async fn serve_server<S, T, E, A>(
115 service: S,
116 transport: T,
117) -> Result<RunningService<RoleServer, S>, ServerInitializeError>
118where
119 S: Service<RoleServer>,
120 T: IntoTransport<RoleServer, E, A>,
121 E: std::error::Error + Send + Sync + 'static,
122{
123 serve_server_with_ct(service, transport, CancellationToken::new()).await
124}
125
126/// Helper function to get the next message from the stream
127async fn expect_next_message<T>(
128 transport: &mut T,
129 context: &str,
130) -> Result<ClientJsonRpcMessage, ServerInitializeError>
131where
132 T: Transport<RoleServer>,
133{
134 transport
135 .receive()
136 .await
137 .ok_or_else(|| ServerInitializeError::ConnectionClosed(context.to_string()))
138}
139
140pub async fn serve_server_with_ct<S, T, E, A>(
141 service: S,
142 transport: T,
143 ct: CancellationToken,
144) -> Result<RunningService<RoleServer, S>, ServerInitializeError>
145where
146 S: Service<RoleServer>,
147 T: IntoTransport<RoleServer, E, A>,
148 E: std::error::Error + Send + Sync + 'static,
149{
150 tokio::select! {
151 result = serve_server_with_ct_inner(service, transport.into_transport(), ct.clone()) => { result }
152 _ = ct.cancelled() => {
153 Err(ServerInitializeError::Cancelled)
154 }
155 }
156}
157
158async fn serve_server_with_ct_inner<S, T>(
159 service: S,
160 transport: T,
161 ct: CancellationToken,
162) -> Result<RunningService<RoleServer, S>, ServerInitializeError>
163where
164 S: Service<RoleServer>,
165 T: Transport<RoleServer> + 'static,
166{
167 let mut transport = transport.into_transport();
168 let id_provider = <Arc<AtomicU32RequestIdProvider>>::default();
169
170 // Get initialize request; the MCP spec permits ping before initialize.
171 // See: https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization
172 let (request, id) = loop {
173 let msg = expect_next_message(&mut transport, "initialize request").await?;
174 match msg {
175 ClientJsonRpcMessage::Request(req)
176 if matches!(req.request, ClientRequest::PingRequest(_)) =>
177 {
178 transport
179 .send(ServerJsonRpcMessage::response(
180 ServerResult::EmptyResult(EmptyResult {}),
181 req.id,
182 ))
183 .await
184 .map_err(|error| {
185 ServerInitializeError::transport::<T>(
186 error,
187 "sending pre-init ping response",
188 )
189 })?;
190 }
191 ClientJsonRpcMessage::Request(req) => break (req.request, req.id),
192 other => {
193 return Err(ServerInitializeError::ExpectedInitializeRequest(Some(
194 other,
195 )));
196 }
197 }
198 };
199
200 let ClientRequest::InitializeRequest(peer_info) = &request else {
201 return Err(ServerInitializeError::ExpectedInitializeRequest(Some(
202 ClientJsonRpcMessage::request(request, id),
203 )));
204 };
205 let (peer, peer_rx) = Peer::new(id_provider, Some(peer_info.params.clone()));
206 let context = RequestContext {
207 ct: ct.child_token(),
208 id: id.clone(),
209 meta: request.get_meta().clone(),
210 extensions: request.extensions().clone(),
211 peer: peer.clone(),
212 };
213 // Send initialize response
214 let init_response = service.handle_request(request.clone(), context).await;
215 let mut init_response = match init_response {
216 Ok(ServerResult::InitializeResult(init_response)) => init_response,
217 Ok(result) => {
218 return Err(ServerInitializeError::UnexpectedInitializeResponse(result));
219 }
220 Err(e) => {
221 transport
222 .send(ServerJsonRpcMessage::error(e.clone(), id))
223 .await
224 .map_err(|error| {
225 ServerInitializeError::transport::<T>(error, "sending error response")
226 })?;
227 return Err(ServerInitializeError::InitializeFailed(e));
228 }
229 };
230 let peer_protocol_version = peer_info.params.protocol_version.clone();
231 let protocol_version = match peer_protocol_version
232 .partial_cmp(&init_response.protocol_version)
233 .ok_or(ServerInitializeError::UnsupportedProtocolVersion(
234 peer_protocol_version,
235 ))? {
236 std::cmp::Ordering::Less => peer_info.params.protocol_version.clone(),
237 _ => init_response.protocol_version,
238 };
239 init_response.protocol_version = protocol_version;
240 transport
241 .send(ServerJsonRpcMessage::response(
242 ServerResult::InitializeResult(init_response),
243 id,
244 ))
245 .await
246 .map_err(|error| {
247 ServerInitializeError::transport::<T>(error, "sending initialize response")
248 })?;
249
250 // Enter the main service loop immediately after sending InitializeResult.
251 // The initialized notification will be handled as a regular notification by serve_inner.
252 // This matches the TypeScript SDK behavior: no init gate, no waiting for initialized.
253 // Streamable HTTP has no ordering guarantee between POSTs, and the MCP spec uses
254 // SHOULD NOT (not MUST NOT) for pre-initialized messages, so any request arriving
255 // before initialized is processed normally.
256 Ok(serve_inner(service, transport, peer, peer_rx, ct))
257}
258
259macro_rules! method {
260 (peer_req $method:ident $Req:ident() => $Resp: ident ) => {
261 pub async fn $method(&self) -> Result<$Resp, ServiceError> {
262 let result = self
263 .send_request(ServerRequest::$Req($Req {
264 method: Default::default(),
265 extensions: Default::default(),
266 }))
267 .await?;
268 match result {
269 ClientResult::$Resp(result) => Ok(result),
270 _ => Err(ServiceError::UnexpectedResponse),
271 }
272 }
273 };
274 (peer_req $method:ident $Req:ident($Param: ident) => $Resp: ident ) => {
275 pub async fn $method(&self, params: $Param) -> Result<$Resp, ServiceError> {
276 let result = self
277 .send_request(ServerRequest::$Req($Req {
278 method: Default::default(),
279 params,
280 extensions: Default::default(),
281 }))
282 .await?;
283 match result {
284 ClientResult::$Resp(result) => Ok(result),
285 _ => Err(ServiceError::UnexpectedResponse),
286 }
287 }
288 };
289 (peer_req $method:ident $Req:ident($Param: ident)) => {
290 pub fn $method(
291 &self,
292 params: $Param,
293 ) -> impl Future<Output = Result<(), ServiceError>> + Send + '_ {
294 async move {
295 let result = self
296 .send_request(ServerRequest::$Req($Req {
297 method: Default::default(),
298 params,
299 }))
300 .await?;
301 match result {
302 ClientResult::EmptyResult(_) => Ok(()),
303 _ => Err(ServiceError::UnexpectedResponse),
304 }
305 }
306 }
307 };
308
309 (peer_not $method:ident $Not:ident($Param: ident)) => {
310 pub async fn $method(&self, params: $Param) -> Result<(), ServiceError> {
311 self.send_notification(ServerNotification::$Not($Not {
312 method: Default::default(),
313 params,
314 extensions: Default::default(),
315 }))
316 .await?;
317 Ok(())
318 }
319 };
320 (peer_not $method:ident $Not:ident) => {
321 pub async fn $method(&self) -> Result<(), ServiceError> {
322 self.send_notification(ServerNotification::$Not($Not {
323 method: Default::default(),
324 extensions: Default::default(),
325 }))
326 .await?;
327 Ok(())
328 }
329 };
330
331 // Timeout-only variants (base method should be created separately with peer_req)
332 (peer_req_with_timeout $method_with_timeout:ident $Req:ident() => $Resp: ident) => {
333 pub async fn $method_with_timeout(
334 &self,
335 timeout: Option<std::time::Duration>,
336 ) -> Result<$Resp, ServiceError> {
337 let request = ServerRequest::$Req($Req {
338 method: Default::default(),
339 extensions: Default::default(),
340 });
341 let options = crate::service::PeerRequestOptions {
342 timeout,
343 meta: None,
344 };
345 let result = self
346 .send_request_with_option(request, options)
347 .await?
348 .await_response()
349 .await?;
350 match result {
351 ClientResult::$Resp(result) => Ok(result),
352 _ => Err(ServiceError::UnexpectedResponse),
353 }
354 }
355 };
356
357 (peer_req_with_timeout $method_with_timeout:ident $Req:ident($Param: ident) => $Resp: ident) => {
358 pub async fn $method_with_timeout(
359 &self,
360 params: $Param,
361 timeout: Option<std::time::Duration>,
362 ) -> Result<$Resp, ServiceError> {
363 let request = ServerRequest::$Req($Req {
364 method: Default::default(),
365 params,
366 extensions: Default::default(),
367 });
368 let options = crate::service::PeerRequestOptions {
369 timeout,
370 meta: None,
371 };
372 let result = self
373 .send_request_with_option(request, options)
374 .await?
375 .await_response()
376 .await?;
377 match result {
378 ClientResult::$Resp(result) => Ok(result),
379 _ => Err(ServiceError::UnexpectedResponse),
380 }
381 }
382 };
383}
384
385impl Peer<RoleServer> {
386 /// Check if the client supports sampling tools capability.
387 pub fn supports_sampling_tools(&self) -> bool {
388 if let Some(client_info) = self.peer_info() {
389 client_info
390 .capabilities
391 .sampling
392 .as_ref()
393 .and_then(|s| s.tools.as_ref())
394 .is_some()
395 } else {
396 false
397 }
398 }
399
400 pub async fn create_message(
401 &self,
402 params: CreateMessageRequestParams,
403 ) -> Result<CreateMessageResult, ServiceError> {
404 // MUST throw error when tools/toolChoice provided without capability
405 if (params.tools.is_some() || params.tool_choice.is_some())
406 && !self.supports_sampling_tools()
407 {
408 return Err(ServiceError::McpError(ErrorData::invalid_params(
409 "tools or toolChoice provided but client does not support sampling tools capability",
410 None,
411 )));
412 }
413 // Validate message structure
414 params
415 .validate()
416 .map_err(|e| ServiceError::McpError(ErrorData::invalid_params(e, None)))?;
417 let result = self
418 .send_request(ServerRequest::CreateMessageRequest(CreateMessageRequest {
419 method: Default::default(),
420 params,
421 extensions: Default::default(),
422 }))
423 .await?;
424 match result {
425 ClientResult::CreateMessageResult(result) => Ok(*result),
426 _ => Err(ServiceError::UnexpectedResponse),
427 }
428 }
429 method!(peer_req list_roots ListRootsRequest() => ListRootsResult);
430 #[cfg(feature = "elicitation")]
431 method!(peer_req create_elicitation CreateElicitationRequest(CreateElicitationRequestParams) => CreateElicitationResult);
432 #[cfg(feature = "elicitation")]
433 method!(peer_req_with_timeout create_elicitation_with_timeout CreateElicitationRequest(CreateElicitationRequestParams) => CreateElicitationResult);
434 #[cfg(feature = "elicitation")]
435 method!(peer_not notify_url_elicitation_completed ElicitationCompletionNotification(ElicitationResponseNotificationParam));
436
437 method!(peer_not notify_cancelled CancelledNotification(CancelledNotificationParam));
438 method!(peer_not notify_progress ProgressNotification(ProgressNotificationParam));
439 method!(peer_not notify_logging_message LoggingMessageNotification(LoggingMessageNotificationParam));
440 method!(peer_not notify_resource_updated ResourceUpdatedNotification(ResourceUpdatedNotificationParam));
441 method!(peer_not notify_resource_list_changed ResourceListChangedNotification);
442 method!(peer_not notify_tool_list_changed ToolListChangedNotification);
443 method!(peer_not notify_prompt_list_changed PromptListChangedNotification);
444}
445
446// =============================================================================
447// ELICITATION CONVENIENCE METHODS
448// These methods are specific to server role and provide typed elicitation functionality
449// =============================================================================
450
451/// Errors that can occur during typed elicitation operations
452#[cfg(feature = "elicitation")]
453#[derive(Error, Debug)]
454#[non_exhaustive]
455pub enum ElicitationError {
456 /// The elicitation request failed at the service level
457 #[error("Service error: {0}")]
458 Service(#[from] ServiceError),
459
460 /// User explicitly declined to provide the requested information
461 /// This indicates a conscious decision by the user to reject the request
462 /// (e.g., clicked "Reject", "Decline", "No", etc.)
463 #[error("User explicitly declined the request")]
464 UserDeclined,
465
466 /// User dismissed the request without making an explicit choice
467 /// This indicates the user cancelled without explicitly declining
468 /// (e.g., closed dialog, clicked outside, pressed Escape, etc.)
469 #[error("User cancelled/dismissed the request")]
470 UserCancelled,
471
472 /// The response data could not be parsed into the requested type
473 #[error("Failed to parse response data: {error}\nReceived data: {data}")]
474 ParseError {
475 error: serde_json::Error,
476 data: serde_json::Value,
477 },
478
479 /// No response content was provided by the user
480 #[error("No response content provided")]
481 NoContent,
482
483 /// Client does not support elicitation capability
484 #[error("Client does not support elicitation - capability not declared during initialization")]
485 CapabilityNotSupported,
486}
487
488/// Marker trait to ensure that elicitation types generate object-type JSON schemas.
489///
490/// This trait provides compile-time safety to ensure that types used with
491/// `elicit<T>()` methods will generate JSON schemas of type "object", which
492/// aligns with MCP client expectations for structured data input.
493///
494/// # Type Safety Rationale
495///
496/// MCP clients typically expect JSON objects for elicitation schemas to
497/// provide structured forms and validation. This trait prevents common
498/// mistakes like:
499///
500/// ```compile_fail
501/// // These would not compile due to missing ElicitationSafe bound:
502/// let name: String = server.elicit("Enter name").await?; // Primitive
503/// let items: Vec<i32> = server.elicit("Enter items").await?; // Array
504/// ```
505#[cfg(feature = "elicitation")]
506pub trait ElicitationSafe: schemars::JsonSchema {}
507
508/// Macro to mark types as safe for elicitation by verifying they generate object schemas.
509///
510/// This macro automatically implements the `ElicitationSafe` trait for struct types
511/// that should be used with `elicit<T>()` methods.
512///
513/// # Example
514///
515/// ```rust
516/// use rmcp::elicit_safe;
517/// use schemars::JsonSchema;
518/// use serde::{Deserialize, Serialize};
519///
520/// #[derive(Serialize, Deserialize, JsonSchema)]
521/// struct UserProfile {
522/// name: String,
523/// email: String,
524/// }
525///
526/// elicit_safe!(UserProfile);
527///
528/// // Now safe to use in async context:
529/// // let profile: UserProfile = server.elicit("Enter profile").await?;
530/// ```
531#[cfg(feature = "elicitation")]
532#[macro_export]
533macro_rules! elicit_safe {
534 ($($t:ty),* $(,)?) => {
535 $(
536 impl $crate::service::ElicitationSafe for $t {}
537 )*
538 };
539}
540
541#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
542#[non_exhaustive]
543pub enum ElicitationMode {
544 Form,
545 Url,
546}
547
548#[cfg(feature = "elicitation")]
549impl Peer<RoleServer> {
550 /// Check if the client supports elicitation capability
551 ///
552 /// Returns true if the client declared elicitation capability during initialization,
553 /// false otherwise. According to MCP 2025-06-18 specification, clients that support
554 /// elicitation MUST declare the capability during initialization.
555 pub fn supported_elicitation_modes(&self) -> HashSet<ElicitationMode> {
556 if let Some(client_info) = self.peer_info() {
557 if let Some(elicit_capability) = &client_info.capabilities.elicitation {
558 let mut modes = HashSet::new();
559 // Backward compatibility: if neither form nor url is specified, assume form
560 if elicit_capability.form.is_none() && elicit_capability.url.is_none() {
561 modes.insert(ElicitationMode::Form);
562 } else {
563 if elicit_capability.form.is_some() {
564 modes.insert(ElicitationMode::Form);
565 }
566 if elicit_capability.url.is_some() {
567 modes.insert(ElicitationMode::Url);
568 }
569 }
570 modes
571 } else {
572 HashSet::new()
573 }
574 } else {
575 HashSet::new()
576 }
577 }
578
579 /// Request typed data from the user with automatic schema generation.
580 ///
581 /// This method automatically generates the JSON schema from the Rust type using `schemars`,
582 /// eliminating the need to manually create schemas. The response is automatically parsed
583 /// into the requested type.
584 ///
585 /// **Requires the `elicitation` feature to be enabled.**
586 ///
587 /// # Type Requirements
588 /// The type `T` must implement:
589 /// - `schemars::JsonSchema` - for automatic schema generation
590 /// - `serde::Deserialize` - for parsing the response
591 ///
592 /// # Arguments
593 /// * `message` - The prompt message for the user
594 ///
595 /// # Returns
596 /// * `Ok(Some(data))` if user provided valid data that matches type T
597 /// * `Err(ElicitationError::UserDeclined)` if user explicitly declined the request
598 /// * `Err(ElicitationError::UserCancelled)` if user cancelled/dismissed the request
599 /// * `Err(ElicitationError::ParseError { .. })` if response data couldn't be parsed into type T
600 /// * `Err(ElicitationError::NoContent)` if no response content was provided
601 /// * `Err(ElicitationError::Service(_))` if the underlying service call failed
602 ///
603 /// # Example
604 ///
605 /// Add to your `Cargo.toml`:
606 /// ```toml
607 /// [dependencies]
608 /// rmcp = { version = "0.3", features = ["elicitation"] }
609 /// serde = { version = "1.0", features = ["derive"] }
610 /// schemars = "1.0"
611 /// ```
612 ///
613 /// ```rust,no_run
614 /// # use rmcp::*;
615 /// # use rmcp::service::ElicitationError;
616 /// # use serde::{Deserialize, Serialize};
617 /// # use schemars::JsonSchema;
618 /// #
619 /// #[derive(Debug, Serialize, Deserialize, JsonSchema)]
620 /// struct UserProfile {
621 /// #[schemars(description = "Full name")]
622 /// name: String,
623 /// #[schemars(description = "Email address")]
624 /// email: String,
625 /// #[schemars(description = "Age")]
626 /// age: u8,
627 /// }
628 ///
629 /// // Mark as safe for elicitation (generates object schema)
630 /// rmcp::elicit_safe!(UserProfile);
631 ///
632 /// # async fn example(peer: Peer<RoleServer>) -> Result<(), Box<dyn std::error::Error>> {
633 /// match peer.elicit::<UserProfile>("Please enter your profile information").await {
634 /// Ok(Some(profile)) => {
635 /// println!("Name: {}, Email: {}, Age: {}", profile.name, profile.email, profile.age);
636 /// }
637 /// Ok(None) => {
638 /// println!("User provided no content");
639 /// }
640 /// Err(ElicitationError::UserDeclined) => {
641 /// println!("User explicitly declined to provide information");
642 /// // Handle explicit decline - perhaps offer alternatives
643 /// }
644 /// Err(ElicitationError::UserCancelled) => {
645 /// println!("User cancelled the request");
646 /// // Handle cancellation - perhaps prompt again later
647 /// }
648 /// Err(ElicitationError::ParseError { error, data }) => {
649 /// println!("Failed to parse response: {}\nData: {}", error, data);
650 /// }
651 /// Err(e) => return Err(e.into()),
652 /// }
653 /// # Ok(())
654 /// # }
655 /// ```
656 #[cfg(all(feature = "schemars", feature = "elicitation"))]
657 pub async fn elicit<T>(&self, message: impl Into<String>) -> Result<Option<T>, ElicitationError>
658 where
659 T: ElicitationSafe + for<'de> serde::Deserialize<'de>,
660 {
661 self.elicit_with_timeout(message, None).await
662 }
663
664 /// Request typed data from the user with custom timeout.
665 ///
666 /// Same as `elicit()` but allows specifying a custom timeout for the request.
667 /// If the user doesn't respond within the timeout, the request will be cancelled.
668 ///
669 /// # Arguments
670 /// * `message` - The prompt message for the user
671 /// * `timeout` - Optional timeout duration. If None, uses default timeout behavior
672 ///
673 /// # Returns
674 /// Same as `elicit()` but may also return `ServiceError::Timeout` if timeout expires
675 ///
676 /// # Example
677 /// ```rust,no_run
678 /// # use rmcp::*;
679 /// # use rmcp::service::ElicitationError;
680 /// # use serde::{Deserialize, Serialize};
681 /// # use schemars::JsonSchema;
682 /// # use std::time::Duration;
683 /// #
684 /// #[derive(Debug, Serialize, Deserialize, JsonSchema)]
685 /// struct QuickResponse {
686 /// answer: String,
687 /// }
688 ///
689 /// // Mark as safe for elicitation
690 /// rmcp::elicit_safe!(QuickResponse);
691 ///
692 /// # async fn example(peer: Peer<RoleServer>) -> Result<(), Box<dyn std::error::Error>> {
693 /// // Give user 30 seconds to respond
694 /// let timeout = Some(Duration::from_secs(30));
695 /// match peer.elicit_with_timeout::<QuickResponse>(
696 /// "Quick question - what's your answer?",
697 /// timeout
698 /// ).await {
699 /// Ok(Some(response)) => println!("Got answer: {}", response.answer),
700 /// Ok(None) => println!("User provided no content"),
701 /// Err(ElicitationError::UserDeclined) => {
702 /// println!("User explicitly declined");
703 /// // Handle explicit decline
704 /// }
705 /// Err(ElicitationError::UserCancelled) => {
706 /// println!("User cancelled/dismissed");
707 /// // Handle cancellation
708 /// }
709 /// Err(ElicitationError::Service(ServiceError::Timeout { .. })) => {
710 /// println!("User didn't respond in time");
711 /// }
712 /// Err(e) => return Err(e.into()),
713 /// }
714 /// # Ok(())
715 /// # }
716 /// ```
717 #[cfg(all(feature = "schemars", feature = "elicitation"))]
718 pub async fn elicit_with_timeout<T>(
719 &self,
720 message: impl Into<String>,
721 timeout: Option<std::time::Duration>,
722 ) -> Result<Option<T>, ElicitationError>
723 where
724 T: ElicitationSafe + for<'de> serde::Deserialize<'de>,
725 {
726 // Check if client supports form elicitation capability
727 if !self
728 .supported_elicitation_modes()
729 .contains(&ElicitationMode::Form)
730 {
731 return Err(ElicitationError::CapabilityNotSupported);
732 }
733
734 // Generate schema automatically from type
735 let schema = crate::model::ElicitationSchema::from_type::<T>().map_err(|e| {
736 ElicitationError::Service(ServiceError::McpError(crate::ErrorData::invalid_params(
737 format!(
738 "Invalid schema for type {}: {}",
739 std::any::type_name::<T>(),
740 e
741 ),
742 None,
743 )))
744 })?;
745
746 let response = self
747 .create_elicitation_with_timeout(
748 CreateElicitationRequestParams::FormElicitationParams {
749 meta: None,
750 message: message.into(),
751 requested_schema: schema,
752 },
753 timeout,
754 )
755 .await?;
756
757 match response.action {
758 crate::model::ElicitationAction::Accept => {
759 if let Some(value) = response.content {
760 match serde_json::from_value::<T>(value.clone()) {
761 Ok(parsed) => Ok(Some(parsed)),
762 Err(error) => Err(ElicitationError::ParseError { error, data: value }),
763 }
764 } else {
765 Err(ElicitationError::NoContent)
766 }
767 }
768 crate::model::ElicitationAction::Decline => Err(ElicitationError::UserDeclined),
769 crate::model::ElicitationAction::Cancel => Err(ElicitationError::UserCancelled),
770 }
771 }
772
773 /// Request the user to visit a URL and confirm completion.
774 ///
775 /// This method sends a URL elicitation request to the client, prompting the user
776 /// to visit the specified URL and confirm completion. It returns the user's action
777 /// (accept/decline/cancel) without any additional data.
778 /// **Requires the `elicitation` feature to be enabled.**
779 ///
780 /// # Arguments
781 /// * `message` - The prompt message for the user
782 /// * `url` - The URL the user is requested to visit
783 /// * `elicitation_id` - A unique identifier for this elicitation request
784 /// # Returns
785 /// * `Ok(action)` indicating the user's response action
786 /// * `Err(ElicitationError::CapabilityNotSupported)` if client does not support elicitation via URL
787 /// * `Err(ElicitationError::Service(_))` if the underlying service call failed
788 /// # Example
789 /// ```rust,no_run
790 /// # use rmcp::*;
791 /// # use rmcp::model::ElicitationAction;
792 /// # use url::Url;
793 ///
794 /// async fn example(peer: Peer<RoleServer>) -> Result<(), Box<dyn std::error::Error>> {
795 /// let elicit_result = peer.elicit_url("Please visit the following URL to complete the action",
796 /// Url::parse("https://example.com/complete_action")?, "elicit_123").await?;
797 /// match elicit_result {
798 /// ElicitationAction::Accept => {
799 /// println!("User accepted and confirmed completion");
800 /// }
801 /// ElicitationAction::Decline => {
802 /// println!("User declined the request");
803 /// }
804 /// ElicitationAction::Cancel => {
805 /// println!("User cancelled/dismissed the request");
806 /// }
807 /// _ => {}
808 /// }
809 /// Ok(())
810 /// }
811 /// ```
812 #[cfg(feature = "elicitation")]
813 pub async fn elicit_url(
814 &self,
815 message: impl Into<String>,
816 url: impl Into<Url>,
817 elicitation_id: impl Into<String>,
818 ) -> Result<ElicitationAction, ElicitationError> {
819 self.elicit_url_with_timeout(message, url, elicitation_id, None)
820 .await
821 }
822
823 /// Request the user to visit a URL and confirm completion.
824 ///
825 /// Same as `elicit_url()` but allows specifying a custom timeout for the request.
826 ///
827 /// # Arguments
828 /// * `message` - The prompt message for the user
829 /// * `url` - The URL the user is requested to visit
830 /// * `elicitation_id` - A unique identifier for this elicitation request
831 /// * `timeout` - Optional timeout duration. If None, uses default timeout behavior
832 /// # Returns
833 /// * `Ok(action)` indicating the user's response action
834 /// * `Err(ElicitationError::CapabilityNotSupported)` if client does not support elicitation via URL
835 /// * `Err(ElicitationError::Service(_))` if the underlying service call failed
836 /// # Example
837 /// ```rust,no_run
838 /// # use std::time::Duration;
839 /// use rmcp::*;
840 /// # use rmcp::model::ElicitationAction;
841 /// # use url::Url;
842 ///
843 /// async fn example(peer: Peer<RoleServer>) -> Result<(), Box<dyn std::error::Error>> {
844 /// let elicit_result = peer.elicit_url_with_timeout("Please visit the following URL to complete the action",
845 /// Url::parse("https://example.com/complete_action")?,
846 /// "elicit_123",
847 /// Some(Duration::from_secs(30))).await?;
848 /// match elicit_result {
849 /// ElicitationAction::Accept => {
850 /// println!("User accepted and confirmed completion");
851 /// }
852 /// ElicitationAction::Decline => {
853 /// println!("User declined the request");
854 /// }
855 /// ElicitationAction::Cancel => {
856 /// println!("User cancelled/dismissed the request");
857 /// }
858 /// _ => {}
859 /// }
860 /// Ok(())
861 /// }
862 /// ```
863 #[cfg(feature = "elicitation")]
864 pub async fn elicit_url_with_timeout(
865 &self,
866 message: impl Into<String>,
867 url: impl Into<Url>,
868 elicitation_id: impl Into<String>,
869 timeout: Option<std::time::Duration>,
870 ) -> Result<ElicitationAction, ElicitationError> {
871 // Check if client supports url elicitation
872 if !self
873 .supported_elicitation_modes()
874 .contains(&ElicitationMode::Url)
875 {
876 return Err(ElicitationError::CapabilityNotSupported);
877 }
878
879 let action = self
880 .create_elicitation_with_timeout(
881 CreateElicitationRequestParams::UrlElicitationParams {
882 meta: None,
883 message: message.into(),
884 url: url.into().to_string(),
885 elicitation_id: elicitation_id.into(),
886 },
887 timeout,
888 )
889 .await?
890 .action;
891 Ok(action)
892 }
893}