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