mcp_host/server/
multiplexer.rs

1//! Request Multiplexer for bidirectional JSON-RPC communication
2//!
3//! Enables server→client requests (roots/list, sampling/createMessage, etc.)
4//! by tracking pending requests and routing responses back to callers.
5//!
6//! # Architecture
7//!
8//! The multiplexer sits between the server and transport, handling:
9//! - Client→Server: Normal requests (tools/call, resources/read, etc.)
10//! - Server→Client: Requests initiated by the server (roots/list, sampling/createMessage)
11//!
12//! For server-initiated requests, the multiplexer:
13//! 1. Generates a unique request ID (UUID)
14//! 2. Stores a oneshot channel in pending requests map
15//! 3. Sends the request via transport
16//! 4. When response arrives, routes it to the waiting channel
17//!
18//! # Example
19//!
20//! ```rust,ignore
21//! // Server requesting roots from client
22//! let roots = server.request_roots().await?;
23//! for root in roots {
24//!     println!("Client root: {} ({})", root.name, root.uri);
25//! }
26//!
27//! // Server requesting sampling from client
28//! let result = server.request_sampling(messages, preferences).await?;
29//! println!("Model response: {}", result.content);
30//! ```
31
32use std::sync::Arc;
33use std::time::Duration;
34
35use tokio::sync::mpsc;
36
37use dashmap::DashMap;
38use serde::{Deserialize, Serialize};
39use serde_json::Value;
40use thiserror::Error;
41use tokio::sync::oneshot;
42
43use crate::protocol::types::JsonRpcResponse;
44
45/// Errors from server→client requests
46#[derive(Debug, Error)]
47pub enum MultiplexerError {
48    /// Request timed out waiting for response
49    #[error("request timed out after {0:?}")]
50    Timeout(Duration),
51
52    /// Client returned an error response
53    #[error("client error {code}: {message}")]
54    ClientError { code: i32, message: String },
55
56    /// Transport error
57    #[error("transport error: {0}")]
58    Transport(String),
59
60    /// Response channel was closed (internal error)
61    #[error("response channel closed")]
62    ChannelClosed,
63
64    /// Failed to serialize request
65    #[error("serialization error: {0}")]
66    Serialization(#[from] serde_json::Error),
67
68    /// Client doesn't support the requested capability
69    #[error("client does not support {0}")]
70    UnsupportedCapability(String),
71}
72
73/// A pending server-initiated request awaiting response
74pub struct PendingRequest {
75    /// Request ID (UUID string)
76    pub id: String,
77
78    /// Method name for logging/debugging
79    pub method: String,
80
81    /// Channel to send the response
82    pub response_tx: oneshot::Sender<Result<Value, MultiplexerError>>,
83
84    /// When this request was created (for timeout tracking)
85    pub created_at: std::time::Instant,
86}
87
88/// Request multiplexer for handling bidirectional communication
89///
90/// Tracks pending server→client requests and routes responses
91pub struct RequestMultiplexer {
92    /// Pending requests awaiting responses: request_id → PendingRequest
93    pending: DashMap<String, PendingRequest>,
94
95    /// Default timeout for requests
96    default_timeout: Duration,
97}
98
99impl Default for RequestMultiplexer {
100    fn default() -> Self {
101        Self::new()
102    }
103}
104
105impl RequestMultiplexer {
106    /// Create a new request multiplexer
107    pub fn new() -> Self {
108        Self {
109            pending: DashMap::new(),
110            default_timeout: Duration::from_secs(30),
111        }
112    }
113
114    /// Create with custom default timeout
115    pub fn with_timeout(timeout: Duration) -> Self {
116        Self {
117            pending: DashMap::new(),
118            default_timeout: timeout,
119        }
120    }
121
122    /// Get default timeout
123    pub fn default_timeout(&self) -> Duration {
124        self.default_timeout
125    }
126
127    /// Create a new pending request and return the receiver
128    ///
129    /// Returns (request_id, receiver) - the caller should await the receiver
130    pub fn create_pending(
131        &self,
132        method: impl Into<String>,
133    ) -> (String, oneshot::Receiver<Result<Value, MultiplexerError>>) {
134        let id = uuid::Uuid::new_v4().to_string();
135        let method = method.into();
136        let (tx, rx) = oneshot::channel();
137
138        let pending = PendingRequest {
139            id: id.clone(),
140            method,
141            response_tx: tx,
142            created_at: std::time::Instant::now(),
143        };
144
145        self.pending.insert(id.clone(), pending);
146
147        (id, rx)
148    }
149
150    /// Route an incoming response to its pending request
151    ///
152    /// Returns true if the response was routed, false if no matching request found
153    pub fn route_response(&self, response: &JsonRpcResponse) -> bool {
154        // Extract request ID from response
155        let id = match &response.id {
156            Value::String(s) => s.clone(),
157            Value::Number(n) => n.to_string(),
158            _ => return false,
159        };
160
161        // Find and remove the pending request
162        if let Some((_, pending)) = self.pending.remove(&id) {
163            // Build result or error
164            let result = if let Some(ref error) = response.error {
165                Err(MultiplexerError::ClientError {
166                    code: error.code,
167                    message: error.message.clone(),
168                })
169            } else if let Some(ref result) = response.result {
170                Ok(result.clone())
171            } else {
172                // Empty response (no result, no error) - treat as empty object
173                Ok(Value::Object(serde_json::Map::new()))
174            };
175
176            // Send to waiting caller (ignore if receiver dropped)
177            let _ = pending.response_tx.send(result);
178
179            true
180        } else {
181            false
182        }
183    }
184
185    /// Check if a response ID matches a pending request
186    ///
187    /// Used to distinguish client responses from client requests in the message loop
188    pub fn is_pending_response(&self, id: &Value) -> bool {
189        let id_str = match id {
190            Value::String(s) => s.clone(),
191            Value::Number(n) => n.to_string(),
192            _ => return false,
193        };
194
195        self.pending.contains_key(&id_str)
196    }
197
198    /// Get number of pending requests
199    pub fn pending_count(&self) -> usize {
200        self.pending.len()
201    }
202
203    /// Cancel a pending request
204    pub fn cancel(&self, id: &str) {
205        if let Some((_, pending)) = self.pending.remove(id) {
206            let _ = pending
207                .response_tx
208                .send(Err(MultiplexerError::ChannelClosed));
209        }
210    }
211
212    /// Cancel all pending requests
213    pub fn cancel_all(&self) {
214        let ids: Vec<String> = self.pending.iter().map(|e| e.key().clone()).collect();
215        for id in ids {
216            self.cancel(&id);
217        }
218    }
219
220    /// Clean up timed-out requests
221    ///
222    /// Returns the number of requests that were cleaned up
223    pub fn cleanup_timed_out(&self, timeout: Duration) -> usize {
224        let now = std::time::Instant::now();
225        let mut cleaned = 0;
226
227        // Collect IDs to remove (can't modify during iteration with DashMap)
228        let timed_out: Vec<String> = self
229            .pending
230            .iter()
231            .filter(|e| now.duration_since(e.created_at) > timeout)
232            .map(|e| e.key().clone())
233            .collect();
234
235        for id in timed_out {
236            if let Some((_, pending)) = self.pending.remove(&id) {
237                let _ = pending
238                    .response_tx
239                    .send(Err(MultiplexerError::Timeout(timeout)));
240                cleaned += 1;
241            }
242        }
243
244        cleaned
245    }
246}
247
248/// Server→client request (what we send to the client)
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct JsonRpcClientRequest {
251    /// JSON-RPC version
252    pub jsonrpc: String,
253
254    /// Request ID (UUID string for server-initiated requests)
255    pub id: String,
256
257    /// Method name
258    pub method: String,
259
260    /// Request parameters
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub params: Option<Value>,
263}
264
265impl JsonRpcClientRequest {
266    /// Create a new client request
267    pub fn new(id: impl Into<String>, method: impl Into<String>, params: Option<Value>) -> Self {
268        Self {
269            jsonrpc: "2.0".to_string(),
270            id: id.into(),
271            method: method.into(),
272            params,
273        }
274    }
275}
276
277/// Root entry from roots/list response
278#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct Root {
280    /// URI of the root (e.g., "file:///workspace")
281    pub uri: String,
282
283    /// Human-readable name for the root
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub name: Option<String>,
286}
287
288/// Result of roots/list request
289#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct ListRootsResult {
291    /// List of workspace roots
292    pub roots: Vec<Root>,
293}
294
295/// Sampling message for createMessage request
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct SamplingMessage {
298    /// Role: "user" or "assistant"
299    pub role: String,
300
301    /// Message content
302    pub content: SamplingContent,
303}
304
305/// Content in a sampling message
306#[derive(Debug, Clone, Serialize, Deserialize)]
307#[serde(tag = "type")]
308pub enum SamplingContent {
309    /// Text content
310    #[serde(rename = "text")]
311    Text { text: String },
312
313    /// Image content
314    #[serde(rename = "image")]
315    Image { data: String, mime_type: String },
316}
317
318/// Model preferences for sampling
319#[derive(Debug, Clone, Default, Serialize, Deserialize)]
320#[serde(rename_all = "camelCase")]
321pub struct ModelPreferences {
322    /// Preferred model hints
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub hints: Option<Vec<ModelHint>>,
325
326    /// Cost priority (0.0 = prefer cheap, 1.0 = prefer quality)
327    #[serde(skip_serializing_if = "Option::is_none")]
328    pub cost_priority: Option<f64>,
329
330    /// Speed priority (0.0 = prefer slow, 1.0 = prefer fast)
331    #[serde(skip_serializing_if = "Option::is_none")]
332    pub speed_priority: Option<f64>,
333
334    /// Intelligence priority (0.0 = prefer simple, 1.0 = prefer capable)
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub intelligence_priority: Option<f64>,
337}
338
339/// Model hint for sampling
340#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct ModelHint {
342    /// Model name pattern
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub name: Option<String>,
345}
346
347/// Parameters for sampling/createMessage request
348#[derive(Debug, Clone, Serialize, Deserialize)]
349#[serde(rename_all = "camelCase")]
350pub struct CreateMessageParams {
351    /// Messages to send
352    pub messages: Vec<SamplingMessage>,
353
354    /// Model preferences
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub model_preferences: Option<ModelPreferences>,
357
358    /// System prompt
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub system_prompt: Option<String>,
361
362    /// Include context from MCP servers
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub include_context: Option<String>,
365
366    /// Temperature
367    #[serde(skip_serializing_if = "Option::is_none")]
368    pub temperature: Option<f64>,
369
370    /// Maximum tokens to generate
371    pub max_tokens: i32,
372
373    /// Stop sequences
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub stop_sequences: Option<Vec<String>>,
376}
377
378/// Result of sampling/createMessage request
379#[derive(Debug, Clone, Serialize, Deserialize)]
380#[serde(rename_all = "camelCase")]
381pub struct CreateMessageResult {
382    /// Role of the response
383    pub role: String,
384
385    /// Response content
386    pub content: SamplingContent,
387
388    /// Model that was used
389    pub model: String,
390
391    /// Reason for stopping
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub stop_reason: Option<String>,
394}
395
396/// Client requester for making server→client requests from tools
397///
398/// This is passed to tools via ExecutionContext, allowing them to
399/// make requests to the client (roots/list, sampling/createMessage, elicitation/create, etc.)
400///
401/// # Example
402///
403/// ```rust,ignore
404/// impl Tool for ListRootsTool {
405///     async fn execute(&self, ctx: ExecutionContext<'_>) -> Result<Vec<Box<dyn Content>>, ToolError> {
406///         if let Some(requester) = ctx.client_requester() {
407///             match requester.request_roots(None).await {
408///                 Ok(roots) => {
409///                     let msg = format!("Found {} roots", roots.len());
410///                     Ok(vec![Box::new(TextContent::new(msg))])
411///                 }
412///                 Err(e) => Err(ToolError::Execution(e.to_string())),
413///             }
414///         } else {
415///             Err(ToolError::Execution("No client requester available".into()))
416///         }
417///     }
418/// }
419/// ```
420#[derive(Clone)]
421pub struct ClientRequester {
422    /// Channel to send requests to the transport
423    request_tx: mpsc::UnboundedSender<JsonRpcClientRequest>,
424
425    /// Multiplexer for tracking pending requests
426    multiplexer: Arc<RequestMultiplexer>,
427
428    /// Whether the client supports roots capability
429    supports_roots: bool,
430
431    /// Whether the client supports sampling capability
432    supports_sampling: bool,
433
434    /// Whether the client supports elicitation capability
435    supports_elicitation: bool,
436}
437
438impl ClientRequester {
439    /// Create a new client requester
440    pub fn new(
441        request_tx: mpsc::UnboundedSender<JsonRpcClientRequest>,
442        multiplexer: Arc<RequestMultiplexer>,
443        supports_roots: bool,
444        supports_sampling: bool,
445        supports_elicitation: bool,
446    ) -> Self {
447        Self {
448            request_tx,
449            multiplexer,
450            supports_roots,
451            supports_sampling,
452            supports_elicitation,
453        }
454    }
455
456    /// Check if client supports roots capability
457    pub fn supports_roots(&self) -> bool {
458        self.supports_roots
459    }
460
461    /// Check if client supports sampling capability
462    pub fn supports_sampling(&self) -> bool {
463        self.supports_sampling
464    }
465
466    /// Check if client supports elicitation capability
467    pub fn supports_elicitation(&self) -> bool {
468        self.supports_elicitation
469    }
470
471    /// Request workspace roots from the client
472    ///
473    /// Returns an error if the client doesn't support roots capability.
474    pub async fn request_roots(
475        &self,
476        timeout: Option<Duration>,
477    ) -> Result<Vec<Root>, MultiplexerError> {
478        if !self.supports_roots {
479            return Err(MultiplexerError::UnsupportedCapability("roots".to_string()));
480        }
481
482        // Create pending request
483        let (id, rx) = self.multiplexer.create_pending("roots/list");
484
485        // Build and send the request
486        let request = JsonRpcClientRequest::new(&id, "roots/list", Some(serde_json::json!({})));
487
488        self.request_tx
489            .send(request)
490            .map_err(|e| MultiplexerError::Transport(e.to_string()))?;
491
492        // Wait for response with timeout
493        let timeout = timeout.unwrap_or(self.multiplexer.default_timeout());
494        let result = tokio::time::timeout(timeout, rx)
495            .await
496            .map_err(|_| MultiplexerError::Timeout(timeout))?
497            .map_err(|_| MultiplexerError::ChannelClosed)??;
498
499        // Parse the result
500        let list_result: ListRootsResult = serde_json::from_value(result)?;
501        Ok(list_result.roots)
502    }
503
504    /// Request an LLM completion from the client
505    ///
506    /// Returns an error if the client doesn't support sampling capability.
507    pub async fn request_sampling(
508        &self,
509        params: CreateMessageParams,
510        timeout: Option<Duration>,
511    ) -> Result<CreateMessageResult, MultiplexerError> {
512        if !self.supports_sampling {
513            return Err(MultiplexerError::UnsupportedCapability(
514                "sampling".to_string(),
515            ));
516        }
517
518        // Create pending request
519        let (id, rx) = self.multiplexer.create_pending("sampling/createMessage");
520
521        // Build and send the request
522        let params_value = serde_json::to_value(&params)?;
523        let request = JsonRpcClientRequest::new(&id, "sampling/createMessage", Some(params_value));
524
525        self.request_tx
526            .send(request)
527            .map_err(|e| MultiplexerError::Transport(e.to_string()))?;
528
529        // Wait for response with timeout
530        let timeout = timeout.unwrap_or(self.multiplexer.default_timeout());
531        let result = tokio::time::timeout(timeout, rx)
532            .await
533            .map_err(|_| MultiplexerError::Timeout(timeout))?
534            .map_err(|_| MultiplexerError::ChannelClosed)??;
535
536        // Parse the result
537        let create_result: CreateMessageResult = serde_json::from_value(result)?;
538        Ok(create_result)
539    }
540
541    /// Request user input elicitation from the client
542    ///
543    /// Sends an `elicitation/create` request to the client and waits for the response.
544    /// The client must have the `elicitation` capability advertised.
545    ///
546    /// # Arguments
547    ///
548    /// * `message` - The message to show the user
549    /// * `requested_schema` - The schema defining the structure of requested input
550    /// * `timeout` - Optional timeout for the request
551    ///
552    /// # Returns
553    ///
554    /// The user's response (accept/decline/cancel with optional data), or an error if:
555    /// - Client doesn't support elicitation capability
556    /// - Request times out
557    /// - Transport error occurs
558    ///
559    /// # Example
560    ///
561    /// ```rust,ignore
562    /// use mcp_host::protocol::elicitation::ElicitationSchema;
563    ///
564    /// let schema = ElicitationSchema::builder()
565    ///     .required_email("email")
566    ///     .optional_bool("subscribe", false)
567    ///     .build_unchecked();
568    ///
569    /// let result = requester.request_elicitation(
570    ///     "Please provide your email".to_string(),
571    ///     serde_json::to_value(&schema).unwrap(),
572    ///     None,
573    /// ).await?;
574    ///
575    /// match result.action {
576    ///     ElicitationAction::Accept => {
577    ///         let email = result.content.unwrap()["email"].as_str();
578    ///     }
579    ///     ElicitationAction::Decline | ElicitationAction::Cancel => {
580    ///         // User declined or cancelled
581    ///     }
582    /// }
583    /// ```
584    pub async fn request_elicitation(
585        &self,
586        message: String,
587        requested_schema: Value,
588        timeout: Option<Duration>,
589    ) -> Result<crate::protocol::types::CreateElicitationResult, MultiplexerError> {
590        if !self.supports_elicitation {
591            return Err(MultiplexerError::UnsupportedCapability(
592                "elicitation".to_string(),
593            ));
594        }
595
596        // Create pending request
597        let (id, rx) = self.multiplexer.create_pending("elicitation/create");
598
599        // Build request params
600        let params = serde_json::json!({
601            "message": message,
602            "requestedSchema": requested_schema,
603        });
604
605        let request = JsonRpcClientRequest::new(&id, "elicitation/create", Some(params));
606
607        self.request_tx
608            .send(request)
609            .map_err(|e| MultiplexerError::Transport(e.to_string()))?;
610
611        // Wait for response with timeout
612        let timeout = timeout.unwrap_or(self.multiplexer.default_timeout());
613        let result = tokio::time::timeout(timeout, rx)
614            .await
615            .map_err(|_| MultiplexerError::Timeout(timeout))?
616            .map_err(|_| MultiplexerError::ChannelClosed)??;
617
618        // Parse the result
619        let elicitation_result: crate::protocol::types::CreateElicitationResult =
620            serde_json::from_value(result)?;
621        Ok(elicitation_result)
622    }
623}
624
625#[cfg(test)]
626mod tests {
627    use super::*;
628
629    #[test]
630    fn test_multiplexer_create_pending() {
631        let mux = RequestMultiplexer::new();
632
633        let (id1, _rx1) = mux.create_pending("roots/list");
634        let (id2, _rx2) = mux.create_pending("sampling/createMessage");
635
636        assert_ne!(id1, id2);
637        assert_eq!(mux.pending_count(), 2);
638    }
639
640    #[tokio::test]
641    async fn test_multiplexer_route_response() {
642        let mux = RequestMultiplexer::new();
643
644        let (id, rx) = mux.create_pending("test/method");
645
646        // Create a response
647        let response = JsonRpcResponse {
648            jsonrpc: "2.0".to_string(),
649            id: Value::String(id.clone()),
650            result: Some(serde_json::json!({"status": "ok"})),
651            error: None,
652        };
653
654        // Route it
655        assert!(mux.route_response(&response));
656        assert_eq!(mux.pending_count(), 0);
657
658        // Receiver should get the result
659        let result = rx.await.unwrap().unwrap();
660        assert_eq!(result["status"], "ok");
661    }
662
663    #[tokio::test]
664    async fn test_multiplexer_route_error() {
665        let mux = RequestMultiplexer::new();
666
667        let (id, rx) = mux.create_pending("test/method");
668
669        // Create an error response
670        let response = JsonRpcResponse {
671            jsonrpc: "2.0".to_string(),
672            id: Value::String(id.clone()),
673            result: None,
674            error: Some(crate::protocol::types::JsonRpcError {
675                code: -32600,
676                message: "Invalid request".to_string(),
677                data: None,
678            }),
679        };
680
681        // Route it
682        assert!(mux.route_response(&response));
683
684        // Receiver should get the error
685        let result = rx.await.unwrap();
686        assert!(matches!(
687            result,
688            Err(MultiplexerError::ClientError { code: -32600, .. })
689        ));
690    }
691
692    #[test]
693    fn test_multiplexer_is_pending() {
694        let mux = RequestMultiplexer::new();
695
696        let (id, _rx) = mux.create_pending("test");
697
698        assert!(mux.is_pending_response(&Value::String(id.clone())));
699        assert!(!mux.is_pending_response(&Value::String("unknown".to_string())));
700    }
701
702    #[test]
703    fn test_multiplexer_cancel() {
704        let mux = RequestMultiplexer::new();
705
706        let (id, _rx) = mux.create_pending("test");
707        assert_eq!(mux.pending_count(), 1);
708
709        mux.cancel(&id);
710        assert_eq!(mux.pending_count(), 0);
711    }
712
713    #[test]
714    fn test_client_request_serialization() {
715        let req = JsonRpcClientRequest::new("abc-123", "roots/list", Some(serde_json::json!({})));
716
717        let json = serde_json::to_string(&req).unwrap();
718        assert!(json.contains("\"jsonrpc\":\"2.0\""));
719        assert!(json.contains("\"id\":\"abc-123\""));
720        assert!(json.contains("\"method\":\"roots/list\""));
721    }
722
723    #[test]
724    fn test_root_deserialization() {
725        let json = r#"{"uri": "file:///workspace", "name": "My Project"}"#;
726        let root: Root = serde_json::from_str(json).unwrap();
727
728        assert_eq!(root.uri, "file:///workspace");
729        assert_eq!(root.name, Some("My Project".to_string()));
730    }
731
732    #[test]
733    fn test_sampling_content() {
734        let content = SamplingContent::Text {
735            text: "Hello, world!".to_string(),
736        };
737
738        let json = serde_json::to_string(&content).unwrap();
739        assert!(json.contains("\"type\":\"text\""));
740        assert!(json.contains("\"text\":\"Hello, world!\""));
741    }
742
743    #[test]
744    fn test_client_requester_capability_checks() {
745        let (tx, _rx) = mpsc::unbounded_channel();
746        let mux = Arc::new(RequestMultiplexer::new());
747
748        // All capabilities enabled
749        let requester = ClientRequester::new(tx.clone(), mux.clone(), true, true, true);
750        assert!(requester.supports_roots());
751        assert!(requester.supports_sampling());
752        assert!(requester.supports_elicitation());
753
754        // Only roots enabled
755        let requester = ClientRequester::new(tx.clone(), mux.clone(), true, false, false);
756        assert!(requester.supports_roots());
757        assert!(!requester.supports_sampling());
758        assert!(!requester.supports_elicitation());
759
760        // Only elicitation enabled
761        let requester = ClientRequester::new(tx.clone(), mux.clone(), false, false, true);
762        assert!(!requester.supports_roots());
763        assert!(!requester.supports_sampling());
764        assert!(requester.supports_elicitation());
765    }
766
767    #[tokio::test]
768    async fn test_request_elicitation_unsupported() {
769        let (tx, _rx) = mpsc::unbounded_channel();
770        let mux = Arc::new(RequestMultiplexer::new());
771
772        // Elicitation not supported
773        let requester = ClientRequester::new(tx.clone(), mux.clone(), false, false, false);
774
775        let result = requester
776            .request_elicitation(
777                "Test message".to_string(),
778                serde_json::json!({"type": "object", "properties": {}}),
779                None,
780            )
781            .await;
782
783        assert!(matches!(
784            result,
785            Err(MultiplexerError::UnsupportedCapability(cap)) if cap == "elicitation"
786        ));
787    }
788
789    #[test]
790    fn test_elicitation_result_deserialization() {
791        use crate::protocol::types::{CreateElicitationResult, ElicitationAction};
792
793        // Accept with content
794        let json = r#"{"action": "accept", "content": {"email": "test@example.com"}}"#;
795        let result: CreateElicitationResult = serde_json::from_str(json).unwrap();
796        assert_eq!(result.action, ElicitationAction::Accept);
797        assert!(result.content.is_some());
798        assert_eq!(result.content.unwrap()["email"], "test@example.com");
799
800        // Decline without content
801        let json = r#"{"action": "decline"}"#;
802        let result: CreateElicitationResult = serde_json::from_str(json).unwrap();
803        assert_eq!(result.action, ElicitationAction::Decline);
804        assert!(result.content.is_none());
805
806        // Cancel without content
807        let json = r#"{"action": "cancel"}"#;
808        let result: CreateElicitationResult = serde_json::from_str(json).unwrap();
809        assert_eq!(result.action, ElicitationAction::Cancel);
810        assert!(result.content.is_none());
811    }
812}