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}