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