Skip to main content

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}