mcpkit_core/
error.rs

1//! Unified error handling for the MCP SDK.
2//!
3//! This module provides a single, context-rich error type that replaces
4//! the nested error hierarchies found in other implementations.
5//!
6//! # Design Philosophy
7//!
8//! - **Single error type**: All errors flow through [`McpError`]
9//! - **Rich context**: Errors preserve context through the entire call stack
10//! - **JSON-RPC compatible**: Easy conversion to JSON-RPC error responses
11//! - **Diagnostic-friendly**: Integrates with [`miette`] for beautiful error reports
12//! - **Size-optimized**: Large error variants are boxed to keep `Result<T, McpError>` small
13//!
14//! # Error Handling Patterns
15//!
16//! The SDK has two distinct error handling patterns for different scenarios:
17//!
18//! ## Pattern 1: `Result<T, McpError>` - For SDK/Framework Errors
19//!
20//! Use `Result<T, McpError>` for errors that indicate something went wrong
21//! with the MCP protocol, transport, or SDK internals:
22//!
23//! - **Transport failures**: Connection lost, timeout, I/O errors
24//! - **Protocol errors**: Invalid JSON-RPC, version mismatch, missing fields
25//! - **Resource not found**: Requested resource/tool/prompt doesn't exist
26//! - **Capability errors**: Feature not supported by client/server
27//! - **Internal errors**: Unexpected SDK state, serialization failures
28//!
29//! These errors typically indicate the request cannot be completed and
30//! require intervention (reconnection, configuration change, bug fix).
31//!
32//! ```rust,no_run
33//! # use mcpkit_core::error::McpError;
34//! # struct Tool;
35//! # struct ListResult { tools: Vec<Tool> }
36//! # struct Client;
37//! # impl Client {
38//! #     fn has_tools(&self) -> bool { true }
39//! #     fn ensure_capability(&self, _: &str, _: bool) -> Result<(), McpError> { Ok(()) }
40//! #     async fn request(&self, _: &str, _: Option<()>) -> Result<ListResult, McpError> { Ok(ListResult { tools: vec![] }) }
41//! async fn list_tools(&self) -> Result<Vec<Tool>, McpError> {
42//!     self.ensure_capability("tools", self.has_tools())?;
43//!     // Transport errors propagate as McpError
44//!     let result = self.request("tools/list", None).await?;
45//!     Ok(result.tools)
46//! }
47//! # }
48//! ```
49//!
50//! ## Pattern 2: `ToolOutput::RecoverableError` - For User/LLM-Correctable Errors
51//!
52//! Use [`ToolOutput::error()`](crate::types::ToolOutput::error) for errors that the LLM
53//! can potentially self-correct by adjusting its input:
54//!
55//! - **Validation failures**: Invalid argument format, out-of-range values
56//! - **Business logic errors**: Division by zero, empty query, invalid date
57//! - **Missing optional data**: Lookup returned no results
58//! - **Rate limiting**: Too many requests (suggest retry)
59//!
60//! These errors are returned to the LLM with `is_error: true` in the response,
61//! allowing the model to understand what went wrong and try again.
62//!
63//! ```rust,no_run
64//! # use mcpkit_core::types::ToolOutput;
65//! # struct Calculator;
66//! # impl Calculator {
67//! // #[tool(description = "Divide two numbers")]
68//! async fn divide(&self, a: f64, b: f64) -> ToolOutput {
69//!     if b == 0.0 {
70//!         return ToolOutput::error_with_suggestion(
71//!             "Cannot divide by zero",
72//!             "Use a non-zero divisor",
73//!         );
74//!     }
75//!     ToolOutput::text((a / b).to_string())
76//! }
77//! # }
78//! ```
79//!
80//! ## Decision Guide
81//!
82//! | Scenario | Use | Reason |
83//! |----------|-----|--------|
84//! | Database connection failed | `McpError` | Infrastructure issue |
85//! | User provided invalid email format | `ToolOutput::error` | LLM can fix input |
86//! | Tool doesn't exist | `McpError` | Protocol/discovery issue |
87//! | Search returned no results | `ToolOutput::text("No results")` | Expected outcome |
88//! | API rate limit exceeded | `ToolOutput::error_with_suggestion` | Temporary, can retry |
89//! | Authentication required | `McpError` | Configuration issue |
90//! | Invalid number format in input | `ToolOutput::error` | LLM can fix input |
91//!
92//! ## Context Chaining
93//!
94//! For `McpError`, use context chaining to provide detailed diagnostics:
95//!
96//! ```rust
97//! use mcpkit_core::error::{McpError, McpResultExt};
98//!
99//! fn fetch_data() -> Result<String, McpError> {
100//!     let user_id = 42;
101//!     // Errors automatically get context
102//!     let result: Result<(), McpError> = Err(McpError::resource_not_found("user://42"));
103//!     result
104//!         .context("Failed to fetch user data")
105//!         .with_context(|| format!("User ID: {}", user_id))?;
106//!     Ok("data".to_string())
107//! }
108//! ```
109
110use miette::Diagnostic;
111use serde::{Deserialize, Serialize};
112use std::fmt;
113use thiserror::Error;
114
115/// Type alias for boxed errors that are Send + Sync.
116pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
117
118// ============================================================================
119// Boxed Error Detail Types (to reduce McpError enum size)
120// ============================================================================
121
122/// Details for invalid params errors (boxed to reduce enum size).
123#[derive(Debug)]
124pub struct InvalidParamsDetails {
125    /// The method that received invalid parameters.
126    pub method: String,
127    /// Human-readable error message.
128    pub message: String,
129    /// The parameter path that failed (e.g., "arguments.query").
130    pub param_path: Option<String>,
131    /// The expected type or format.
132    pub expected: Option<String>,
133    /// The actual value received (truncated/redacted if needed).
134    pub actual: Option<String>,
135    /// The underlying error, if available.
136    pub source: Option<BoxError>,
137}
138
139impl fmt::Display for InvalidParamsDetails {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        write!(f, "Invalid params for '{}': {}", self.method, self.message)
142    }
143}
144
145impl std::error::Error for InvalidParamsDetails {
146    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
147        self.source.as_ref().map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
148    }
149}
150
151/// Details for transport errors (boxed to reduce enum size).
152#[derive(Debug)]
153pub struct TransportDetails {
154    /// Classification of the transport error.
155    pub kind: TransportErrorKind,
156    /// Human-readable error message.
157    pub message: String,
158    /// Transport-specific context for debugging.
159    pub context: TransportContext,
160    /// The underlying error, if available.
161    pub source: Option<BoxError>,
162}
163
164impl fmt::Display for TransportDetails {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        write!(f, "Transport error ({}): {}", self.kind, self.message)
167    }
168}
169
170impl std::error::Error for TransportDetails {
171    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
172        self.source.as_ref().map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
173    }
174}
175
176/// Details for tool execution errors (boxed to reduce enum size).
177#[derive(Debug)]
178pub struct ToolExecutionDetails {
179    /// The name of the tool that failed.
180    pub tool: String,
181    /// Human-readable error message.
182    pub message: String,
183    /// Whether the LLM should see this error for self-correction.
184    pub is_recoverable: bool,
185    /// Additional structured error data.
186    pub data: Option<serde_json::Value>,
187    /// The underlying error, if available.
188    pub source: Option<BoxError>,
189}
190
191impl fmt::Display for ToolExecutionDetails {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        write!(f, "Tool '{}' failed: {}", self.tool, self.message)
194    }
195}
196
197impl std::error::Error for ToolExecutionDetails {
198    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
199        self.source.as_ref().map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
200    }
201}
202
203/// Details for handshake errors (boxed to reduce enum size).
204#[derive(Debug)]
205pub struct HandshakeDetails {
206    /// Human-readable error message.
207    pub message: String,
208    /// Client protocol version, if available.
209    pub client_version: Option<String>,
210    /// Server protocol version, if available.
211    pub server_version: Option<String>,
212    /// The underlying error, if available.
213    pub source: Option<BoxError>,
214}
215
216impl fmt::Display for HandshakeDetails {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        write!(f, "Handshake failed: {}", self.message)
219    }
220}
221
222impl std::error::Error for HandshakeDetails {
223    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
224        self.source.as_ref().map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
225    }
226}
227
228/// The primary error type for the MCP SDK.
229///
230/// This unified error type replaces nested error hierarchies with a single,
231/// context-rich type that preserves error chains and provides excellent
232/// diagnostic output.
233///
234/// Large error variants are boxed to keep `Result<T, McpError>` small
235/// (approximately 24 bytes on 64-bit systems).
236#[derive(Error, Diagnostic, Debug)]
237#[allow(clippy::large_enum_variant)] // We've intentionally boxed large variants
238pub enum McpError {
239    // ========================================================================
240    // JSON-RPC Protocol Errors (-32700 to -32600)
241    // ========================================================================
242    /// Invalid JSON was received by the server.
243    #[error("Parse error: {message}")]
244    #[diagnostic(
245        code(mcp::protocol::parse_error),
246        help("Ensure the message is valid JSON-RPC 2.0 format")
247    )]
248    Parse {
249        /// Human-readable error message.
250        message: String,
251        /// The underlying parse error, if available.
252        #[source]
253        source: Option<BoxError>,
254    },
255
256    /// The JSON sent is not a valid Request object.
257    #[error("Invalid request: {message}")]
258    #[diagnostic(code(mcp::protocol::invalid_request))]
259    InvalidRequest {
260        /// Human-readable error message.
261        message: String,
262        /// The underlying error, if available.
263        #[source]
264        source: Option<BoxError>,
265    },
266
267    /// The method does not exist or is not available.
268    #[error("Method not found: {method}")]
269    #[diagnostic(code(mcp::protocol::method_not_found))]
270    MethodNotFound {
271        /// The method that was requested.
272        method: String,
273        /// List of available methods for suggestions (boxed to reduce size).
274        available: Box<[String]>,
275    },
276
277    /// Invalid method parameter(s) (details boxed to reduce enum size).
278    #[error("Invalid params for '{}': {}", .0.method, .0.message)]
279    #[diagnostic(code(mcp::protocol::invalid_params))]
280    InvalidParams(#[source] Box<InvalidParamsDetails>),
281
282    /// Internal JSON-RPC error.
283    #[error("Internal error: {message}")]
284    #[diagnostic(code(mcp::protocol::internal_error), severity(error))]
285    Internal {
286        /// Human-readable error message.
287        message: String,
288        /// The underlying error, if available.
289        #[source]
290        source: Option<BoxError>,
291    },
292
293    // ========================================================================
294    // Transport Errors
295    // ========================================================================
296    /// Transport-level error (details boxed to reduce enum size).
297    #[error("Transport error ({}): {}", .0.kind, .0.message)]
298    #[diagnostic(code(mcp::transport::error))]
299    Transport(#[source] Box<TransportDetails>),
300
301    // ========================================================================
302    // Tool Execution Errors
303    // ========================================================================
304    /// A tool execution failed (details boxed to reduce enum size).
305    #[error("Tool '{}' failed: {}", .0.tool, .0.message)]
306    #[diagnostic(code(mcp::tool::execution_error))]
307    ToolExecution(#[source] Box<ToolExecutionDetails>),
308
309    // ========================================================================
310    // Resource Errors
311    // ========================================================================
312    /// A requested resource was not found.
313    #[error("Resource not found: {uri}")]
314    #[diagnostic(
315        code(mcp::resource::not_found),
316        help("Verify the URI is correct and the resource exists")
317    )]
318    ResourceNotFound {
319        /// The URI of the resource that was not found.
320        uri: String,
321    },
322
323    /// Access to a resource was denied.
324    #[error("Resource access denied: {uri}")]
325    #[diagnostic(code(mcp::resource::access_denied))]
326    ResourceAccessDenied {
327        /// The URI of the resource.
328        uri: String,
329        /// The reason for denial, if available.
330        reason: Option<String>,
331    },
332
333    // ========================================================================
334    // Connection/Session Errors
335    // ========================================================================
336    /// Connection establishment failed.
337    #[error("Connection failed: {message}")]
338    #[diagnostic(code(mcp::connection::failed))]
339    ConnectionFailed {
340        /// Human-readable error message.
341        message: String,
342        /// The underlying error, if available.
343        #[source]
344        source: Option<BoxError>,
345    },
346
347    /// Session has expired.
348    #[error("Session expired: {session_id}")]
349    #[diagnostic(
350        code(mcp::session::expired),
351        help("Re-initialize the connection to continue")
352    )]
353    SessionExpired {
354        /// The expired session ID.
355        session_id: String,
356    },
357
358    /// Protocol handshake failed (details boxed to reduce enum size).
359    #[error("Handshake failed: {}", .0.message)]
360    #[diagnostic(code(mcp::handshake::failed))]
361    HandshakeFailed(#[source] Box<HandshakeDetails>),
362
363    // ========================================================================
364    // Capability Errors
365    // ========================================================================
366    /// A requested capability is not supported.
367    #[error("Capability not supported: {capability}")]
368    #[diagnostic(code(mcp::capability::not_supported))]
369    CapabilityNotSupported {
370        /// The capability that was requested.
371        capability: String,
372        /// List of available capabilities (boxed to reduce size).
373        available: Box<[String]>,
374    },
375
376    // ========================================================================
377    // User/Client Errors
378    // ========================================================================
379    /// User rejected an operation.
380    #[error("User rejected: {message}")]
381    #[diagnostic(code(mcp::user::rejected))]
382    UserRejected {
383        /// Human-readable message about what was rejected.
384        message: String,
385        /// The operation that was rejected.
386        operation: String,
387    },
388
389    // ========================================================================
390    // Timeout Errors
391    // ========================================================================
392    /// An operation timed out.
393    #[error("Timeout after {duration:?}: {operation}")]
394    #[diagnostic(
395        code(mcp::timeout),
396        help("Consider increasing the timeout or checking connectivity")
397    )]
398    Timeout {
399        /// The operation that timed out.
400        operation: String,
401        /// How long we waited before timing out.
402        duration: std::time::Duration,
403    },
404
405    // ========================================================================
406    // Cancellation
407    // ========================================================================
408    /// An operation was cancelled.
409    #[error("Operation cancelled: {operation}")]
410    #[diagnostic(code(mcp::cancelled))]
411    Cancelled {
412        /// The operation that was cancelled.
413        operation: String,
414        /// Reason for cancellation, if provided.
415        reason: Option<String>,
416    },
417
418    // ========================================================================
419    // Context-Wrapped Errors
420    // ========================================================================
421    /// An error with additional context.
422    #[error("{context}: {source}")]
423    #[diagnostic(code(mcp::context))]
424    WithContext {
425        /// The context message.
426        context: String,
427        /// The underlying error.
428        #[source]
429        source: Box<McpError>,
430    },
431
432    // ========================================================================
433    // Generic Internal Error (simple variant)
434    // ========================================================================
435    /// A simple internal error with just a message.
436    #[error("Internal error: {message}")]
437    #[diagnostic(code(mcp::internal))]
438    InternalMessage {
439        /// Human-readable error message.
440        message: String,
441    },
442}
443
444/// Classification of transport errors.
445#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
446#[serde(rename_all = "snake_case")]
447pub enum TransportErrorKind {
448    /// Connection could not be established.
449    ConnectionFailed,
450    /// Connection was closed unexpectedly.
451    ConnectionClosed,
452    /// Read operation failed.
453    ReadFailed,
454    /// Write operation failed.
455    WriteFailed,
456    /// TLS/SSL error occurred.
457    TlsError,
458    /// DNS resolution failed.
459    DnsResolutionFailed,
460    /// Operation timed out.
461    Timeout,
462    /// Message format was invalid.
463    InvalidMessage,
464    /// Protocol violation detected.
465    ProtocolViolation,
466    /// Resources exhausted (e.g., too many connections).
467    ResourceExhausted,
468    /// Rate limit exceeded.
469    RateLimited,
470}
471
472impl fmt::Display for TransportErrorKind {
473    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
474        match self {
475            Self::ConnectionFailed => write!(f, "connection failed"),
476            Self::ConnectionClosed => write!(f, "connection closed"),
477            Self::ReadFailed => write!(f, "read failed"),
478            Self::WriteFailed => write!(f, "write failed"),
479            Self::TlsError => write!(f, "TLS error"),
480            Self::DnsResolutionFailed => write!(f, "DNS resolution failed"),
481            Self::Timeout => write!(f, "timeout"),
482            Self::InvalidMessage => write!(f, "invalid message"),
483            Self::ProtocolViolation => write!(f, "protocol violation"),
484            Self::ResourceExhausted => write!(f, "resource exhausted"),
485            Self::RateLimited => write!(f, "rate limited"),
486        }
487    }
488}
489
490/// Additional context for transport errors.
491#[derive(Debug, Clone, Default, Serialize, Deserialize)]
492pub struct TransportContext {
493    /// Transport type (stdio, http, websocket, unix).
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub transport_type: Option<String>,
496    /// Remote endpoint address.
497    #[serde(skip_serializing_if = "Option::is_none")]
498    pub remote_addr: Option<String>,
499    /// Local endpoint address.
500    #[serde(skip_serializing_if = "Option::is_none")]
501    pub local_addr: Option<String>,
502    /// Bytes sent before error occurred.
503    #[serde(skip_serializing_if = "Option::is_none")]
504    pub bytes_sent: Option<u64>,
505    /// Bytes received before error occurred.
506    #[serde(skip_serializing_if = "Option::is_none")]
507    pub bytes_received: Option<u64>,
508    /// Connection duration before error.
509    #[serde(skip_serializing_if = "Option::is_none")]
510    pub connection_duration_ms: Option<u64>,
511}
512
513impl TransportContext {
514    /// Create a new transport context for a specific transport type.
515    #[must_use]
516    pub fn new(transport_type: impl Into<String>) -> Self {
517        Self {
518            transport_type: Some(transport_type.into()),
519            ..Default::default()
520        }
521    }
522
523    /// Set the remote address.
524    #[must_use]
525    pub fn with_remote_addr(mut self, addr: impl Into<String>) -> Self {
526        self.remote_addr = Some(addr.into());
527        self
528    }
529
530    /// Set the local address.
531    #[must_use]
532    pub fn with_local_addr(mut self, addr: impl Into<String>) -> Self {
533        self.local_addr = Some(addr.into());
534        self
535    }
536}
537
538// ============================================================================
539// Error Construction Helpers
540// ============================================================================
541
542impl McpError {
543    /// Create a parse error.
544    pub fn parse(message: impl Into<String>) -> Self {
545        Self::Parse {
546            message: message.into(),
547            source: None,
548        }
549    }
550
551    /// Create a parse error with a source.
552    pub fn parse_with_source<E: std::error::Error + Send + Sync + 'static>(
553        message: impl Into<String>,
554        source: E,
555    ) -> Self {
556        Self::Parse {
557            message: message.into(),
558            source: Some(Box::new(source)),
559        }
560    }
561
562    /// Create an invalid request error.
563    pub fn invalid_request(message: impl Into<String>) -> Self {
564        Self::InvalidRequest {
565            message: message.into(),
566            source: None,
567        }
568    }
569
570    /// Create a method not found error.
571    pub fn method_not_found(method: impl Into<String>) -> Self {
572        Self::MethodNotFound {
573            method: method.into(),
574            available: Box::new([]),
575        }
576    }
577
578    /// Create a method not found error with suggestions.
579    pub fn method_not_found_with_suggestions(
580        method: impl Into<String>,
581        available: Vec<String>,
582    ) -> Self {
583        Self::MethodNotFound {
584            method: method.into(),
585            available: available.into_boxed_slice(),
586        }
587    }
588
589    /// Create an invalid params error.
590    pub fn invalid_params(method: impl Into<String>, message: impl Into<String>) -> Self {
591        Self::InvalidParams(Box::new(InvalidParamsDetails {
592            method: method.into(),
593            message: message.into(),
594            param_path: None,
595            expected: None,
596            actual: None,
597            source: None,
598        }))
599    }
600
601    /// Create an invalid params error with full details.
602    pub fn invalid_params_detailed(
603        method: impl Into<String>,
604        message: impl Into<String>,
605        param_path: Option<String>,
606        expected: Option<String>,
607        actual: Option<String>,
608    ) -> Self {
609        Self::InvalidParams(Box::new(InvalidParamsDetails {
610            method: method.into(),
611            message: message.into(),
612            param_path,
613            expected,
614            actual,
615            source: None,
616        }))
617    }
618
619    /// Create an internal error.
620    pub fn internal(message: impl Into<String>) -> Self {
621        Self::Internal {
622            message: message.into(),
623            source: None,
624        }
625    }
626
627    /// Create an internal error with a source.
628    pub fn internal_with_source<E: std::error::Error + Send + Sync + 'static>(
629        message: impl Into<String>,
630        source: E,
631    ) -> Self {
632        Self::Internal {
633            message: message.into(),
634            source: Some(Box::new(source)),
635        }
636    }
637
638    /// Create a transport error.
639    pub fn transport(kind: TransportErrorKind, message: impl Into<String>) -> Self {
640        Self::Transport(Box::new(TransportDetails {
641            kind,
642            message: message.into(),
643            context: TransportContext::default(),
644            source: None,
645        }))
646    }
647
648    /// Create a transport error with context.
649    pub fn transport_with_context(
650        kind: TransportErrorKind,
651        message: impl Into<String>,
652        context: TransportContext,
653    ) -> Self {
654        Self::Transport(Box::new(TransportDetails {
655            kind,
656            message: message.into(),
657            context,
658            source: None,
659        }))
660    }
661
662    /// Create a tool execution error.
663    pub fn tool_error(tool: impl Into<String>, message: impl Into<String>) -> Self {
664        Self::ToolExecution(Box::new(ToolExecutionDetails {
665            tool: tool.into(),
666            message: message.into(),
667            is_recoverable: true,
668            data: None,
669            source: None,
670        }))
671    }
672
673    /// Create a tool execution error with full details.
674    pub fn tool_error_detailed(
675        tool: impl Into<String>,
676        message: impl Into<String>,
677        is_recoverable: bool,
678        data: Option<serde_json::Value>,
679    ) -> Self {
680        Self::ToolExecution(Box::new(ToolExecutionDetails {
681            tool: tool.into(),
682            message: message.into(),
683            is_recoverable,
684            data,
685            source: None,
686        }))
687    }
688
689    /// Create a resource not found error.
690    pub fn resource_not_found(uri: impl Into<String>) -> Self {
691        Self::ResourceNotFound { uri: uri.into() }
692    }
693
694    /// Create a handshake failed error.
695    pub fn handshake_failed(message: impl Into<String>) -> Self {
696        Self::HandshakeFailed(Box::new(HandshakeDetails {
697            message: message.into(),
698            client_version: None,
699            server_version: None,
700            source: None,
701        }))
702    }
703
704    /// Create a handshake failed error with version info.
705    pub fn handshake_failed_with_versions(
706        message: impl Into<String>,
707        client_version: Option<String>,
708        server_version: Option<String>,
709    ) -> Self {
710        Self::HandshakeFailed(Box::new(HandshakeDetails {
711            message: message.into(),
712            client_version,
713            server_version,
714            source: None,
715        }))
716    }
717
718    /// Create a capability not supported error.
719    pub fn capability_not_supported(capability: impl Into<String>) -> Self {
720        Self::CapabilityNotSupported {
721            capability: capability.into(),
722            available: Box::new([]),
723        }
724    }
725
726    /// Create a capability not supported error with available list.
727    pub fn capability_not_supported_with_available(
728        capability: impl Into<String>,
729        available: Vec<String>,
730    ) -> Self {
731        Self::CapabilityNotSupported {
732            capability: capability.into(),
733            available: available.into_boxed_slice(),
734        }
735    }
736
737    /// Create a timeout error.
738    pub fn timeout(operation: impl Into<String>, duration: std::time::Duration) -> Self {
739        Self::Timeout {
740            operation: operation.into(),
741            duration,
742        }
743    }
744
745    /// Create a cancelled error.
746    pub fn cancelled(operation: impl Into<String>) -> Self {
747        Self::Cancelled {
748            operation: operation.into(),
749            reason: None,
750        }
751    }
752
753    /// Create a cancelled error with reason.
754    pub fn cancelled_with_reason(operation: impl Into<String>, reason: impl Into<String>) -> Self {
755        Self::Cancelled {
756            operation: operation.into(),
757            reason: Some(reason.into()),
758        }
759    }
760}
761
762// ============================================================================
763// JSON-RPC Error Codes
764// ============================================================================
765
766/// Standard JSON-RPC error codes.
767pub mod codes {
768    /// Invalid JSON was received.
769    pub const PARSE_ERROR: i32 = -32700;
770    /// The JSON sent is not a valid Request object.
771    pub const INVALID_REQUEST: i32 = -32600;
772    /// The method does not exist.
773    pub const METHOD_NOT_FOUND: i32 = -32601;
774    /// Invalid method parameters.
775    pub const INVALID_PARAMS: i32 = -32602;
776    /// Internal JSON-RPC error.
777    pub const INTERNAL_ERROR: i32 = -32603;
778
779    /// Server error range start.
780    pub const SERVER_ERROR_START: i32 = -32000;
781    /// Server error range end.
782    pub const SERVER_ERROR_END: i32 = -32099;
783
784    // MCP-specific codes
785    /// User rejected the operation.
786    pub const USER_REJECTED: i32 = -1;
787    /// Resource was not found.
788    pub const RESOURCE_NOT_FOUND: i32 = -32002;
789}
790
791impl McpError {
792    /// Get the JSON-RPC error code for this error.
793    #[must_use]
794    pub fn code(&self) -> i32 {
795        match self {
796            Self::Parse { .. } => codes::PARSE_ERROR,
797            Self::InvalidRequest { .. } => codes::INVALID_REQUEST,
798            Self::MethodNotFound { .. } => codes::METHOD_NOT_FOUND,
799            Self::InvalidParams(_) => codes::INVALID_PARAMS,
800            Self::Internal { .. } => codes::INTERNAL_ERROR,
801            Self::Transport(_) => codes::SERVER_ERROR_START,
802            Self::ToolExecution(_) => codes::SERVER_ERROR_START - 1,
803            Self::ResourceNotFound { .. } => codes::RESOURCE_NOT_FOUND,
804            Self::ResourceAccessDenied { .. } => codes::SERVER_ERROR_START - 2,
805            Self::ConnectionFailed { .. } => codes::SERVER_ERROR_START - 3,
806            Self::SessionExpired { .. } => codes::SERVER_ERROR_START - 4,
807            Self::HandshakeFailed(_) => codes::SERVER_ERROR_START - 5,
808            Self::CapabilityNotSupported { .. } => codes::SERVER_ERROR_START - 6,
809            Self::UserRejected { .. } => codes::USER_REJECTED,
810            Self::Timeout { .. } => codes::SERVER_ERROR_START - 7,
811            Self::Cancelled { .. } => codes::SERVER_ERROR_START - 8,
812            Self::WithContext { source, .. } => source.code(),
813            Self::InternalMessage { .. } => codes::INTERNAL_ERROR,
814        }
815    }
816
817    /// Check if this is a recoverable error (LLM can retry).
818    #[must_use]
819    pub fn is_recoverable(&self) -> bool {
820        match self {
821            Self::ToolExecution(details) => details.is_recoverable,
822            Self::InvalidParams(_) => true,
823            Self::ResourceNotFound { .. } => true,
824            Self::Timeout { .. } => true,
825            Self::WithContext { source, .. } => source.is_recoverable(),
826            Self::InternalMessage { .. } => false,
827            _ => false,
828        }
829    }
830}
831
832// ============================================================================
833// JSON-RPC Error Response Type
834// ============================================================================
835
836/// A JSON-RPC error response object.
837#[derive(Debug, Clone, Serialize, Deserialize)]
838pub struct JsonRpcError {
839    /// Error code.
840    pub code: i32,
841    /// Error message.
842    pub message: String,
843    /// Additional error data.
844    #[serde(skip_serializing_if = "Option::is_none")]
845    pub data: Option<serde_json::Value>,
846}
847
848impl JsonRpcError {
849    /// Create an "invalid params" error (-32602).
850    pub fn invalid_params(message: impl Into<String>) -> Self {
851        Self {
852            code: -32602,
853            message: message.into(),
854            data: None,
855        }
856    }
857
858    /// Create an "internal error" (-32603).
859    pub fn internal_error(message: impl Into<String>) -> Self {
860        Self {
861            code: -32603,
862            message: message.into(),
863            data: None,
864        }
865    }
866
867    /// Create a "method not found" error (-32601).
868    pub fn method_not_found(message: impl Into<String>) -> Self {
869        Self {
870            code: -32601,
871            message: message.into(),
872            data: None,
873        }
874    }
875
876    /// Create a "parse error" (-32700).
877    pub fn parse_error(message: impl Into<String>) -> Self {
878        Self {
879            code: -32700,
880            message: message.into(),
881            data: None,
882        }
883    }
884
885    /// Create an "invalid request" error (-32600).
886    pub fn invalid_request(message: impl Into<String>) -> Self {
887        Self {
888            code: -32600,
889            message: message.into(),
890            data: None,
891        }
892    }
893}
894
895impl From<&McpError> for JsonRpcError {
896    fn from(err: &McpError) -> Self {
897        let code = err.code();
898        let message = err.to_string();
899        let data = match err {
900            McpError::MethodNotFound { method, available, .. } => Some(serde_json::json!({
901                "method": method,
902                "available": available,
903            })),
904            McpError::InvalidParams(details) => Some(serde_json::json!({
905                "method": details.method,
906                "param_path": details.param_path,
907                "expected": details.expected,
908                "actual": details.actual,
909            })),
910            McpError::Transport(details) => Some(serde_json::json!({
911                "kind": format!("{:?}", details.kind),
912                "context": details.context,
913            })),
914            McpError::ToolExecution(details) => {
915                details.data.clone().or_else(|| Some(serde_json::json!({ "tool": details.tool })))
916            }
917            McpError::HandshakeFailed(details) => Some(serde_json::json!({
918                "client_version": details.client_version,
919                "server_version": details.server_version,
920            })),
921            McpError::WithContext { source, .. } => {
922                let inner: JsonRpcError = source.as_ref().into();
923                inner.data
924            }
925            _ => None,
926        };
927
928        Self {
929            code,
930            message,
931            data,
932        }
933    }
934}
935
936impl From<McpError> for JsonRpcError {
937    fn from(err: McpError) -> Self {
938        Self::from(&err)
939    }
940}
941
942// ============================================================================
943// Context Extension Trait
944// ============================================================================
945
946/// Extension trait for adding context to `Result` types.
947///
948/// This provides `anyhow`-style context methods while preserving the
949/// typed error system.
950///
951/// # Example
952///
953/// ```rust
954/// use mcpkit_core::error::{McpError, McpResultExt};
955///
956/// fn process() -> Result<(), McpError> {
957///     let result: Result<(), McpError> = Err(McpError::internal("oops"));
958///     result.context("Failed to process data")?;
959///     Ok(())
960/// }
961/// ```
962pub trait McpResultExt<T> {
963    /// Add context to an error.
964    fn context<C: Into<String>>(self, context: C) -> Result<T, McpError>;
965
966    /// Add context lazily (only evaluated on error).
967    fn with_context<C, F>(self, f: F) -> Result<T, McpError>
968    where
969        C: Into<String>,
970        F: FnOnce() -> C;
971}
972
973impl<T> McpResultExt<T> for Result<T, McpError> {
974    fn context<C: Into<String>>(self, context: C) -> Result<T, McpError> {
975        self.map_err(|e| McpError::WithContext {
976            context: context.into(),
977            source: Box::new(e),
978        })
979    }
980
981    fn with_context<C, F>(self, f: F) -> Result<T, McpError>
982    where
983        C: Into<String>,
984        F: FnOnce() -> C,
985    {
986        self.map_err(|e| McpError::WithContext {
987            context: f().into(),
988            source: Box::new(e),
989        })
990    }
991}
992
993// ============================================================================
994// Standard Error Conversions
995// ============================================================================
996
997impl From<serde_json::Error> for McpError {
998    fn from(err: serde_json::Error) -> Self {
999        Self::parse_with_source("JSON serialization/deserialization error", err)
1000    }
1001}
1002
1003impl From<std::io::Error> for McpError {
1004    fn from(err: std::io::Error) -> Self {
1005        let kind = match err.kind() {
1006            std::io::ErrorKind::NotFound => TransportErrorKind::ConnectionFailed,
1007            std::io::ErrorKind::ConnectionRefused => TransportErrorKind::ConnectionFailed,
1008            std::io::ErrorKind::ConnectionReset => TransportErrorKind::ConnectionClosed,
1009            std::io::ErrorKind::ConnectionAborted => TransportErrorKind::ConnectionClosed,
1010            std::io::ErrorKind::TimedOut => TransportErrorKind::Timeout,
1011            std::io::ErrorKind::WriteZero => TransportErrorKind::WriteFailed,
1012            std::io::ErrorKind::UnexpectedEof => TransportErrorKind::ReadFailed,
1013            _ => TransportErrorKind::ReadFailed,
1014        };
1015        let message = err.to_string();
1016        Self::Transport(Box::new(TransportDetails {
1017            kind,
1018            message,
1019            context: TransportContext::default(),
1020            source: Some(Box::new(err)),
1021        }))
1022    }
1023}
1024
1025#[cfg(test)]
1026mod tests {
1027    use super::*;
1028
1029    #[test]
1030    fn test_error_size_is_small() {
1031        // Verify that McpError is reasonably small (64 bytes or less on 64-bit).
1032        // This ensures Result<T, McpError> doesn't bloat return values.
1033        let size = std::mem::size_of::<McpError>();
1034        assert!(
1035            size <= 64,
1036            "McpError is {} bytes, should be <= 64 bytes. Consider boxing more variants.",
1037            size
1038        );
1039
1040        // Also verify Result<(), McpError> is small
1041        let result_size = std::mem::size_of::<Result<(), McpError>>();
1042        assert!(
1043            result_size <= 72,
1044            "Result<(), McpError> is {} bytes, should be <= 72 bytes.",
1045            result_size
1046        );
1047    }
1048
1049    #[test]
1050    fn test_error_codes() {
1051        assert_eq!(McpError::parse("test").code(), codes::PARSE_ERROR);
1052        assert_eq!(
1053            McpError::invalid_request("test").code(),
1054            codes::INVALID_REQUEST
1055        );
1056        assert_eq!(
1057            McpError::method_not_found("test").code(),
1058            codes::METHOD_NOT_FOUND
1059        );
1060        assert_eq!(
1061            McpError::invalid_params("m", "test").code(),
1062            codes::INVALID_PARAMS
1063        );
1064        assert_eq!(McpError::internal("test").code(), codes::INTERNAL_ERROR);
1065        assert_eq!(
1066            McpError::transport(TransportErrorKind::ConnectionFailed, "test").code(),
1067            codes::SERVER_ERROR_START
1068        );
1069        assert_eq!(
1070            McpError::tool_error("tool", "test").code(),
1071            codes::SERVER_ERROR_START - 1
1072        );
1073        assert_eq!(
1074            McpError::handshake_failed("test").code(),
1075            codes::SERVER_ERROR_START - 5
1076        );
1077    }
1078
1079    #[test]
1080    fn test_context_chaining() {
1081        fn inner() -> Result<(), McpError> {
1082            Err(McpError::resource_not_found("test://resource"))
1083        }
1084
1085        fn outer() -> Result<(), McpError> {
1086            inner().context("Failed in outer")?;
1087            Ok(())
1088        }
1089
1090        let err = outer().unwrap_err();
1091        assert!(err.to_string().contains("Failed in outer"));
1092
1093        // Verify code propagates through context
1094        assert_eq!(err.code(), codes::RESOURCE_NOT_FOUND);
1095    }
1096
1097    #[test]
1098    fn test_json_rpc_error_conversion() {
1099        let err = McpError::method_not_found_with_suggestions(
1100            "unknown_method",
1101            vec!["tools/list".to_string(), "resources/list".to_string()],
1102        );
1103
1104        let json_err: JsonRpcError = (&err).into();
1105        assert_eq!(json_err.code, codes::METHOD_NOT_FOUND);
1106        assert!(json_err.message.contains("unknown_method"));
1107        assert!(json_err.data.is_some());
1108    }
1109
1110    #[test]
1111    fn test_json_rpc_error_conversion_boxed_variants() {
1112        // Test InvalidParams (boxed)
1113        let err = McpError::invalid_params_detailed(
1114            "test_method",
1115            "invalid value",
1116            Some("args.count".to_string()),
1117            Some("number".to_string()),
1118            Some("string".to_string()),
1119        );
1120        let json_err: JsonRpcError = (&err).into();
1121        assert_eq!(json_err.code, codes::INVALID_PARAMS);
1122        let data = json_err.data.unwrap();
1123        assert_eq!(data["method"], "test_method");
1124        assert_eq!(data["param_path"], "args.count");
1125
1126        // Test Transport (boxed)
1127        let err = McpError::transport_with_context(
1128            TransportErrorKind::ConnectionFailed,
1129            "connection refused",
1130            TransportContext::new("websocket").with_remote_addr("ws://localhost:8080"),
1131        );
1132        let json_err: JsonRpcError = (&err).into();
1133        assert_eq!(json_err.code, codes::SERVER_ERROR_START);
1134        assert!(json_err.data.is_some());
1135
1136        // Test ToolExecution (boxed)
1137        let err = McpError::tool_error_detailed(
1138            "calculator",
1139            "division by zero",
1140            true,
1141            Some(serde_json::json!({"operation": "divide"})),
1142        );
1143        let json_err: JsonRpcError = (&err).into();
1144        assert!(json_err.data.is_some());
1145        let data = json_err.data.unwrap();
1146        assert_eq!(data["operation"], "divide");
1147
1148        // Test HandshakeFailed (boxed)
1149        let err = McpError::handshake_failed_with_versions(
1150            "version mismatch",
1151            Some("2024-11-05".to_string()),
1152            Some("2025-11-25".to_string()),
1153        );
1154        let json_err: JsonRpcError = (&err).into();
1155        assert!(json_err.data.is_some());
1156        let data = json_err.data.unwrap();
1157        assert_eq!(data["client_version"], "2024-11-05");
1158        assert_eq!(data["server_version"], "2025-11-25");
1159    }
1160
1161    #[test]
1162    fn test_recoverable_errors() {
1163        assert!(McpError::invalid_params("m", "test").is_recoverable());
1164        assert!(McpError::resource_not_found("uri").is_recoverable());
1165        assert!(!McpError::internal("test").is_recoverable());
1166
1167        // Test boxed tool execution with recoverable flag
1168        let recoverable_tool = McpError::tool_error_detailed("tool", "error", true, None);
1169        assert!(recoverable_tool.is_recoverable());
1170
1171        let non_recoverable_tool = McpError::tool_error_detailed("tool", "error", false, None);
1172        assert!(!non_recoverable_tool.is_recoverable());
1173    }
1174
1175    #[test]
1176    fn test_boxed_error_display() {
1177        // Ensure Display works correctly for boxed variants
1178        let err = McpError::invalid_params("method", "bad params");
1179        assert!(err.to_string().contains("method"));
1180        assert!(err.to_string().contains("bad params"));
1181
1182        let err = McpError::transport(TransportErrorKind::Timeout, "connection timed out");
1183        assert!(err.to_string().contains("timeout"));
1184        assert!(err.to_string().contains("connection timed out"));
1185
1186        let err = McpError::tool_error("my_tool", "tool failed");
1187        assert!(err.to_string().contains("my_tool"));
1188        assert!(err.to_string().contains("tool failed"));
1189
1190        let err = McpError::handshake_failed("protocol mismatch");
1191        assert!(err.to_string().contains("protocol mismatch"));
1192    }
1193
1194    #[test]
1195    fn test_io_error_conversion() {
1196        let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
1197        let mcp_err: McpError = io_err.into();
1198
1199        // Should be converted to Transport error with appropriate kind
1200        if let McpError::Transport(details) = mcp_err {
1201            assert_eq!(details.kind, TransportErrorKind::ConnectionFailed);
1202            assert!(details.message.contains("refused"));
1203        } else {
1204            panic!("Expected Transport error variant");
1205        }
1206    }
1207}