Skip to main content

prism_mcp_rs/client/
request_handler.rs

1//! Client-side request handling for server-initiated requests
2//!
3//! Module provides the infrastructure for handling requests that the MCP server
4//! sends to the client, enabling true bidirectional communication as defined in the
5//! MCP 2025-11-25 specification
6//!
7//! Key features:
8//! - Sampling/createMessage request handling (LLM integration)
9//! - Roots/list request handling (file system access)
10//! - Elicitation/create request handling (user input forms)
11//! - Ping request handling (connectivity testing)
12//!
13//! # Example
14//! ```rust
15//! use prism_mcp_rs::client::{McpClient, InteractiveClientRequestHandler};
16//!
17//! let mut client = McpClient::new("my-app".to_string(), "1.0.0".to_string());
18//! let handler = InteractiveClientRequestHandler::new("My Application")
19//! .add_root("file:///home/user", Some("Home Directory"))
20//! .auto_accept_elicitation(true);
21//! client.set_request_handler(handler);
22//! ```
23//!
24//! # Sampling Integration Example
25//!
26//! For production LLM integration with OpenAI or Anthropic:
27//!
28//! ```rust,ignore
29//! use async_trait::async_trait;
30//! use openai_api_rust::v1::api::Client as OpenAIClient;
31//! use prism_mcp_rs::client::ClientRequestHandler;
32//!
33//! struct OpenAIRequestHandler {
34//!     openai_client: OpenAIClient,
35//!     api_key: String,
36//! }
37//!
38//! #[async_trait]
39//! impl ClientRequestHandler for OpenAIRequestHandler {
40//!     async fn handle_create_message(
41//!         &self,
42//!         params: CreateMessageParams,
43//!     ) -> McpResult<CreateMessageResult> {
44//!         // Convert MCP messages to OpenAI format
45//!         let openai_messages = params.messages.iter().map(|msg| {
46//!             openai_api_rust::v1::chat::ChatCompletionMessage {
47//!                 role: match msg.role {
48//!                     Role::User => "user".to_string(),
49//!                     Role::Assistant => "assistant".to_string(),
50//!                 },
51//!                 content: msg.content.as_text().unwrap_or("").to_string(),
52//!             }
53//!         }).collect();
54//!
55//!         // Call OpenAI API
56//!         let response = self.openai_client.chat_completion(
57//!             openai_api_rust::v1::chat::ChatCompletionRequest {
58//!                 model: params.model_preferences
59//!                     .and_then(|p| p.hints.first().map(|h| h.name.clone()))
60//!                     .unwrap_or("gpt-4".to_string()),
61//!                 messages: openai_messages,
62//!                 max_tokens: Some(params.max_tokens as i32),
63//!                 temperature: params.temperature,
64//!                 // ... other parameters
65//!             }
66//!         ).await?;
67//!
68//!         // Convert OpenAI response to MCP format
69//!         Ok(CreateMessageResult {
70//!             model: response.model,
71//!             stop_reason: Some(StopReason::EndTurn),
72//!             role: Role::Assistant,
73//!             content: SamplingContent::Text {
74//!                 text: response.choices[0].message.content.clone(),
75//!                 annotations: None,
76//!                 meta: None,
77//!             },
78//!             meta: None,
79//!         })
80//!     }
81//!     // ... other handler methods
82//! }
83//! ```
84//!
85//! # Roots Security Model
86//!
87//! **IMPORTANT**: Roots provide guidance but DO NOT enforce security boundaries.
88//!
89//! Security considerations:
90//! - Roots are hints to servers about intended working directories
91//! - Actual file access is ALWAYS mediated by the client's OS-level permissions
92//! - Clients should validate all file operations against their security policies
93//! - Never grant roots to directories containing sensitive data (e.g., ~/.ssh, /etc)
94//! - Consider implementing additional access controls in your handler
95//!
96//! Example secure root configuration:
97//! ```rust
98//! use prism_mcp_rs::client::InteractiveClientRequestHandler;
99//! use std::path::PathBuf;
100//!
101//! let handler = InteractiveClientRequestHandler::new("Secure App")
102//!     // Only grant access to specific project directories
103//!     .add_root("file:///home/user/projects/current", Some("Current Project"))
104//!     // Avoid sensitive directories
105//!     // NEVER: .add_root("file:///home/user/.ssh", Some("SSH Keys"))
106//!     // NEVER: .add_root("file:///etc", Some("System Config"))
107//!     .verbose(true);
108//! ```
109
110use async_trait::async_trait;
111use std::collections::HashMap;
112use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader};
113use tracing::{debug, info};
114
115use crate::core::error::{McpError, McpResult};
116use crate::protocol::messages::*;
117use crate::protocol::types::*;
118
119/// Trait for handling server-initiated requests
120///
121/// Trait defines the interface for processing requests that the MCP server
122/// sends to the client. Each method corresponds to a specific MCP method that
123/// servers can invoke on clients for bidirectional communication.
124#[async_trait]
125pub trait ClientRequestHandler: Send + Sync {
126    /// Handle sampling/createMessage request from the server
127    ///
128    /// Method is called when the server wants the client to generate
129    /// a message using an LLM (Large Language Model). This enables the server
130    /// to leverage the client's AI capabilities.
131    ///
132    /// # Arguments
133    /// * `params` - Parameters for message creation including conversation context
134    ///
135    /// # Returns
136    /// Result containing the generated message or an error
137    async fn handle_create_message(
138        &self,
139        params: CreateMessageParams,
140    ) -> McpResult<CreateMessageResult>;
141
142    /// Handle roots/list request from the server
143    ///
144    /// Method is called when the server wants to know what root directories
145    /// or file system areas the client has access to. This is used for file system
146    /// integration and resource discovery.
147    ///
148    /// # Arguments
149    /// * `params` - Parameters for root listing (typically empty)
150    ///
151    /// # Returns
152    /// Result containing available roots or an error
153    async fn handle_list_roots(&self, params: ListRootsParams) -> McpResult<ListRootsResult>;
154
155    /// Handle elicitation/create request from the server
156    ///
157    /// Method is called when the server wants to collect structured input
158    /// from the user through a form-like interface. This enables interactive
159    /// workflows where the server needs user confirmation or additional data.
160    ///
161    /// # Arguments
162    /// * `params` - Parameters defining the form schema and message
163    ///
164    /// # Returns
165    /// Result containing user's response (accept/decline/cancel) and form data
166    async fn handle_elicit(&self, params: ElicitParams) -> McpResult<ElicitResult>;
167
168    /// Handle ping request from the server
169    ///
170    /// Method is called when the server wants to test connectivity
171    /// or measure latency to the client.
172    ///
173    /// # Arguments
174    /// * `params` - Ping parameters (typically empty)
175    ///
176    /// # Returns
177    /// Result containing ping response or an error
178    async fn handle_ping(&self, params: PingParams) -> McpResult<PingResult>;
179}
180
181/// Default implementation that rejects server requests by default
182///
183/// This is the default request handler that provides minimal responses
184/// to server-initiated requests. It's used when no custom handler
185/// is configured.
186pub struct DefaultClientRequestHandler;
187
188#[async_trait]
189impl ClientRequestHandler for DefaultClientRequestHandler {
190    async fn handle_create_message(
191        &self,
192        _params: CreateMessageParams,
193    ) -> McpResult<CreateMessageResult> {
194        Err(McpError::Protocol(
195            "LLM sampling not supported - no handler configured".to_string(),
196        ))
197    }
198
199    async fn handle_list_roots(&self, _params: ListRootsParams) -> McpResult<ListRootsResult> {
200        Ok(ListRootsResult {
201            roots: vec![],
202            meta: None,
203        })
204    }
205
206    async fn handle_elicit(&self, _params: ElicitParams) -> McpResult<ElicitResult> {
207        Err(McpError::Protocol(
208            "User interaction not supported - no handler configured".to_string(),
209        ))
210    }
211
212    async fn handle_ping(&self, _params: PingParams) -> McpResult<PingResult> {
213        Ok(PingResult { meta: None })
214    }
215}
216
217/// Interactive client request handler with user prompts
218///
219/// This handler provides interactive responses to server requests,
220/// prompting the user for input when needed. It's suitable for
221/// command-line applications and interactive environments.
222///
223/// # Features
224/// - Interactive user prompts for elicitation requests
225/// - Configurable root directories
226/// - Optional auto-acceptance for testing
227/// - Console-based user interaction
228pub struct InteractiveClientRequestHandler {
229    /// Application name for user prompts
230    app_name: String,
231    /// Available root directories
232    roots: Vec<Root>,
233    /// Whether to auto-accept elicitation requests (for testing)
234    auto_accept_elicitation: bool,
235    /// Whether to show verbose output
236    verbose: bool,
237}
238
239impl InteractiveClientRequestHandler {
240    /// Create a new interactive request handler
241    ///
242    /// # Arguments
243    /// * `app_name` - Name of the application for user prompts
244    ///
245    /// # Example
246    /// ```rust
247    /// use prism_mcp_rs::client::InteractiveClientRequestHandler;
248    ///
249    /// let handler = InteractiveClientRequestHandler::new("My App");
250    /// ```
251    pub fn new<S: Into<String>>(app_name: S) -> Self {
252        Self {
253            app_name: app_name.into(),
254            roots: Vec::new(),
255            auto_accept_elicitation: false,
256            verbose: false,
257        }
258    }
259
260    /// Add a root directory that the client can access
261    ///
262    /// # Arguments
263    /// * `uri` - URI of the root directory (e.g., "file:///home/user")
264    /// * `name` - Optional display name for the root
265    ///
266    /// # Example
267    /// ```rust
268    /// use prism_mcp_rs::client::InteractiveClientRequestHandler;
269    ///
270    /// let handler = InteractiveClientRequestHandler::new("My App")
271    /// .add_root("file:///home/user", Some("Home Directory"));
272    /// ```
273    pub fn add_root<S: Into<String>>(mut self, uri: S, name: Option<S>) -> Self {
274        let mut root = Root::new(uri.into());
275        if let Some(n) = name {
276            root = root.with_name(n.into());
277        }
278        self.roots.push(root);
279        self
280    }
281
282    /// Set whether to auto-accept elicitation requests
283    ///
284    /// When enabled, elicitation requests are automatically accepted
285    /// with empty form data. This is useful for testing and automation.
286    ///
287    /// # Arguments
288    /// * `auto_accept` - Whether to auto-accept requests
289    ///
290    /// # Example
291    /// ```rust
292    /// use prism_mcp_rs::client::InteractiveClientRequestHandler;
293    ///
294    /// let handler = InteractiveClientRequestHandler::new("My App")
295    /// .auto_accept_elicitation(true); // For testing
296    /// ```
297    pub fn auto_accept_elicitation(mut self, auto_accept: bool) -> Self {
298        self.auto_accept_elicitation = auto_accept;
299        self
300    }
301
302    /// Set verbose output mode
303    ///
304    /// When enabled, the handler will print detailed information about
305    /// each request it processes.
306    ///
307    /// # Arguments
308    /// * `verbose` - Whether to enable verbose output
309    pub fn verbose(mut self, verbose: bool) -> Self {
310        self.verbose = verbose;
311        self
312    }
313
314    /// Add common root directories for the current platform
315    ///
316    /// Method adds platform-appropriate root directories like
317    /// home directory, documents, etc.
318    ///
319    /// # Example
320    /// ```rust
321    /// use prism_mcp_rs::client::InteractiveClientRequestHandler;
322    ///
323    /// let handler = InteractiveClientRequestHandler::new("My App")
324    /// .add_common_roots();
325    /// ```
326    /// Add common file system roots
327    pub fn add_common_roots(mut self) -> Self {
328        // Add home directory
329        if let Some(home_dir) = dirs::home_dir() {
330            let home_uri = format!("file://{}", home_dir.display());
331            self.roots
332                .push(Root::new(home_uri).with_name("Home Directory".to_string()));
333        }
334
335        // Add documents directory
336        if let Some(docs_dir) = dirs::document_dir() {
337            let docs_uri = format!("file://{}", docs_dir.display());
338            self.roots
339                .push(Root::new(docs_uri).with_name("Documents".to_string()));
340        }
341
342        // Add desktop directory
343        if let Some(desktop_dir) = dirs::desktop_dir() {
344            let desktop_uri = format!("file://{}", desktop_dir.display());
345            self.roots
346                .push(Root::new(desktop_uri).with_name("Desktop".to_string()));
347        }
348
349        self
350    }
351
352    /// Prompt user for yes/no input
353    async fn prompt_yes_no(&self, message: &str) -> McpResult<bool> {
354        println!("\n[{}] {}", self.app_name, message);
355        print!("Continue? (y/N): ");
356        io::stdout().flush().await.map_err(McpError::io)?;
357
358        let stdin = io::stdin();
359        let mut reader = BufReader::new(stdin);
360        let mut input = String::new();
361
362        reader.read_line(&mut input).await.map_err(McpError::io)?;
363        let input = input.trim().to_lowercase();
364
365        Ok(input == "y" || input == "yes")
366    }
367
368    /// Collect form data from user based on schema
369    async fn collect_form_data(
370        &self,
371        schema: &ElicitationSchema,
372    ) -> McpResult<HashMap<String, serde_json::Value>> {
373        let mut form_data = HashMap::new();
374
375        println!(
376            "\n[{}] Please provide the following information:",
377            self.app_name
378        );
379
380        // Show required fields if any
381        if let Some(ref required_fields) = schema.required {
382            if !required_fields.is_empty() {
383                println!("Required fields: {}", required_fields.join(", "));
384            }
385        }
386
387        for (field_name, field_def) in &schema.properties {
388            let prompt = match field_def {
389                PrimitiveSchemaDefinition::String {
390                    title, description, ..
391                } => {
392                    format!(
393                        "{} ({}): ",
394                        title.as_ref().unwrap_or(field_name),
395                        description.as_ref().unwrap_or(&"string".to_string())
396                    )
397                }
398                PrimitiveSchemaDefinition::Number {
399                    title, description, ..
400                } => {
401                    format!(
402                        "{} ({}): ",
403                        title.as_ref().unwrap_or(field_name),
404                        description.as_ref().unwrap_or(&"number".to_string())
405                    )
406                }
407                PrimitiveSchemaDefinition::Integer {
408                    title, description, ..
409                } => {
410                    format!(
411                        "{} ({}): ",
412                        title.as_ref().unwrap_or(field_name),
413                        description.as_ref().unwrap_or(&"integer".to_string())
414                    )
415                }
416                PrimitiveSchemaDefinition::Boolean {
417                    title, description, ..
418                } => {
419                    format!(
420                        "{} ({}) (y/N): ",
421                        title.as_ref().unwrap_or(field_name),
422                        description.as_ref().unwrap_or(&"yes/no".to_string())
423                    )
424                }
425            };
426
427            print!("{prompt}");
428            io::stdout().flush().await.map_err(McpError::io)?;
429
430            let stdin = io::stdin();
431            let mut reader = BufReader::new(stdin);
432            let mut input = String::new();
433
434            reader.read_line(&mut input).await.map_err(McpError::io)?;
435            let input = input.trim();
436
437            // Parse input based on field type
438            let value = match field_def {
439                PrimitiveSchemaDefinition::String { .. } => {
440                    serde_json::Value::String(input.to_string())
441                }
442                PrimitiveSchemaDefinition::Number { .. } => {
443                    if let Ok(num) = input.parse::<f64>() {
444                        serde_json::Value::Number(
445                            serde_json::Number::from_f64(num)
446                                .unwrap_or_else(|| serde_json::Number::from(0)),
447                        )
448                    } else {
449                        return Err(McpError::validation(format!("Invalid number: {input}")));
450                    }
451                }
452                PrimitiveSchemaDefinition::Integer { .. } => {
453                    if let Ok(num) = input.parse::<i64>() {
454                        serde_json::Value::Number(serde_json::Number::from(num))
455                    } else {
456                        return Err(McpError::validation(format!("Invalid integer: {input}")));
457                    }
458                }
459                PrimitiveSchemaDefinition::Boolean { .. } => {
460                    let input_lower = input.to_lowercase();
461                    let value = input_lower == "y" || input_lower == "yes" || input_lower == "true";
462                    serde_json::Value::Bool(value)
463                }
464            };
465
466            form_data.insert(field_name.clone(), value);
467        }
468
469        // Validate required fields are present and non-empty
470        if let Some(ref required_fields) = schema.required {
471            for field_name in required_fields {
472                match form_data.get(field_name) {
473                    None => {
474                        return Err(McpError::validation(format!(
475                            "Required field '{}' is missing",
476                            field_name
477                        )));
478                    }
479                    Some(serde_json::Value::String(s)) if s.is_empty() => {
480                        return Err(McpError::validation(format!(
481                            "Required field '{}' cannot be empty",
482                            field_name
483                        )));
484                    }
485                    _ => {} // Field is present and valid
486                }
487            }
488        }
489
490        Ok(form_data)
491    }
492}
493
494#[async_trait]
495impl ClientRequestHandler for InteractiveClientRequestHandler {
496    async fn handle_create_message(
497        &self,
498        params: CreateMessageParams,
499    ) -> McpResult<CreateMessageResult> {
500        if self.verbose {
501            info!(
502                "Server requested LLM sampling with {} messages, max_tokens: {}",
503                params.messages.len(),
504                params.max_tokens
505            );
506        }
507
508        // For now, we don't have LLM integration, so we provide a helpful error
509        // In a real implementation, this would integrate with OpenAI, Anthropic, etc.
510        Err(McpError::Protocol(
511            "LLM sampling not implemented - this would require integration with an AI service like OpenAI or Anthropic".to_string()
512        ))
513    }
514
515    async fn handle_list_roots(&self, _params: ListRootsParams) -> McpResult<ListRootsResult> {
516        if self.verbose {
517            info!(
518                "Server requested roots list - returning {} configured roots",
519                self.roots.len()
520            );
521        }
522
523        Ok(ListRootsResult {
524            roots: self.roots.clone(),
525            meta: None,
526        })
527    }
528
529    async fn handle_elicit(&self, params: ElicitParams) -> McpResult<ElicitResult> {
530        if self.verbose {
531            info!("Server requested user input: {}", params.message);
532        }
533
534        // Auto-accept mode for testing
535        if self.auto_accept_elicitation {
536            debug!("Auto-accepting elicitation request");
537            return Ok(ElicitResult {
538                action: ElicitationAction::Accept,
539                content: Some(HashMap::new()),
540                meta: None,
541            });
542        }
543
544        // Show the message to the user
545        println!("\n=== Server Request ===");
546        println!("{}", params.message);
547        println!("======================");
548
549        // Ask if user wants to proceed
550        let proceed = self
551            .prompt_yes_no("The server is requesting your input.")
552            .await?;
553
554        if !proceed {
555            return Ok(ElicitResult {
556                action: ElicitationAction::Decline,
557                content: None,
558                meta: None,
559            });
560        }
561
562        // URL-mode elicitation is a client UX flow; this simple handler cannot launch
563        // URLs, so it fails safely.
564        if matches!(params.mode, Some(ElicitationMode::Url)) {
565            return Ok(ElicitResult {
566                action: ElicitationAction::Decline,
567                content: None,
568                meta: None,
569            });
570        }
571
572        let Some(requested_schema) = params.requested_schema.as_ref() else {
573            return Ok(ElicitResult {
574                action: ElicitationAction::Cancel,
575                content: None,
576                meta: None,
577            });
578        };
579
580        // Collect form data
581        let form_data = match self.collect_form_data(requested_schema).await {
582            Ok(data) => data,
583            Err(_) => {
584                return Ok(ElicitResult {
585                    action: ElicitationAction::Cancel,
586                    content: None,
587                    meta: None,
588                });
589            }
590        };
591
592        Ok(ElicitResult {
593            action: ElicitationAction::Accept,
594            content: Some(form_data),
595            meta: None,
596        })
597    }
598
599    async fn handle_ping(&self, _params: PingParams) -> McpResult<PingResult> {
600        if self.verbose {
601            debug!("Server ping received - responding");
602        }
603
604        Ok(PingResult { meta: None })
605    }
606}
607
608/// Simple request handler for headless/automated environments
609///
610/// This handler provides non-interactive responses suitable for
611/// automated environments, testing, and headless applications.
612/// It never prompts the user and provides sensible defaults.
613pub struct AutomatedClientRequestHandler {
614    /// Available root directories
615    roots: Vec<Root>,
616    /// Default responses for elicitation requests
617    default_responses: HashMap<String, serde_json::Value>,
618}
619
620impl AutomatedClientRequestHandler {
621    /// Create a new automated request handler
622    ///
623    /// # Example
624    /// ```rust
625    /// use prism_mcp_rs::client::AutomatedClientRequestHandler;
626    ///
627    /// let handler = AutomatedClientRequestHandler::new();
628    /// ```
629    pub fn new() -> Self {
630        Self {
631            roots: Vec::new(),
632            default_responses: HashMap::new(),
633        }
634    }
635
636    /// Add a root directory
637    ///
638    /// # Arguments
639    /// * `uri` - URI of the root directory
640    /// * `name` - Optional display name
641    pub fn add_root<S: Into<String>>(mut self, uri: S, name: Option<S>) -> Self {
642        let mut root = Root::new(uri.into());
643        if let Some(n) = name {
644            root = root.with_name(n.into());
645        }
646        self.roots.push(root);
647        self
648    }
649
650    /// Set a default response for elicitation fields
651    ///
652    /// # Arguments
653    /// * `field_name` - Name of the form field
654    /// * `value` - Default value to provide
655    pub fn set_default_response<S: Into<String>>(
656        mut self,
657        field_name: S,
658        value: serde_json::Value,
659    ) -> Self {
660        self.default_responses.insert(field_name.into(), value);
661        self
662    }
663}
664
665impl Default for AutomatedClientRequestHandler {
666    fn default() -> Self {
667        Self::new()
668    }
669}
670
671#[async_trait]
672impl ClientRequestHandler for AutomatedClientRequestHandler {
673    async fn handle_create_message(
674        &self,
675        _params: CreateMessageParams,
676    ) -> McpResult<CreateMessageResult> {
677        // Automated environments can't provide LLM sampling
678        Err(McpError::Protocol(
679            "LLM sampling not available in automated mode".to_string(),
680        ))
681    }
682
683    async fn handle_list_roots(&self, _params: ListRootsParams) -> McpResult<ListRootsResult> {
684        Ok(ListRootsResult {
685            roots: self.roots.clone(),
686            meta: None,
687        })
688    }
689
690    async fn handle_elicit(&self, params: ElicitParams) -> McpResult<ElicitResult> {
691        // URL-mode elicitation can't be actioned in automated mode.
692        if matches!(params.mode, Some(ElicitationMode::Url)) {
693            return Ok(ElicitResult {
694                action: ElicitationAction::Decline,
695                content: None,
696                meta: None,
697            });
698        }
699
700        let Some(requested_schema) = params.requested_schema.as_ref() else {
701            return Ok(ElicitResult {
702                action: ElicitationAction::Cancel,
703                content: None,
704                meta: None,
705            });
706        };
707
708        // Generate form data using defaults or empty values
709        let mut form_data = HashMap::new();
710
711        for (field_name, field_def) in &requested_schema.properties {
712            let value = if let Some(default_value) = self.default_responses.get(field_name) {
713                default_value.clone()
714            } else {
715                // Provide sensible defaults based on field type
716                match field_def {
717                    PrimitiveSchemaDefinition::String { .. } => {
718                        serde_json::Value::String(String::new())
719                    }
720                    PrimitiveSchemaDefinition::Number { .. } => {
721                        serde_json::Value::Number(serde_json::Number::from(0))
722                    }
723                    PrimitiveSchemaDefinition::Integer { .. } => {
724                        serde_json::Value::Number(serde_json::Number::from(0))
725                    }
726                    PrimitiveSchemaDefinition::Boolean { default, .. } => {
727                        serde_json::Value::Bool(default.unwrap_or(false))
728                    }
729                }
730            };
731
732            form_data.insert(field_name.clone(), value);
733        }
734
735        Ok(ElicitResult {
736            action: ElicitationAction::Accept,
737            content: Some(form_data),
738            meta: None,
739        })
740    }
741
742    async fn handle_ping(&self, _params: PingParams) -> McpResult<PingResult> {
743        Ok(PingResult { meta: None })
744    }
745}
746
747#[cfg(test)]
748mod tests {
749    use super::*;
750
751    #[tokio::test]
752    async fn test_default_handler() {
753        let handler = DefaultClientRequestHandler;
754
755        // Test ping (should succeed)
756        let ping_result = handler.handle_ping(PingParams { meta: None }).await;
757        assert!(ping_result.is_ok());
758
759        // Test roots list (should return empty)
760        let roots_result = handler
761            .handle_list_roots(ListRootsParams { meta: None })
762            .await;
763        assert!(roots_result.is_ok());
764        assert!(roots_result.unwrap().roots.is_empty());
765
766        // Test create message (should fail)
767        let create_params = CreateMessageParams {
768            messages: vec![],
769            max_tokens: 100,
770            system_prompt: None,
771            include_context: None,
772            temperature: None,
773            stop_sequences: None,
774            model_preferences: None,
775            tools: None,
776            tool_choice: None,
777            metadata: None,
778            meta: None,
779        };
780        let create_result = handler.handle_create_message(create_params).await;
781        assert!(create_result.is_err());
782
783        // Test elicit (should fail)
784        let elicit_params = ElicitParams {
785            message: "Test message".to_string(),
786            mode: Some(ElicitationMode::Form),
787            url: None,
788            requested_schema: Some(ElicitationSchema {
789                schema_type: "object".to_string(),
790                properties: HashMap::new(),
791                required: None,
792            }),
793            meta: None,
794        };
795        let elicit_result = handler.handle_elicit(elicit_params).await;
796        assert!(elicit_result.is_err());
797    }
798
799    #[tokio::test]
800    async fn test_interactive_handler_builder() {
801        let handler = InteractiveClientRequestHandler::new("Test App")
802            .add_root("file:///test", Some("Test Root"))
803            .auto_accept_elicitation(true)
804            .verbose(true);
805
806        // Test roots list
807        let roots_result = handler
808            .handle_list_roots(ListRootsParams { meta: None })
809            .await;
810        assert!(roots_result.is_ok());
811        let roots = roots_result.unwrap().roots;
812        assert_eq!(roots.len(), 1);
813        assert_eq!(roots[0].uri, "file:///test");
814        assert_eq!(roots[0].name, Some("Test Root".to_string()));
815
816        // Test elicit with auto-accept
817        let elicit_params = ElicitParams {
818            message: "Test message".to_string(),
819            mode: Some(ElicitationMode::Form),
820            url: None,
821            requested_schema: Some(ElicitationSchema {
822                schema_type: "object".to_string(),
823                properties: HashMap::new(),
824                required: None,
825            }),
826            meta: None,
827        };
828        let elicit_result = handler.handle_elicit(elicit_params).await;
829        assert!(elicit_result.is_ok());
830        let result = elicit_result.unwrap();
831        assert!(matches!(result.action, ElicitationAction::Accept));
832        assert!(result.content.is_some());
833    }
834
835    #[tokio::test]
836    async fn test_elicitation_required_field_validation() {
837        let _handler =
838            InteractiveClientRequestHandler::new("Test App").auto_accept_elicitation(false);
839
840        // Test that required fields are properly tracked
841        let mut properties = HashMap::new();
842        properties.insert(
843            "email".to_string(),
844            PrimitiveSchemaDefinition::String {
845                title: Some("Email Address".to_string()),
846                description: Some("Your email for confirmation".to_string()),
847                min_length: None,
848                max_length: None,
849                format: Some("email".to_string()),
850                enum_values: None,
851                enum_names: None,
852            },
853        );
854        properties.insert(
855            "optional_field".to_string(),
856            PrimitiveSchemaDefinition::String {
857                title: Some("Optional Info".to_string()),
858                description: None,
859                min_length: None,
860                max_length: None,
861                format: None,
862                enum_values: None,
863                enum_names: None,
864            },
865        );
866
867        let _elicit_params = ElicitParams {
868            message: "Please provide your email".to_string(),
869            mode: Some(ElicitationMode::Form),
870            url: None,
871            requested_schema: Some(ElicitationSchema {
872                schema_type: "object".to_string(),
873                properties,
874                required: Some(vec!["email".to_string()]), // email is required
875            }),
876            meta: None,
877        };
878
879        // With auto_accept off and no user input simulation, this will fail
880        // In a real scenario, we'd need to mock stdin or use dependency injection
881        // This test demonstrates the structure is in place
882    }
883
884    #[tokio::test]
885    async fn test_automated_handler() {
886        let handler = AutomatedClientRequestHandler::new()
887            .add_root("file:///automated", Some("Automated Root"))
888            .set_default_response(
889                "test_field",
890                serde_json::Value::String("test_value".to_string()),
891            );
892
893        // Test roots list
894        let roots_result = handler
895            .handle_list_roots(ListRootsParams { meta: None })
896            .await;
897        assert!(roots_result.is_ok());
898        let roots = roots_result.unwrap().roots;
899        assert_eq!(roots.len(), 1);
900
901        // Test elicit with default responses
902        let mut properties = HashMap::new();
903        properties.insert(
904            "test_field".to_string(),
905            PrimitiveSchemaDefinition::String {
906                title: Some("Test Field".to_string()),
907                description: None,
908                min_length: None,
909                max_length: None,
910                format: None,
911                enum_values: None,
912                enum_names: None,
913            },
914        );
915
916        let elicit_params = ElicitParams {
917            message: "Test message".to_string(),
918            mode: Some(ElicitationMode::Form),
919            url: None,
920            requested_schema: Some(ElicitationSchema {
921                schema_type: "object".to_string(),
922                properties,
923                required: None,
924            }),
925            meta: None,
926        };
927
928        let elicit_result = handler.handle_elicit(elicit_params).await;
929        assert!(elicit_result.is_ok());
930        let result = elicit_result.unwrap();
931        assert!(matches!(result.action, ElicitationAction::Accept));
932
933        let content = result.content.unwrap();
934        assert_eq!(
935            content.get("test_field"),
936            Some(&serde_json::Value::String("test_value".to_string()))
937        );
938    }
939}