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