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