turbomcp_client/handlers.rs
1//! Handler traits for bidirectional communication in MCP client
2//!
3//! This module provides handler traits and registration mechanisms for processing
4//! server-initiated requests. The MCP protocol is bidirectional, meaning servers
5//! can also send requests to clients for various purposes like elicitation,
6//! progress reporting, logging, and resource updates.
7//!
8//! ## Handler Types
9//!
10//! - **ElicitationHandler**: Handle user input requests from servers
11//! - **ProgressHandler**: Process progress notifications from long-running operations
12//! - **LogHandler**: Route server log messages to client logging systems
13//! - **ResourceUpdateHandler**: Handle notifications when resources change
14//!
15//! ## Usage
16//!
17//! ```rust,no_run
18//! use turbomcp_client::handlers::{ElicitationHandler, ElicitationRequest, ElicitationResponse, ElicitationAction, HandlerError};
19//! use async_trait::async_trait;
20//!
21//! // Implement elicitation handler
22//! #[derive(Debug)]
23//! struct MyElicitationHandler;
24//!
25//! #[async_trait]
26//! impl ElicitationHandler for MyElicitationHandler {
27//! async fn handle_elicitation(
28//! &self,
29//! request: ElicitationRequest,
30//! ) -> Result<ElicitationResponse, HandlerError> {
31//! // Display the prompt to the user
32//! eprintln!("\n{}", request.message());
33//! eprintln!("---");
34//!
35//! // Access the typed schema (not serde_json::Value!)
36//! let mut content = std::collections::HashMap::new();
37//! for (field_name, field_def) in &request.schema().properties {
38//! eprint!("{}: ", field_name);
39//!
40//! let mut input = String::new();
41//! std::io::stdin().read_line(&mut input)
42//! .map_err(|e| HandlerError::Generic {
43//! message: e.to_string()
44//! })?;
45//!
46//! let input = input.trim();
47//!
48//! // Parse input based on field type (from typed schema!)
49//! use turbomcp_protocol::types::PrimitiveSchemaDefinition;
50//! let value: serde_json::Value = match field_def {
51//! PrimitiveSchemaDefinition::Boolean { .. } => {
52//! serde_json::json!(input == "true" || input == "yes" || input == "1")
53//! }
54//! PrimitiveSchemaDefinition::Number { .. } | PrimitiveSchemaDefinition::Integer { .. } => {
55//! input.parse::<f64>()
56//! .map(|n| serde_json::json!(n))
57//! .unwrap_or_else(|_| serde_json::json!(input))
58//! }
59//! _ => serde_json::json!(input),
60//! };
61//!
62//! content.insert(field_name.clone(), value);
63//! }
64//!
65//! Ok(ElicitationResponse::accept(content))
66//! }
67//! }
68//! ```
69
70use async_trait::async_trait;
71use std::collections::HashMap;
72use std::sync::Arc;
73use std::time::Duration;
74use thiserror::Error;
75use tracing::{debug, error, info, warn};
76use turbomcp_protocol::MessageId;
77use turbomcp_protocol::jsonrpc::JsonRpcError;
78use turbomcp_protocol::types::LogLevel;
79
80// Re-export MCP protocol notification types directly (MCP spec compliance)
81pub use turbomcp_protocol::types::{
82 CancelledNotification, // MCP 2025-06-18 spec
83 LoggingNotification, // MCP 2025-06-18 spec
84 ProgressNotification, // MCP 2025-06-18 spec
85 ResourceUpdatedNotification, // MCP 2025-06-18 spec
86};
87
88// ============================================================================
89// ERROR TYPES FOR HANDLER OPERATIONS
90// ============================================================================
91
92/// Errors that can occur during handler operations
93#[derive(Error, Debug)]
94#[non_exhaustive]
95pub enum HandlerError {
96 /// Handler operation failed due to user cancellation
97 #[error("User cancelled the operation")]
98 UserCancelled,
99
100 /// Handler operation timed out
101 #[error("Handler operation timed out after {timeout_seconds} seconds")]
102 Timeout { timeout_seconds: u64 },
103
104 /// Input validation failed
105 #[error("Invalid input: {details}")]
106 InvalidInput { details: String },
107
108 /// Handler configuration error
109 #[error("Handler configuration error: {message}")]
110 Configuration { message: String },
111
112 /// Generic handler error
113 #[error("Handler error: {message}")]
114 Generic { message: String },
115
116 /// External system error (e.g., UI framework, database)
117 #[error("External system error: {source}")]
118 External {
119 #[from]
120 source: Box<dyn std::error::Error + Send + Sync>,
121 },
122}
123
124impl HandlerError {
125 /// Convert handler error to JSON-RPC error
126 ///
127 /// This method centralizes the mapping between handler errors and
128 /// JSON-RPC error codes, ensuring consistency across all handlers.
129 ///
130 /// # Error Code Mapping
131 ///
132 /// - **-1**: User rejected sampling request (MCP 2025-06-18 spec)
133 /// - **-32801**: Handler operation timed out
134 /// - **-32602**: Invalid input (bad request)
135 /// - **-32601**: Handler configuration error (method not found)
136 /// - **-32603**: Generic/external handler error (internal error)
137 ///
138 /// # Examples
139 ///
140 /// ```rust
141 /// use turbomcp_client::handlers::HandlerError;
142 ///
143 /// let error = HandlerError::UserCancelled;
144 /// let jsonrpc_error = error.into_jsonrpc_error();
145 /// assert_eq!(jsonrpc_error.code, -1);
146 /// assert!(jsonrpc_error.message.contains("User rejected"));
147 /// ```
148 pub fn into_jsonrpc_error(&self) -> JsonRpcError {
149 let (code, message) = match self {
150 HandlerError::UserCancelled => (-1, "User rejected sampling request".to_string()),
151 HandlerError::Timeout { timeout_seconds } => (
152 -32801,
153 format!(
154 "Handler operation timed out after {} seconds",
155 timeout_seconds
156 ),
157 ),
158 HandlerError::InvalidInput { details } => {
159 (-32602, format!("Invalid input: {}", details))
160 }
161 HandlerError::Configuration { message } => {
162 (-32601, format!("Handler configuration error: {}", message))
163 }
164 HandlerError::Generic { message } => (-32603, format!("Handler error: {}", message)),
165 HandlerError::External { source } => {
166 (-32603, format!("External system error: {}", source))
167 }
168 };
169
170 JsonRpcError {
171 code,
172 message,
173 data: None,
174 }
175 }
176}
177
178pub type HandlerResult<T> = Result<T, HandlerError>;
179
180// ============================================================================
181// ELICITATION HANDLER TRAIT
182// ============================================================================
183
184/// Ergonomic wrapper around protocol ElicitRequest with request ID
185///
186/// This type wraps the protocol-level `ElicitRequest` and adds the request ID
187/// from the JSON-RPC envelope. It provides ergonomic accessors while preserving
188/// full type safety from the protocol layer.
189///
190/// # Design Philosophy
191///
192/// Rather than duplicating protocol types, we wrap them. This ensures:
193/// - Type safety is preserved (ElicitationSchema stays typed!)
194/// - No data loss (Duration instead of lossy integer seconds)
195/// - Single source of truth (protocol crate defines MCP types)
196/// - Automatic sync (protocol changes propagate automatically)
197///
198/// # Examples
199///
200/// ```rust,no_run
201/// use turbomcp_client::handlers::ElicitationRequest;
202///
203/// async fn handle(request: ElicitationRequest) {
204/// // Access request ID
205/// println!("ID: {:?}", request.id());
206///
207/// // Access message
208/// println!("Message: {}", request.message());
209///
210/// // Access typed schema (not Value!)
211/// for (name, property) in &request.schema().properties {
212/// println!("Field: {}", name);
213/// }
214///
215/// // Access timeout as Duration
216/// if let Some(timeout) = request.timeout() {
217/// println!("Timeout: {:?}", timeout);
218/// }
219/// }
220/// ```
221#[derive(Debug, Clone)]
222pub struct ElicitationRequest {
223 id: MessageId,
224 inner: turbomcp_protocol::types::ElicitRequest,
225}
226
227impl ElicitationRequest {
228 /// Create a new elicitation request wrapper
229 ///
230 /// # Arguments
231 ///
232 /// * `id` - Request ID from JSON-RPC envelope
233 /// * `request` - Protocol-level elicit request
234 pub fn new(id: MessageId, request: turbomcp_protocol::types::ElicitRequest) -> Self {
235 Self { id, inner: request }
236 }
237
238 /// Get request ID from JSON-RPC envelope
239 pub fn id(&self) -> &MessageId {
240 &self.id
241 }
242
243 /// Get human-readable message for the user
244 ///
245 /// This is the primary prompt/question being asked of the user.
246 pub fn message(&self) -> &str {
247 &self.inner.params.message
248 }
249
250 /// Get schema defining expected response structure
251 ///
252 /// Returns the typed `ElicitationSchema` which provides:
253 /// - Type-safe access to properties
254 /// - Required field information
255 /// - Validation constraints
256 ///
257 /// # Note
258 ///
259 /// This returns a TYPED schema, not `serde_json::Value`.
260 /// You can inspect the schema structure type-safely:
261 ///
262 /// ```rust,no_run
263 /// # use turbomcp_client::handlers::ElicitationRequest;
264 /// # use turbomcp_protocol::types::PrimitiveSchemaDefinition;
265 /// # async fn example(request: ElicitationRequest) {
266 /// for (name, definition) in &request.schema().properties {
267 /// match definition {
268 /// PrimitiveSchemaDefinition::String { description, .. } => {
269 /// println!("String field: {}", name);
270 /// }
271 /// PrimitiveSchemaDefinition::Number { minimum, maximum, .. } => {
272 /// println!("Number field: {} ({:?}-{:?})", name, minimum, maximum);
273 /// }
274 /// _ => {}
275 /// }
276 /// }
277 /// # }
278 /// ```
279 pub fn schema(&self) -> &turbomcp_protocol::types::ElicitationSchema {
280 &self.inner.params.schema
281 }
282
283 /// Get optional timeout as Duration
284 ///
285 /// Converts milliseconds from the protocol to ergonomic `Duration` type.
286 /// No data loss occurs (unlike converting to integer seconds).
287 pub fn timeout(&self) -> Option<Duration> {
288 self.inner
289 .params
290 .timeout_ms
291 .map(|ms| Duration::from_millis(ms as u64))
292 }
293
294 /// Check if request can be cancelled by the user
295 pub fn is_cancellable(&self) -> bool {
296 self.inner.params.cancellable.unwrap_or(false)
297 }
298
299 /// Get access to underlying protocol request if needed
300 ///
301 /// For advanced use cases where you need the raw protocol type.
302 pub fn as_protocol(&self) -> &turbomcp_protocol::types::ElicitRequest {
303 &self.inner
304 }
305
306 /// Consume wrapper and return protocol request
307 pub fn into_protocol(self) -> turbomcp_protocol::types::ElicitRequest {
308 self.inner
309 }
310}
311
312// Re-export protocol action enum (no need to duplicate)
313pub use turbomcp_protocol::types::ElicitationAction;
314
315/// Elicitation response builder
316///
317/// Wrapper around protocol `ElicitResult` with ergonomic factory methods.
318///
319/// # Examples
320///
321/// ```rust
322/// use turbomcp_client::handlers::ElicitationResponse;
323/// use std::collections::HashMap;
324///
325/// // Accept with content
326/// let mut content = HashMap::new();
327/// content.insert("name".to_string(), serde_json::json!("Alice"));
328/// let response = ElicitationResponse::accept(content);
329///
330/// // Decline
331/// let response = ElicitationResponse::decline();
332///
333/// // Cancel
334/// let response = ElicitationResponse::cancel();
335/// ```
336#[derive(Debug, Clone)]
337pub struct ElicitationResponse {
338 inner: turbomcp_protocol::types::ElicitResult,
339}
340
341impl ElicitationResponse {
342 /// Create response with accept action and user content
343 ///
344 /// # Arguments
345 ///
346 /// * `content` - User-submitted data conforming to the request schema
347 pub fn accept(content: HashMap<String, serde_json::Value>) -> Self {
348 Self {
349 inner: turbomcp_protocol::types::ElicitResult {
350 action: ElicitationAction::Accept,
351 content: Some(content),
352 _meta: None,
353 },
354 }
355 }
356
357 /// Create response with decline action (user explicitly declined)
358 pub fn decline() -> Self {
359 Self {
360 inner: turbomcp_protocol::types::ElicitResult {
361 action: ElicitationAction::Decline,
362 content: None,
363 _meta: None,
364 },
365 }
366 }
367
368 /// Create response with cancel action (user dismissed without choice)
369 pub fn cancel() -> Self {
370 Self {
371 inner: turbomcp_protocol::types::ElicitResult {
372 action: ElicitationAction::Cancel,
373 content: None,
374 _meta: None,
375 },
376 }
377 }
378
379 /// Get the action from this response
380 pub fn action(&self) -> ElicitationAction {
381 self.inner.action
382 }
383
384 /// Get the content from this response
385 pub fn content(&self) -> Option<&HashMap<String, serde_json::Value>> {
386 self.inner.content.as_ref()
387 }
388
389 /// Convert to protocol type for sending over the wire
390 pub(crate) fn into_protocol(self) -> turbomcp_protocol::types::ElicitResult {
391 self.inner
392 }
393}
394
395/// Handler for server-initiated elicitation requests
396///
397/// Elicitation is a mechanism where servers can request user input during
398/// operations. For example, a server might need user preferences, authentication
399/// credentials, or configuration choices to complete a task.
400///
401/// Implementations should:
402/// - Present the schema/prompt to the user in an appropriate UI
403/// - Validate user input against the provided schema
404/// - Handle user cancellation gracefully
405/// - Respect timeout constraints
406///
407/// # Examples
408///
409/// ```rust,no_run
410/// use turbomcp_client::handlers::{ElicitationAction, ElicitationHandler, ElicitationRequest, ElicitationResponse, HandlerResult};
411/// use async_trait::async_trait;
412/// use serde_json::json;
413///
414/// #[derive(Debug)]
415/// struct CLIElicitationHandler;
416///
417/// #[async_trait]
418/// impl ElicitationHandler for CLIElicitationHandler {
419/// async fn handle_elicitation(
420/// &self,
421/// request: ElicitationRequest,
422/// ) -> HandlerResult<ElicitationResponse> {
423/// println!("Server request: {}", request.message());
424///
425/// // In a real implementation, you would:
426/// // 1. Inspect the typed schema to understand what input is needed
427/// // 2. Present an appropriate UI (CLI prompts, GUI forms, etc.)
428/// // 3. Validate the user's input against the schema
429/// // 4. Return the structured response
430///
431/// let mut content = std::collections::HashMap::new();
432/// content.insert("user_choice".to_string(), json!("example_value"));
433/// Ok(ElicitationResponse::accept(content))
434/// }
435/// }
436/// ```
437#[async_trait]
438pub trait ElicitationHandler: Send + Sync + std::fmt::Debug {
439 /// Handle an elicitation request from the server
440 ///
441 /// This method is called when a server needs user input. The implementation
442 /// should present the request to the user and collect their response.
443 ///
444 /// # Arguments
445 ///
446 /// * `request` - The elicitation request containing prompt, schema, and metadata
447 ///
448 /// # Returns
449 ///
450 /// Returns the user's response or an error if the operation failed.
451 async fn handle_elicitation(
452 &self,
453 request: ElicitationRequest,
454 ) -> HandlerResult<ElicitationResponse>;
455}
456
457// ============================================================================
458// PROGRESS HANDLER TRAIT
459// ============================================================================
460
461// ProgressNotification is re-exported from protocol (see imports above)
462// This ensures MCP 2025-06-18 spec compliance
463
464/// Handler for server progress notifications
465///
466/// Progress handlers receive notifications about long-running server operations.
467/// This allows clients to display progress bars, status updates, or other
468/// feedback to users during operations that take significant time.
469///
470/// # Examples
471///
472/// ```rust,no_run
473/// use turbomcp_client::handlers::{ProgressHandler, ProgressNotification, HandlerResult};
474/// use async_trait::async_trait;
475///
476/// #[derive(Debug)]
477/// struct ProgressBarHandler;
478///
479/// #[async_trait]
480/// impl ProgressHandler for ProgressBarHandler {
481/// async fn handle_progress(&self, notification: ProgressNotification) -> HandlerResult<()> {
482/// // Use MCP spec fields directly
483/// let progress_val = notification.progress;
484/// if let Some(total) = notification.total {
485/// let percentage = (progress_val / total) * 100.0;
486/// println!("Progress: {:.1}%", percentage);
487/// } else {
488/// println!("Progress: {}", progress_val);
489/// }
490///
491/// if let Some(msg) = ¬ification.message {
492/// println!("Status: {}", msg);
493/// }
494///
495/// Ok(())
496/// }
497/// }
498/// ```
499#[async_trait]
500pub trait ProgressHandler: Send + Sync + std::fmt::Debug {
501 /// Handle a progress notification from the server
502 ///
503 /// This method is called when the server sends progress updates for
504 /// long-running operations.
505 ///
506 /// # Arguments
507 ///
508 /// * `notification` - Progress information including current status and completion state
509 ///
510 /// # Returns
511 ///
512 /// Returns `Ok(())` if the notification was processed successfully.
513 async fn handle_progress(&self, notification: ProgressNotification) -> HandlerResult<()>;
514}
515
516// ============================================================================
517// LOG HANDLER TRAIT
518// ============================================================================
519
520// LoggingNotification is re-exported from protocol (see imports above)
521// This ensures MCP 2025-06-18 spec compliance
522
523/// Handler for server log messages
524///
525/// Log handlers receive log messages from the server and can route them to
526/// the client's logging system. This is useful for debugging, monitoring,
527/// and maintaining a unified log across client and server.
528///
529/// # Examples
530///
531/// ```rust,no_run
532/// use turbomcp_client::handlers::{LogHandler, LoggingNotification, HandlerResult};
533/// use turbomcp_protocol::types::LogLevel;
534/// use async_trait::async_trait;
535///
536/// #[derive(Debug)]
537/// struct TraceLogHandler;
538///
539/// #[async_trait]
540/// impl LogHandler for TraceLogHandler {
541/// async fn handle_log(&self, log: LoggingNotification) -> HandlerResult<()> {
542/// // MCP spec: data can be any JSON type (string, object, etc.)
543/// let message = log.data.to_string();
544/// match log.level {
545/// LogLevel::Error => tracing::error!("Server: {}", message),
546/// LogLevel::Warning => tracing::warn!("Server: {}", message),
547/// LogLevel::Info => tracing::info!("Server: {}", message),
548/// LogLevel::Debug => tracing::debug!("Server: {}", message),
549/// LogLevel::Notice => tracing::info!("Server: {}", message),
550/// LogLevel::Critical => tracing::error!("Server CRITICAL: {}", message),
551/// LogLevel::Alert => tracing::error!("Server ALERT: {}", message),
552/// LogLevel::Emergency => tracing::error!("Server EMERGENCY: {}", message),
553/// }
554/// Ok(())
555/// }
556/// }
557/// ```
558#[async_trait]
559pub trait LogHandler: Send + Sync + std::fmt::Debug {
560 /// Handle a log message from the server
561 ///
562 /// This method is called when the server sends log messages to the client.
563 /// Implementations can route these to the client's logging system.
564 ///
565 /// # Arguments
566 ///
567 /// * `log` - The log notification with level and data (per MCP 2025-06-18 spec)
568 ///
569 /// # Returns
570 ///
571 /// Returns `Ok(())` if the log message was processed successfully.
572 async fn handle_log(&self, log: LoggingNotification) -> HandlerResult<()>;
573}
574
575// ============================================================================
576// RESOURCE UPDATE HANDLER TRAIT
577// ============================================================================
578
579// ResourceUpdatedNotification is re-exported from protocol (see imports above)
580// This ensures MCP 2025-06-18 spec compliance
581//
582// Per MCP spec: This notification ONLY contains the URI of the changed resource.
583// Clients must call resources/read to get the updated content.
584
585/// Handler for resource update notifications
586///
587/// Resource update handlers receive notifications when resources that the
588/// client has subscribed to are modified. This enables reactive updates
589/// to cached data or UI refreshes when server-side resources change.
590///
591/// # Examples
592///
593/// ```rust,no_run
594/// use turbomcp_client::handlers::{ResourceUpdateHandler, ResourceUpdatedNotification, HandlerResult};
595/// use async_trait::async_trait;
596///
597/// #[derive(Debug)]
598/// struct CacheInvalidationHandler;
599///
600/// #[async_trait]
601/// impl ResourceUpdateHandler for CacheInvalidationHandler {
602/// async fn handle_resource_update(
603/// &self,
604/// notification: ResourceUpdatedNotification,
605/// ) -> HandlerResult<()> {
606/// // Per MCP spec: notification only contains URI
607/// // Client must call resources/read to get updated content
608/// println!("Resource {} was updated", notification.uri);
609///
610/// // In a real implementation, you might:
611/// // - Invalidate cached data for this resource
612/// // - Refresh UI components that display this resource
613/// // - Log the change for audit purposes
614/// // - Trigger dependent computations
615///
616/// Ok(())
617/// }
618/// }
619/// ```
620#[async_trait]
621pub trait ResourceUpdateHandler: Send + Sync + std::fmt::Debug {
622 /// Handle a resource update notification
623 ///
624 /// This method is called when a subscribed resource changes on the server.
625 ///
626 /// # Arguments
627 ///
628 /// * `notification` - Information about the resource change
629 ///
630 /// # Returns
631 ///
632 /// Returns `Ok(())` if the notification was processed successfully.
633 async fn handle_resource_update(
634 &self,
635 notification: ResourceUpdatedNotification,
636 ) -> HandlerResult<()>;
637}
638
639// ============================================================================
640// ROOTS HANDLER TRAIT
641// ============================================================================
642
643/// Roots handler for responding to server requests for filesystem roots
644///
645/// Per MCP 2025-06-18 specification, `roots/list` is a SERVER->CLIENT request.
646/// Servers ask clients what filesystem roots (directories/files) they have access to.
647/// This is commonly used when servers need to understand their operating boundaries,
648/// such as which repositories or project directories they can access.
649///
650/// # Examples
651///
652/// ```rust,no_run
653/// use turbomcp_client::handlers::{RootsHandler, HandlerResult};
654/// use turbomcp_protocol::types::Root;
655/// use async_trait::async_trait;
656///
657/// #[derive(Debug)]
658/// struct MyRootsHandler {
659/// project_dirs: Vec<String>,
660/// }
661///
662/// #[async_trait]
663/// impl RootsHandler for MyRootsHandler {
664/// async fn handle_roots_request(&self) -> HandlerResult<Vec<Root>> {
665/// Ok(self.project_dirs
666/// .iter()
667/// .map(|dir| Root {
668/// uri: format!("file://{}", dir).into(),
669/// name: Some(dir.split('/').last().unwrap_or("").to_string()),
670/// })
671/// .collect())
672/// }
673/// }
674/// ```
675#[async_trait]
676pub trait RootsHandler: Send + Sync + std::fmt::Debug {
677 /// Handle a roots/list request from the server
678 ///
679 /// This method is called when the server wants to know which filesystem roots
680 /// the client has available. The implementation should return a list of Root
681 /// objects representing directories or files the server can operate on.
682 ///
683 /// # Returns
684 ///
685 /// Returns a vector of Root objects, each with a URI (must start with file://)
686 /// and optional human-readable name.
687 ///
688 /// # Note
689 ///
690 /// Per MCP specification, URIs must start with `file://` for now. This restriction
691 /// may be relaxed in future protocol versions.
692 async fn handle_roots_request(&self) -> HandlerResult<Vec<turbomcp_protocol::types::Root>>;
693}
694
695// ============================================================================
696// CANCELLATION HANDLER TRAIT
697// ============================================================================
698
699/// Cancellation handler for processing cancellation notifications
700///
701/// Per MCP 2025-06-18 specification, `notifications/cancelled` can be sent by
702/// either side to indicate cancellation of a previously-issued request.
703///
704/// When the server sends a cancellation notification, it indicates that a request
705/// the client sent is being cancelled and the result will be unused. The client
706/// SHOULD cease any associated processing.
707///
708/// # MCP Specification
709///
710/// From the MCP spec:
711/// - "The request SHOULD still be in-flight, but due to communication latency,
712/// it is always possible that this notification MAY arrive after the request
713/// has already finished."
714/// - "A client MUST NOT attempt to cancel its `initialize` request."
715///
716/// # Examples
717///
718/// ```rust,no_run
719/// use turbomcp_client::handlers::{CancellationHandler, CancelledNotification, HandlerResult};
720/// use async_trait::async_trait;
721///
722/// #[derive(Debug)]
723/// struct MyCancellationHandler;
724///
725/// #[async_trait]
726/// impl CancellationHandler for MyCancellationHandler {
727/// async fn handle_cancellation(&self, notification: CancelledNotification) -> HandlerResult<()> {
728/// println!("Request {} was cancelled", notification.request_id);
729/// if let Some(reason) = ¬ification.reason {
730/// println!("Reason: {}", reason);
731/// }
732///
733/// // In a real implementation:
734/// // - Look up the in-flight request by notification.request_id
735/// // - Signal cancellation (e.g., via CancellationToken)
736/// // - Clean up any resources
737///
738/// Ok(())
739/// }
740/// }
741/// ```
742#[async_trait]
743pub trait CancellationHandler: Send + Sync + std::fmt::Debug {
744 /// Handle a cancellation notification
745 ///
746 /// This method is called when the server cancels a request that the client
747 /// previously issued.
748 ///
749 /// # Arguments
750 ///
751 /// * `notification` - The cancellation notification containing request ID and optional reason
752 ///
753 /// # Returns
754 ///
755 /// Returns `Ok(())` if the cancellation was processed successfully.
756 async fn handle_cancellation(&self, notification: CancelledNotification) -> HandlerResult<()>;
757}
758
759// ============================================================================
760// LIST CHANGED HANDLER TRAITS
761// ============================================================================
762
763/// Handler for resource list changes
764///
765/// Per MCP 2025-06-18 specification, `notifications/resources/list_changed` is
766/// an optional notification from the server to the client, informing it that the
767/// list of resources it can read from has changed.
768///
769/// This notification has no parameters - it simply signals that the client should
770/// re-query the server's resource list if needed.
771///
772/// # Examples
773///
774/// ```rust,no_run
775/// use turbomcp_client::handlers::{ResourceListChangedHandler, HandlerResult};
776/// use async_trait::async_trait;
777///
778/// #[derive(Debug)]
779/// struct MyResourceListHandler;
780///
781/// #[async_trait]
782/// impl ResourceListChangedHandler for MyResourceListHandler {
783/// async fn handle_resource_list_changed(&self) -> HandlerResult<()> {
784/// println!("Server's resource list changed - refreshing...");
785/// // In a real implementation, re-query: client.list_resources().await
786/// Ok(())
787/// }
788/// }
789/// ```
790#[async_trait]
791pub trait ResourceListChangedHandler: Send + Sync + std::fmt::Debug {
792 /// Handle a resource list changed notification
793 ///
794 /// This method is called when the server's available resource list changes.
795 ///
796 /// # Returns
797 ///
798 /// Returns `Ok(())` if the notification was processed successfully.
799 async fn handle_resource_list_changed(&self) -> HandlerResult<()>;
800}
801
802/// Handler for prompt list changes
803///
804/// Per MCP 2025-06-18 specification, `notifications/prompts/list_changed` is
805/// an optional notification from the server to the client, informing it that the
806/// list of prompts it offers has changed.
807///
808/// # Examples
809///
810/// ```rust,no_run
811/// use turbomcp_client::handlers::{PromptListChangedHandler, HandlerResult};
812/// use async_trait::async_trait;
813///
814/// #[derive(Debug)]
815/// struct MyPromptListHandler;
816///
817/// #[async_trait]
818/// impl PromptListChangedHandler for MyPromptListHandler {
819/// async fn handle_prompt_list_changed(&self) -> HandlerResult<()> {
820/// println!("Server's prompt list changed - refreshing...");
821/// Ok(())
822/// }
823/// }
824/// ```
825#[async_trait]
826pub trait PromptListChangedHandler: Send + Sync + std::fmt::Debug {
827 /// Handle a prompt list changed notification
828 ///
829 /// This method is called when the server's available prompt list changes.
830 ///
831 /// # Returns
832 ///
833 /// Returns `Ok(())` if the notification was processed successfully.
834 async fn handle_prompt_list_changed(&self) -> HandlerResult<()>;
835}
836
837/// Handler for tool list changes
838///
839/// Per MCP 2025-06-18 specification, `notifications/tools/list_changed` is
840/// an optional notification from the server to the client, informing it that the
841/// list of tools it offers has changed.
842///
843/// # Examples
844///
845/// ```rust,no_run
846/// use turbomcp_client::handlers::{ToolListChangedHandler, HandlerResult};
847/// use async_trait::async_trait;
848///
849/// #[derive(Debug)]
850/// struct MyToolListHandler;
851///
852/// #[async_trait]
853/// impl ToolListChangedHandler for MyToolListHandler {
854/// async fn handle_tool_list_changed(&self) -> HandlerResult<()> {
855/// println!("Server's tool list changed - refreshing...");
856/// Ok(())
857/// }
858/// }
859/// ```
860#[async_trait]
861pub trait ToolListChangedHandler: Send + Sync + std::fmt::Debug {
862 /// Handle a tool list changed notification
863 ///
864 /// This method is called when the server's available tool list changes.
865 ///
866 /// # Returns
867 ///
868 /// Returns `Ok(())` if the notification was processed successfully.
869 async fn handle_tool_list_changed(&self) -> HandlerResult<()>;
870}
871
872// ============================================================================
873// HANDLER REGISTRY FOR CLIENT
874// ============================================================================
875
876/// Registry for managing client-side handlers
877///
878/// This registry holds all the handler implementations and provides methods
879/// for registering and invoking them. It's used internally by the Client
880/// to dispatch server-initiated requests to the appropriate handlers.
881#[derive(Debug, Default)]
882pub struct HandlerRegistry {
883 /// Roots handler for filesystem root requests
884 pub roots: Option<Arc<dyn RootsHandler>>,
885
886 /// Elicitation handler for user input requests
887 pub elicitation: Option<Arc<dyn ElicitationHandler>>,
888
889 /// Progress handler for operation updates
890 pub progress: Option<Arc<dyn ProgressHandler>>,
891
892 /// Log handler for server log messages
893 pub log: Option<Arc<dyn LogHandler>>,
894
895 /// Resource update handler for resource change notifications
896 pub resource_update: Option<Arc<dyn ResourceUpdateHandler>>,
897
898 /// Cancellation handler for cancellation notifications
899 pub cancellation: Option<Arc<dyn CancellationHandler>>,
900
901 /// Resource list changed handler
902 pub resource_list_changed: Option<Arc<dyn ResourceListChangedHandler>>,
903
904 /// Prompt list changed handler
905 pub prompt_list_changed: Option<Arc<dyn PromptListChangedHandler>>,
906
907 /// Tool list changed handler
908 pub tool_list_changed: Option<Arc<dyn ToolListChangedHandler>>,
909}
910
911impl HandlerRegistry {
912 /// Create a new empty handler registry
913 pub fn new() -> Self {
914 Self::default()
915 }
916
917 /// Register a roots handler
918 pub fn set_roots_handler(&mut self, handler: Arc<dyn RootsHandler>) {
919 debug!("Registering roots handler");
920 self.roots = Some(handler);
921 }
922
923 /// Register an elicitation handler
924 pub fn set_elicitation_handler(&mut self, handler: Arc<dyn ElicitationHandler>) {
925 debug!("Registering elicitation handler");
926 self.elicitation = Some(handler);
927 }
928
929 /// Register a progress handler
930 pub fn set_progress_handler(&mut self, handler: Arc<dyn ProgressHandler>) {
931 debug!("Registering progress handler");
932 self.progress = Some(handler);
933 }
934
935 /// Register a log handler
936 pub fn set_log_handler(&mut self, handler: Arc<dyn LogHandler>) {
937 debug!("Registering log handler");
938 self.log = Some(handler);
939 }
940
941 /// Register a resource update handler
942 pub fn set_resource_update_handler(&mut self, handler: Arc<dyn ResourceUpdateHandler>) {
943 debug!("Registering resource update handler");
944 self.resource_update = Some(handler);
945 }
946
947 /// Register a cancellation handler
948 pub fn set_cancellation_handler(&mut self, handler: Arc<dyn CancellationHandler>) {
949 debug!("Registering cancellation handler");
950 self.cancellation = Some(handler);
951 }
952
953 /// Register a resource list changed handler
954 pub fn set_resource_list_changed_handler(
955 &mut self,
956 handler: Arc<dyn ResourceListChangedHandler>,
957 ) {
958 debug!("Registering resource list changed handler");
959 self.resource_list_changed = Some(handler);
960 }
961
962 /// Register a prompt list changed handler
963 pub fn set_prompt_list_changed_handler(&mut self, handler: Arc<dyn PromptListChangedHandler>) {
964 debug!("Registering prompt list changed handler");
965 self.prompt_list_changed = Some(handler);
966 }
967
968 /// Register a tool list changed handler
969 pub fn set_tool_list_changed_handler(&mut self, handler: Arc<dyn ToolListChangedHandler>) {
970 debug!("Registering tool list changed handler");
971 self.tool_list_changed = Some(handler);
972 }
973
974 /// Check if a roots handler is registered
975 pub fn has_roots_handler(&self) -> bool {
976 self.roots.is_some()
977 }
978
979 /// Check if an elicitation handler is registered
980 pub fn has_elicitation_handler(&self) -> bool {
981 self.elicitation.is_some()
982 }
983
984 /// Check if a progress handler is registered
985 pub fn has_progress_handler(&self) -> bool {
986 self.progress.is_some()
987 }
988
989 /// Check if a log handler is registered
990 pub fn has_log_handler(&self) -> bool {
991 self.log.is_some()
992 }
993
994 /// Check if a resource update handler is registered
995 pub fn has_resource_update_handler(&self) -> bool {
996 self.resource_update.is_some()
997 }
998
999 /// Get the progress handler if registered
1000 pub fn get_progress_handler(&self) -> Option<Arc<dyn ProgressHandler>> {
1001 self.progress.clone()
1002 }
1003
1004 /// Get the log handler if registered
1005 pub fn get_log_handler(&self) -> Option<Arc<dyn LogHandler>> {
1006 self.log.clone()
1007 }
1008
1009 /// Get the resource update handler if registered
1010 pub fn get_resource_update_handler(&self) -> Option<Arc<dyn ResourceUpdateHandler>> {
1011 self.resource_update.clone()
1012 }
1013
1014 /// Get the cancellation handler if registered
1015 pub fn get_cancellation_handler(&self) -> Option<Arc<dyn CancellationHandler>> {
1016 self.cancellation.clone()
1017 }
1018
1019 /// Get the resource list changed handler if registered
1020 pub fn get_resource_list_changed_handler(&self) -> Option<Arc<dyn ResourceListChangedHandler>> {
1021 self.resource_list_changed.clone()
1022 }
1023
1024 /// Get the prompt list changed handler if registered
1025 pub fn get_prompt_list_changed_handler(&self) -> Option<Arc<dyn PromptListChangedHandler>> {
1026 self.prompt_list_changed.clone()
1027 }
1028
1029 /// Get the tool list changed handler if registered
1030 pub fn get_tool_list_changed_handler(&self) -> Option<Arc<dyn ToolListChangedHandler>> {
1031 self.tool_list_changed.clone()
1032 }
1033
1034 /// Handle a roots/list request from the server
1035 pub async fn handle_roots_request(&self) -> HandlerResult<Vec<turbomcp_protocol::types::Root>> {
1036 match &self.roots {
1037 Some(handler) => {
1038 info!("Processing roots/list request from server");
1039 handler.handle_roots_request().await
1040 }
1041 None => {
1042 warn!("No roots handler registered, returning empty roots list");
1043 // Return empty list per MCP spec - client has no roots available
1044 Ok(Vec::new())
1045 }
1046 }
1047 }
1048
1049 /// Handle an elicitation request
1050 pub async fn handle_elicitation(
1051 &self,
1052 request: ElicitationRequest,
1053 ) -> HandlerResult<ElicitationResponse> {
1054 match &self.elicitation {
1055 Some(handler) => {
1056 info!("Processing elicitation request: {}", request.id);
1057 handler.handle_elicitation(request).await
1058 }
1059 None => {
1060 warn!("No elicitation handler registered, declining request");
1061 Err(HandlerError::Configuration {
1062 message: "No elicitation handler registered".to_string(),
1063 })
1064 }
1065 }
1066 }
1067
1068 /// Handle a progress notification
1069 pub async fn handle_progress(&self, notification: ProgressNotification) -> HandlerResult<()> {
1070 match &self.progress {
1071 Some(handler) => {
1072 debug!("Processing progress notification");
1073 handler.handle_progress(notification).await
1074 }
1075 None => {
1076 debug!("No progress handler registered, ignoring notification");
1077 Ok(())
1078 }
1079 }
1080 }
1081
1082 /// Handle a log message
1083 pub async fn handle_log(&self, log: LoggingNotification) -> HandlerResult<()> {
1084 match &self.log {
1085 Some(handler) => handler.handle_log(log).await,
1086 None => {
1087 debug!("No log handler registered, ignoring log message");
1088 Ok(())
1089 }
1090 }
1091 }
1092
1093 /// Handle a resource update notification
1094 pub async fn handle_resource_update(
1095 &self,
1096 notification: ResourceUpdatedNotification,
1097 ) -> HandlerResult<()> {
1098 match &self.resource_update {
1099 Some(handler) => {
1100 debug!("Processing resource update: {}", notification.uri);
1101 handler.handle_resource_update(notification).await
1102 }
1103 None => {
1104 debug!("No resource update handler registered, ignoring notification");
1105 Ok(())
1106 }
1107 }
1108 }
1109}
1110
1111// ============================================================================
1112// DEFAULT HANDLER IMPLEMENTATIONS
1113// ============================================================================
1114
1115/// Default elicitation handler that declines all requests
1116#[derive(Debug)]
1117pub struct DeclineElicitationHandler;
1118
1119#[async_trait]
1120impl ElicitationHandler for DeclineElicitationHandler {
1121 async fn handle_elicitation(
1122 &self,
1123 request: ElicitationRequest,
1124 ) -> HandlerResult<ElicitationResponse> {
1125 warn!("Declining elicitation request: {}", request.message());
1126 Ok(ElicitationResponse::decline())
1127 }
1128}
1129
1130/// Default progress handler that logs progress to tracing
1131#[derive(Debug)]
1132pub struct LoggingProgressHandler;
1133
1134#[async_trait]
1135impl ProgressHandler for LoggingProgressHandler {
1136 async fn handle_progress(&self, notification: ProgressNotification) -> HandlerResult<()> {
1137 // Per MCP spec: progress notifications have progress, total (optional), message (optional)
1138 let progress = notification.progress;
1139 let progress_str = if let Some(total) = notification.total {
1140 format!("{}/{}", progress, total)
1141 } else {
1142 format!("{}", progress)
1143 };
1144
1145 if let Some(message) = ¬ification.message {
1146 info!("Progress {}: {}", progress_str, message);
1147 } else {
1148 info!("Progress: {}", progress_str);
1149 }
1150
1151 Ok(())
1152 }
1153}
1154
1155/// Default log handler that routes server logs to tracing
1156#[derive(Debug)]
1157pub struct TracingLogHandler;
1158
1159#[async_trait]
1160impl LogHandler for TracingLogHandler {
1161 async fn handle_log(&self, log: LoggingNotification) -> HandlerResult<()> {
1162 let logger_prefix = log.logger.as_deref().unwrap_or("server");
1163
1164 // Per MCP spec: data can be any JSON type (string, object, etc.)
1165 let message = log.data.to_string();
1166 match log.level {
1167 LogLevel::Error => error!("[{}] {}", logger_prefix, message),
1168 LogLevel::Warning => warn!("[{}] {}", logger_prefix, message),
1169 LogLevel::Info => info!("[{}] {}", logger_prefix, message),
1170 LogLevel::Debug => debug!("[{}] {}", logger_prefix, message),
1171 LogLevel::Notice => info!("[{}] [NOTICE] {}", logger_prefix, message),
1172 LogLevel::Critical => error!("[{}] [CRITICAL] {}", logger_prefix, message),
1173 LogLevel::Alert => error!("[{}] [ALERT] {}", logger_prefix, message),
1174 LogLevel::Emergency => error!("[{}] [EMERGENCY] {}", logger_prefix, message),
1175 }
1176
1177 Ok(())
1178 }
1179}
1180
1181/// Default resource update handler that logs changes
1182#[derive(Debug)]
1183pub struct LoggingResourceUpdateHandler;
1184
1185#[async_trait]
1186impl ResourceUpdateHandler for LoggingResourceUpdateHandler {
1187 async fn handle_resource_update(
1188 &self,
1189 notification: ResourceUpdatedNotification,
1190 ) -> HandlerResult<()> {
1191 // Per MCP spec: notification only contains URI
1192 info!("Resource {} was updated", notification.uri);
1193 Ok(())
1194 }
1195}
1196
1197/// Default cancellation handler that logs cancellation notifications
1198#[derive(Debug)]
1199pub struct LoggingCancellationHandler;
1200
1201#[async_trait]
1202impl CancellationHandler for LoggingCancellationHandler {
1203 async fn handle_cancellation(&self, notification: CancelledNotification) -> HandlerResult<()> {
1204 if let Some(reason) = ¬ification.reason {
1205 info!(
1206 "Request {} was cancelled: {}",
1207 notification.request_id, reason
1208 );
1209 } else {
1210 info!("Request {} was cancelled", notification.request_id);
1211 }
1212 Ok(())
1213 }
1214}
1215
1216/// Default resource list changed handler that logs changes
1217#[derive(Debug)]
1218pub struct LoggingResourceListChangedHandler;
1219
1220#[async_trait]
1221impl ResourceListChangedHandler for LoggingResourceListChangedHandler {
1222 async fn handle_resource_list_changed(&self) -> HandlerResult<()> {
1223 info!("Server's resource list changed");
1224 Ok(())
1225 }
1226}
1227
1228/// Default prompt list changed handler that logs changes
1229#[derive(Debug)]
1230pub struct LoggingPromptListChangedHandler;
1231
1232#[async_trait]
1233impl PromptListChangedHandler for LoggingPromptListChangedHandler {
1234 async fn handle_prompt_list_changed(&self) -> HandlerResult<()> {
1235 info!("Server's prompt list changed");
1236 Ok(())
1237 }
1238}
1239
1240/// Default tool list changed handler that logs changes
1241#[derive(Debug)]
1242pub struct LoggingToolListChangedHandler;
1243
1244#[async_trait]
1245impl ToolListChangedHandler for LoggingToolListChangedHandler {
1246 async fn handle_tool_list_changed(&self) -> HandlerResult<()> {
1247 info!("Server's tool list changed");
1248 Ok(())
1249 }
1250}
1251
1252#[cfg(test)]
1253mod tests {
1254 use super::*;
1255 use serde_json::json;
1256 use tokio;
1257
1258 // Test handler implementations
1259 #[derive(Debug)]
1260 struct TestElicitationHandler;
1261
1262 #[async_trait]
1263 impl ElicitationHandler for TestElicitationHandler {
1264 async fn handle_elicitation(
1265 &self,
1266 _request: ElicitationRequest,
1267 ) -> HandlerResult<ElicitationResponse> {
1268 let mut content = HashMap::new();
1269 content.insert("test".to_string(), json!("response"));
1270 Ok(ElicitationResponse::accept(content))
1271 }
1272 }
1273
1274 #[derive(Debug)]
1275 struct TestProgressHandler;
1276
1277 #[async_trait]
1278 impl ProgressHandler for TestProgressHandler {
1279 async fn handle_progress(&self, _notification: ProgressNotification) -> HandlerResult<()> {
1280 Ok(())
1281 }
1282 }
1283
1284 #[tokio::test]
1285 async fn test_handler_registry_creation() {
1286 let registry = HandlerRegistry::new();
1287 assert!(!registry.has_elicitation_handler());
1288 assert!(!registry.has_progress_handler());
1289 assert!(!registry.has_log_handler());
1290 assert!(!registry.has_resource_update_handler());
1291 }
1292
1293 #[tokio::test]
1294 async fn test_elicitation_handler_registration() {
1295 let mut registry = HandlerRegistry::new();
1296 let handler = Arc::new(TestElicitationHandler);
1297
1298 registry.set_elicitation_handler(handler);
1299 assert!(registry.has_elicitation_handler());
1300 }
1301
1302 #[tokio::test]
1303 async fn test_elicitation_request_handling() {
1304 let mut registry = HandlerRegistry::new();
1305 let handler = Arc::new(TestElicitationHandler);
1306 registry.set_elicitation_handler(handler);
1307
1308 // Create protocol request
1309 let proto_request = turbomcp_protocol::types::ElicitRequest {
1310 params: turbomcp_protocol::types::ElicitRequestParams {
1311 message: "Test prompt".to_string(),
1312 schema: turbomcp_protocol::types::ElicitationSchema::new(),
1313 timeout_ms: None,
1314 cancellable: None,
1315 },
1316 _meta: None,
1317 };
1318
1319 // Wrap for handler
1320 let request = ElicitationRequest::new(
1321 turbomcp_protocol::MessageId::String("test-123".to_string()),
1322 proto_request,
1323 );
1324
1325 let response = registry.handle_elicitation(request).await.unwrap();
1326 assert_eq!(response.action(), ElicitationAction::Accept);
1327 assert!(response.content().is_some());
1328 }
1329
1330 #[tokio::test]
1331 async fn test_progress_handler_registration() {
1332 let mut registry = HandlerRegistry::new();
1333 let handler = Arc::new(TestProgressHandler);
1334
1335 registry.set_progress_handler(handler);
1336 assert!(registry.has_progress_handler());
1337 }
1338
1339 #[tokio::test]
1340 async fn test_default_handlers() {
1341 let decline_handler = DeclineElicitationHandler;
1342
1343 // Create protocol request
1344 let proto_request = turbomcp_protocol::types::ElicitRequest {
1345 params: turbomcp_protocol::types::ElicitRequestParams {
1346 message: "Test".to_string(),
1347 schema: turbomcp_protocol::types::ElicitationSchema::new(),
1348 timeout_ms: None,
1349 cancellable: None,
1350 },
1351 _meta: None,
1352 };
1353
1354 // Wrap for handler
1355 let request = ElicitationRequest::new(
1356 turbomcp_protocol::MessageId::String("test".to_string()),
1357 proto_request,
1358 );
1359
1360 let response = decline_handler.handle_elicitation(request).await.unwrap();
1361 assert_eq!(response.action(), ElicitationAction::Decline);
1362 }
1363
1364 #[tokio::test]
1365 async fn test_handler_error_types() {
1366 let error = HandlerError::UserCancelled;
1367 assert!(error.to_string().contains("User cancelled"));
1368
1369 let timeout_error = HandlerError::Timeout {
1370 timeout_seconds: 30,
1371 };
1372 assert!(timeout_error.to_string().contains("30 seconds"));
1373 }
1374
1375 // ========================================================================
1376 // JSON-RPC Error Mapping Tests
1377 // ========================================================================
1378
1379 #[test]
1380 fn test_user_cancelled_error_mapping() {
1381 let error = HandlerError::UserCancelled;
1382 let jsonrpc_error = error.into_jsonrpc_error();
1383
1384 assert_eq!(
1385 jsonrpc_error.code, -1,
1386 "User cancelled should map to -1 per MCP 2025-06-18 spec"
1387 );
1388 assert!(jsonrpc_error.message.contains("User rejected"));
1389 assert!(jsonrpc_error.data.is_none());
1390 }
1391
1392 #[test]
1393 fn test_timeout_error_mapping() {
1394 let error = HandlerError::Timeout {
1395 timeout_seconds: 30,
1396 };
1397 let jsonrpc_error = error.into_jsonrpc_error();
1398
1399 assert_eq!(jsonrpc_error.code, -32801, "Timeout should map to -32801");
1400 assert!(jsonrpc_error.message.contains("30 seconds"));
1401 assert!(jsonrpc_error.data.is_none());
1402 }
1403
1404 #[test]
1405 fn test_invalid_input_error_mapping() {
1406 let error = HandlerError::InvalidInput {
1407 details: "Missing required field".to_string(),
1408 };
1409 let jsonrpc_error = error.into_jsonrpc_error();
1410
1411 assert_eq!(
1412 jsonrpc_error.code, -32602,
1413 "Invalid input should map to -32602"
1414 );
1415 assert!(jsonrpc_error.message.contains("Invalid input"));
1416 assert!(jsonrpc_error.message.contains("Missing required field"));
1417 assert!(jsonrpc_error.data.is_none());
1418 }
1419
1420 #[test]
1421 fn test_configuration_error_mapping() {
1422 let error = HandlerError::Configuration {
1423 message: "Handler not registered".to_string(),
1424 };
1425 let jsonrpc_error = error.into_jsonrpc_error();
1426
1427 assert_eq!(
1428 jsonrpc_error.code, -32601,
1429 "Configuration error should map to -32601"
1430 );
1431 assert!(
1432 jsonrpc_error
1433 .message
1434 .contains("Handler configuration error")
1435 );
1436 assert!(jsonrpc_error.message.contains("Handler not registered"));
1437 assert!(jsonrpc_error.data.is_none());
1438 }
1439
1440 #[test]
1441 fn test_generic_error_mapping() {
1442 let error = HandlerError::Generic {
1443 message: "Something went wrong".to_string(),
1444 };
1445 let jsonrpc_error = error.into_jsonrpc_error();
1446
1447 assert_eq!(
1448 jsonrpc_error.code, -32603,
1449 "Generic error should map to -32603"
1450 );
1451 assert!(jsonrpc_error.message.contains("Handler error"));
1452 assert!(jsonrpc_error.message.contains("Something went wrong"));
1453 assert!(jsonrpc_error.data.is_none());
1454 }
1455
1456 #[test]
1457 fn test_external_error_mapping() {
1458 let external_err = Box::new(std::io::Error::other("Database connection failed"));
1459 let error = HandlerError::External {
1460 source: external_err,
1461 };
1462 let jsonrpc_error = error.into_jsonrpc_error();
1463
1464 assert_eq!(
1465 jsonrpc_error.code, -32603,
1466 "External error should map to -32603"
1467 );
1468 assert!(jsonrpc_error.message.contains("External system error"));
1469 assert!(jsonrpc_error.message.contains("Database connection failed"));
1470 assert!(jsonrpc_error.data.is_none());
1471 }
1472
1473 #[test]
1474 fn test_error_code_uniqueness() {
1475 // Verify that user-facing errors have unique codes
1476 let user_cancelled = HandlerError::UserCancelled.into_jsonrpc_error().code;
1477 let timeout = HandlerError::Timeout { timeout_seconds: 1 }
1478 .into_jsonrpc_error()
1479 .code;
1480 let invalid_input = HandlerError::InvalidInput {
1481 details: "test".to_string(),
1482 }
1483 .into_jsonrpc_error()
1484 .code;
1485 let configuration = HandlerError::Configuration {
1486 message: "test".to_string(),
1487 }
1488 .into_jsonrpc_error()
1489 .code;
1490
1491 // These should all be different
1492 assert_ne!(user_cancelled, timeout);
1493 assert_ne!(user_cancelled, invalid_input);
1494 assert_ne!(user_cancelled, configuration);
1495 assert_ne!(timeout, invalid_input);
1496 assert_ne!(timeout, configuration);
1497 assert_ne!(invalid_input, configuration);
1498 }
1499
1500 #[test]
1501 fn test_error_messages_are_informative() {
1502 // Verify all error messages contain useful information
1503 let errors = vec![
1504 HandlerError::UserCancelled,
1505 HandlerError::Timeout {
1506 timeout_seconds: 42,
1507 },
1508 HandlerError::InvalidInput {
1509 details: "test detail".to_string(),
1510 },
1511 HandlerError::Configuration {
1512 message: "test config".to_string(),
1513 },
1514 HandlerError::Generic {
1515 message: "test generic".to_string(),
1516 },
1517 ];
1518
1519 for error in errors {
1520 let jsonrpc_error = error.into_jsonrpc_error();
1521 assert!(
1522 !jsonrpc_error.message.is_empty(),
1523 "Error message should not be empty"
1524 );
1525 assert!(
1526 jsonrpc_error.message.len() > 10,
1527 "Error message should be descriptive"
1528 );
1529 }
1530 }
1531}