Skip to main content

turbomcp_core/
error.rs

1//! Unified MCP error handling - no_std compatible.
2//!
3//! This module provides a single error type [`McpError`] for all MCP operations,
4//! replacing the previous dual error types (`ServerError` + `protocol::Error`).
5//!
6//! ## Design Goals
7//!
8//! 1. **Single Error Type**: One `McpError` across all crates
9//! 2. **no_std Compatible**: Core error works without std
10//! 3. **Rich Context**: Optional detailed context when `rich-errors` feature enabled
11//! 4. **MCP Compliant**: Maps to JSON-RPC error codes per MCP spec
12//!
13//! ## Features
14//!
15//! - **Default (no_std)**: Lightweight error with kind, message, and basic context
16//! - **`rich-errors`**: Adds UUID tracking and timestamp for observability
17//!
18//! ## Example
19//!
20//! ```rust
21//! use turbomcp_core::error::{McpError, ErrorKind, McpResult};
22//!
23//! fn my_tool() -> McpResult<String> {
24//!     Err(McpError::new(ErrorKind::ToolNotFound, "calculator"))
25//! }
26//! ```
27
28use alloc::boxed::Box;
29use alloc::string::String;
30use core::fmt;
31use serde::{Deserialize, Serialize};
32
33/// Result type alias for MCP operations
34pub type McpResult<T> = core::result::Result<T, McpError>;
35
36/// Unified MCP error type
37///
38/// This is the single error type used across all TurboMCP crates in v3.
39/// It is `no_std` compatible and maps to JSON-RPC error codes per MCP spec.
40///
41/// With `rich-errors` feature enabled, includes UUID tracking and timestamps.
42///
43/// The `context` field is boxed to keep error size small for efficient Result<T, McpError> usage.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct McpError {
46    /// Unique error ID for tracing (only with `rich-errors` feature)
47    #[cfg(feature = "rich-errors")]
48    pub id: uuid::Uuid,
49    /// Error classification
50    pub kind: ErrorKind,
51    /// Human-readable error message
52    pub message: String,
53    /// Source location (file:line for debugging)
54    /// Note: Never serialized to clients to prevent information leakage
55    #[serde(skip_serializing)]
56    pub source_location: Option<String>,
57    /// Additional context (boxed to keep McpError small)
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub context: Option<alloc::boxed::Box<ErrorContext>>,
60    /// Timestamp when error occurred (only with `rich-errors` feature)
61    #[cfg(feature = "rich-errors")]
62    pub timestamp: chrono::DateTime<chrono::Utc>,
63}
64
65/// Additional error context
66#[derive(Debug, Clone, Default, Serialize, Deserialize)]
67pub struct ErrorContext {
68    /// Operation being performed
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub operation: Option<String>,
71    /// Component where error occurred
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub component: Option<String>,
74    /// Request ID for tracing
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub request_id: Option<String>,
77    /// Structured payload for the JSON-RPC error object's `data` member.
78    ///
79    /// Unlike [`source_location`](McpError::source_location), this is meant to
80    /// reach the client: it is set explicitly by the server author via
81    /// [`McpError::with_data`], so it carries only what they chose to expose.
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub data: Option<serde_json::Value>,
84}
85
86/// Error classification for programmatic handling.
87///
88/// This enum is `#[non_exhaustive]` — new variants may be added in future
89/// minor releases without a breaking change.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
91#[serde(rename_all = "snake_case")]
92#[non_exhaustive]
93pub enum ErrorKind {
94    // === MCP-Specific Errors ===
95    /// Tool not found (MCP -32001)
96    ToolNotFound,
97    /// Tool execution failed (MCP -32002)
98    ToolExecutionFailed,
99    /// Prompt not found (MCP -32003)
100    PromptNotFound,
101    /// Resource not found (MCP -32004)
102    ResourceNotFound,
103    /// Resource access denied (MCP -32005)
104    ResourceAccessDenied,
105    /// Capability not supported (MCP -32006)
106    CapabilityNotSupported,
107    /// Protocol version mismatch (MCP -32007)
108    ProtocolVersionMismatch,
109    /// URL elicitation required (MCP -32042)
110    UrlElicitationRequired,
111    /// User rejected the request (MCP -1)
112    UserRejected,
113
114    // === JSON-RPC Standard Errors ===
115    /// Parse error (-32700)
116    ParseError,
117    /// Invalid request (-32600)
118    InvalidRequest,
119    /// Method not found (-32601)
120    MethodNotFound,
121    /// Invalid params (-32602)
122    InvalidParams,
123    /// Internal error (-32603)
124    Internal,
125
126    // === General Application Errors ===
127    /// Authentication failed
128    Authentication,
129    /// Permission denied
130    PermissionDenied,
131    /// Transport/network error
132    Transport,
133    /// Operation timed out
134    Timeout,
135    /// Service unavailable
136    Unavailable,
137    /// Rate limited (-32009)
138    RateLimited,
139    /// Server overloaded (-32010)
140    ServerOverloaded,
141    /// Configuration error
142    Configuration,
143    /// External service failed
144    ExternalService,
145    /// Operation cancelled
146    Cancelled,
147    /// Security violation
148    Security,
149    /// Serialization error
150    Serialization,
151}
152
153impl McpError {
154    /// Create a new error with kind and message
155    #[must_use]
156    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
157        Self {
158            #[cfg(feature = "rich-errors")]
159            id: uuid::Uuid::new_v4(),
160            kind,
161            message: message.into(),
162            source_location: None,
163            context: None,
164            #[cfg(feature = "rich-errors")]
165            timestamp: chrono::Utc::now(),
166        }
167    }
168
169    /// Get the error ID (only available with `rich-errors` feature)
170    #[cfg(feature = "rich-errors")]
171    #[must_use]
172    pub const fn id(&self) -> uuid::Uuid {
173        self.id
174    }
175
176    /// Get the error timestamp (only available with `rich-errors` feature)
177    #[cfg(feature = "rich-errors")]
178    #[must_use]
179    pub const fn timestamp(&self) -> chrono::DateTime<chrono::Utc> {
180        self.timestamp
181    }
182
183    /// Create a validation/invalid params error
184    #[must_use]
185    pub fn invalid_params(message: impl Into<String>) -> Self {
186        Self::new(ErrorKind::InvalidParams, message)
187    }
188
189    /// Create an internal error
190    #[must_use]
191    pub fn internal(message: impl Into<String>) -> Self {
192        Self::new(ErrorKind::Internal, message)
193    }
194
195    /// Create a safe internal error with sanitized message.
196    ///
197    /// Use this for errors that may contain sensitive information (file paths,
198    /// IP addresses, connection strings, etc.). The message is automatically
199    /// sanitized to prevent information leakage per OWASP guidelines.
200    ///
201    /// # Example
202    ///
203    /// ```rust
204    /// use turbomcp_core::error::McpError;
205    ///
206    /// let err = McpError::safe_internal("Failed: postgres://admin:secret@192.168.1.1/db");
207    /// assert!(!err.message.contains("secret"));
208    /// assert!(!err.message.contains("192.168.1.1"));
209    /// ```
210    #[must_use]
211    pub fn safe_internal(message: impl Into<String>) -> Self {
212        let sanitized = crate::security::sanitize_error_message(&message.into());
213        Self::new(ErrorKind::Internal, sanitized)
214    }
215
216    /// Create a safe tool execution error with sanitized message.
217    ///
218    /// Like [`safe_internal`](Self::safe_internal), but specifically for tool execution failures.
219    #[must_use]
220    pub fn safe_tool_execution_failed(
221        tool_name: impl Into<String>,
222        reason: impl Into<String>,
223    ) -> Self {
224        let name = tool_name.into();
225        let sanitized_reason = crate::security::sanitize_error_message(&reason.into());
226        Self::new(
227            ErrorKind::ToolExecutionFailed,
228            alloc::format!("Tool '{}' failed: {}", name, sanitized_reason),
229        )
230        .with_operation("tool_execution")
231    }
232
233    /// Sanitize this error's message in-place.
234    ///
235    /// Call this before returning errors to clients in production to ensure
236    /// no sensitive information is leaked.
237    #[must_use]
238    pub fn sanitized(mut self) -> Self {
239        self.message = crate::security::sanitize_error_message(&self.message);
240        self
241    }
242
243    /// Create a parse error
244    #[must_use]
245    pub fn parse_error(message: impl Into<String>) -> Self {
246        Self::new(ErrorKind::ParseError, message)
247    }
248
249    /// Create an invalid request error
250    #[must_use]
251    pub fn invalid_request(message: impl Into<String>) -> Self {
252        Self::new(ErrorKind::InvalidRequest, message)
253    }
254
255    /// Create a method not found error
256    #[must_use]
257    pub fn method_not_found(method: impl Into<String>) -> Self {
258        let method = method.into();
259        Self::new(
260            ErrorKind::MethodNotFound,
261            alloc::format!("Method not found: {}", method),
262        )
263    }
264
265    /// Create a tool not found error
266    #[must_use]
267    pub fn tool_not_found(tool_name: impl Into<String>) -> Self {
268        let name = tool_name.into();
269        Self::new(
270            ErrorKind::ToolNotFound,
271            alloc::format!("Tool not found: {}", name),
272        )
273        .with_operation("tool_lookup")
274        .with_component("tool_registry")
275    }
276
277    /// Create a tool execution failed error
278    #[must_use]
279    pub fn tool_execution_failed(tool_name: impl Into<String>, reason: impl Into<String>) -> Self {
280        let name = tool_name.into();
281        let reason = reason.into();
282        Self::new(
283            ErrorKind::ToolExecutionFailed,
284            alloc::format!("Tool '{}' failed: {}", name, reason),
285        )
286        .with_operation("tool_execution")
287    }
288
289    /// Create a prompt not found error
290    #[must_use]
291    pub fn prompt_not_found(prompt_name: impl Into<String>) -> Self {
292        let name = prompt_name.into();
293        Self::new(
294            ErrorKind::PromptNotFound,
295            alloc::format!("Prompt not found: {}", name),
296        )
297        .with_operation("prompt_lookup")
298        .with_component("prompt_registry")
299    }
300
301    /// Create a resource not found error
302    #[must_use]
303    pub fn resource_not_found(uri: impl Into<String>) -> Self {
304        let uri = uri.into();
305        Self::new(
306            ErrorKind::ResourceNotFound,
307            alloc::format!("Resource not found: {}", uri),
308        )
309        .with_operation("resource_lookup")
310        .with_component("resource_provider")
311    }
312
313    /// Create a resource access denied error
314    #[must_use]
315    pub fn resource_access_denied(uri: impl Into<String>, reason: impl Into<String>) -> Self {
316        let uri = uri.into();
317        let reason = reason.into();
318        Self::new(
319            ErrorKind::ResourceAccessDenied,
320            alloc::format!("Access denied to '{}': {}", uri, reason),
321        )
322        .with_operation("resource_access")
323        .with_component("resource_security")
324    }
325
326    /// Create a capability not supported error
327    #[must_use]
328    pub fn capability_not_supported(capability: impl Into<String>) -> Self {
329        let cap = capability.into();
330        Self::new(
331            ErrorKind::CapabilityNotSupported,
332            alloc::format!("Capability not supported: {}", cap),
333        )
334    }
335
336    /// Create a protocol version mismatch error
337    #[must_use]
338    pub fn protocol_version_mismatch(
339        client_version: impl Into<String>,
340        server_version: impl Into<String>,
341    ) -> Self {
342        let client = client_version.into();
343        let server = server_version.into();
344        Self::new(
345            ErrorKind::ProtocolVersionMismatch,
346            alloc::format!(
347                "Protocol version mismatch: client={}, server={}",
348                client,
349                server
350            ),
351        )
352    }
353
354    /// Create a timeout error
355    #[must_use]
356    pub fn timeout(message: impl Into<String>) -> Self {
357        Self::new(ErrorKind::Timeout, message)
358    }
359
360    /// Create a transport error
361    #[must_use]
362    pub fn transport(message: impl Into<String>) -> Self {
363        Self::new(ErrorKind::Transport, message)
364    }
365
366    /// Create an authentication error
367    #[must_use]
368    pub fn authentication(message: impl Into<String>) -> Self {
369        Self::new(ErrorKind::Authentication, message)
370    }
371
372    /// Create a permission denied error
373    #[must_use]
374    pub fn permission_denied(message: impl Into<String>) -> Self {
375        Self::new(ErrorKind::PermissionDenied, message)
376    }
377
378    /// Create a rate limited error
379    #[must_use]
380    pub fn rate_limited(message: impl Into<String>) -> Self {
381        Self::new(ErrorKind::RateLimited, message)
382    }
383
384    /// Create a cancelled error
385    #[must_use]
386    pub fn cancelled(message: impl Into<String>) -> Self {
387        Self::new(ErrorKind::Cancelled, message)
388    }
389
390    /// Create a user rejected error
391    #[must_use]
392    pub fn user_rejected(message: impl Into<String>) -> Self {
393        Self::new(ErrorKind::UserRejected, message)
394    }
395
396    /// Create a serialization error
397    #[must_use]
398    pub fn serialization(message: impl Into<String>) -> Self {
399        Self::new(ErrorKind::Serialization, message)
400    }
401
402    /// Create a security error
403    #[must_use]
404    pub fn security(message: impl Into<String>) -> Self {
405        Self::new(ErrorKind::Security, message)
406    }
407
408    /// Create an unavailable error
409    #[must_use]
410    pub fn unavailable(message: impl Into<String>) -> Self {
411        Self::new(ErrorKind::Unavailable, message)
412    }
413
414    /// Create a configuration error
415    #[must_use]
416    pub fn configuration(message: impl Into<String>) -> Self {
417        Self::new(ErrorKind::Configuration, message)
418    }
419
420    /// Create an external service error
421    #[must_use]
422    pub fn external_service(message: impl Into<String>) -> Self {
423        Self::new(ErrorKind::ExternalService, message)
424    }
425
426    /// Create a server overloaded error
427    #[must_use]
428    pub fn server_overloaded() -> Self {
429        Self::new(
430            ErrorKind::ServerOverloaded,
431            "Server is currently overloaded",
432        )
433    }
434
435    /// Create an error from a JSON-RPC error code
436    #[must_use]
437    pub fn from_rpc_code(code: i32, message: impl Into<String>) -> Self {
438        Self::new(ErrorKind::from_i32(code), message)
439    }
440
441    /// Set the operation context
442    #[must_use]
443    pub fn with_operation(mut self, operation: impl Into<String>) -> Self {
444        let ctx = self
445            .context
446            .get_or_insert_with(|| alloc::boxed::Box::new(ErrorContext::default()));
447        ctx.operation = Some(operation.into());
448        self
449    }
450
451    /// Set the component context
452    #[must_use]
453    pub fn with_component(mut self, component: impl Into<String>) -> Self {
454        let ctx = self
455            .context
456            .get_or_insert_with(|| alloc::boxed::Box::new(ErrorContext::default()));
457        ctx.component = Some(component.into());
458        self
459    }
460
461    /// Set the request ID context
462    #[must_use]
463    pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
464        let ctx = self
465            .context
466            .get_or_insert_with(|| alloc::boxed::Box::new(ErrorContext::default()));
467        ctx.request_id = Some(request_id.into());
468        self
469    }
470
471    /// Attach a structured payload that travels to the client.
472    ///
473    /// The value becomes the JSON-RPC error object's `data` member (see
474    /// `From<McpError> for JsonRpcError`) and the `io.turbomcp/errorData`
475    /// entry of a tool result's `_meta` (see [`Self::to_tool_result`]).
476    /// Nothing else in the error is forwarded verbatim — `operation`,
477    /// `component`, and `source_location` stay server-side — so this is the one
478    /// place to put machine-readable detail: a field path, a retry hint, a
479    /// validation report.
480    ///
481    /// # Example
482    ///
483    /// ```rust
484    /// use turbomcp_core::error::McpError;
485    ///
486    /// let err = McpError::invalid_params("start must precede end")
487    ///     .with_data(serde_json::json!({ "field": "start", "got": "2026-01-02" }));
488    /// assert_eq!(err.data().unwrap()["field"], "start");
489    /// ```
490    #[must_use]
491    pub fn with_data(mut self, data: serde_json::Value) -> Self {
492        let ctx = self
493            .context
494            .get_or_insert_with(|| alloc::boxed::Box::new(ErrorContext::default()));
495        ctx.data = Some(data);
496        self
497    }
498
499    /// The structured payload set by [`Self::with_data`], if any.
500    #[must_use]
501    pub fn data(&self) -> Option<&serde_json::Value> {
502        self.context.as_ref().and_then(|ctx| ctx.data.as_ref())
503    }
504
505    /// Render this error as a tool execution error, preserving its kind.
506    ///
507    /// Per MCP (SEP-1303) a tool that runs and fails reports the failure in
508    /// its result with `isError: true`, not as a JSON-RPC error, so the model
509    /// can read it and self-correct. That convention alone would erase the
510    /// error's classification, leaving a client unable to tell bad input from
511    /// an internal fault. The classification is therefore carried in `_meta`,
512    /// which the spec reserves for data the client — not the model — consumes:
513    ///
514    /// | key | value |
515    /// |-----|-------|
516    /// | `io.turbomcp/errorKind` | the [`ErrorKind`] as a snake_case string |
517    /// | `io.turbomcp/errorCode` | the JSON-RPC code the kind maps to |
518    /// | `io.turbomcp/errorData` | the [`Self::with_data`] payload, when set |
519    ///
520    /// # Example
521    ///
522    /// ```rust
523    /// use turbomcp_core::error::McpError;
524    ///
525    /// let result = McpError::invalid_params("bad date").to_tool_result();
526    /// assert!(result.is_error());
527    /// let meta = result.meta.unwrap();
528    /// assert_eq!(meta["io.turbomcp/errorCode"], -32602);
529    /// assert_eq!(meta["io.turbomcp/errorKind"], "invalid_params");
530    /// ```
531    #[must_use]
532    pub fn to_tool_result(&self) -> turbomcp_types::ToolResult {
533        use alloc::string::ToString;
534
535        let mut meta = turbomcp_types::MetaMap::new();
536        meta.insert(
537            crate::meta_keys::ERROR_KIND.to_string(),
538            serde_json::to_value(self.kind).unwrap_or(serde_json::Value::Null),
539        );
540        meta.insert(
541            crate::meta_keys::ERROR_CODE.to_string(),
542            serde_json::Value::from(self.jsonrpc_error_code()),
543        );
544        if let Some(data) = self.data() {
545            meta.insert(crate::meta_keys::ERROR_DATA.to_string(), data.clone());
546        }
547
548        turbomcp_types::ToolResult::error(self.to_string()).with_meta(meta)
549    }
550
551    /// Set the source location (typically file:line)
552    #[must_use]
553    pub fn with_source_location(mut self, location: impl Into<String>) -> Self {
554        self.source_location = Some(location.into());
555        self
556    }
557
558    /// Check if this error is retryable
559    #[must_use]
560    pub const fn is_retryable(&self) -> bool {
561        matches!(
562            self.kind,
563            ErrorKind::Timeout
564                | ErrorKind::Unavailable
565                | ErrorKind::Transport
566                | ErrorKind::ExternalService
567                | ErrorKind::RateLimited
568        )
569    }
570
571    /// Check if this error is temporary
572    #[must_use]
573    pub const fn is_temporary(&self) -> bool {
574        matches!(
575            self.kind,
576            ErrorKind::Timeout
577                | ErrorKind::Unavailable
578                | ErrorKind::RateLimited
579                | ErrorKind::ExternalService
580                | ErrorKind::ServerOverloaded
581        )
582    }
583
584    /// Get the JSON-RPC error code for this error
585    #[must_use]
586    pub const fn jsonrpc_code(&self) -> i32 {
587        self.jsonrpc_error_code()
588    }
589
590    /// Get the JSON-RPC error code (canonical name)
591    #[must_use]
592    pub const fn jsonrpc_error_code(&self) -> i32 {
593        match self.kind {
594            // JSON-RPC standard
595            ErrorKind::ParseError => -32700,
596            ErrorKind::InvalidRequest => -32600,
597            ErrorKind::MethodNotFound => -32601,
598            ErrorKind::InvalidParams => -32602,
599            // Serialization is a server-side bug; map to Internal so it doesn't
600            // collide on the wire with user-visible parameter validation errors.
601            ErrorKind::Internal | ErrorKind::Serialization => -32603,
602            // MCP specific
603            ErrorKind::UserRejected => -1,
604            ErrorKind::ToolNotFound => -32001,
605            ErrorKind::ToolExecutionFailed => -32002,
606            ErrorKind::PromptNotFound => -32003,
607            ErrorKind::ResourceNotFound => -32004,
608            ErrorKind::ResourceAccessDenied => -32005,
609            ErrorKind::CapabilityNotSupported => -32006,
610            ErrorKind::ProtocolVersionMismatch => -32007,
611            ErrorKind::UrlElicitationRequired => -32042,
612            ErrorKind::Authentication => -32008,
613            ErrorKind::RateLimited => -32009,
614            ErrorKind::ServerOverloaded => -32010,
615            // Application specific
616            ErrorKind::PermissionDenied => -32011,
617            ErrorKind::Timeout => -32012,
618            ErrorKind::Unavailable => -32013,
619            ErrorKind::Transport => -32014,
620            ErrorKind::Configuration => -32015,
621            ErrorKind::ExternalService => -32016,
622            ErrorKind::Cancelled => -32017,
623            ErrorKind::Security => -32018,
624        }
625    }
626
627    /// Get the HTTP status code equivalent
628    #[must_use]
629    pub const fn http_status(&self) -> u16 {
630        match self.kind {
631            // 4xx Client errors
632            ErrorKind::InvalidParams
633            | ErrorKind::InvalidRequest
634            | ErrorKind::UserRejected
635            | ErrorKind::ParseError => 400,
636            ErrorKind::Authentication => 401,
637            ErrorKind::PermissionDenied | ErrorKind::Security | ErrorKind::ResourceAccessDenied => {
638                403
639            }
640            ErrorKind::ToolNotFound
641            | ErrorKind::PromptNotFound
642            | ErrorKind::ResourceNotFound
643            | ErrorKind::MethodNotFound => 404,
644            // URL elicitation: server requests client open a URL to continue auth/consent
645            ErrorKind::UrlElicitationRequired => 403,
646            ErrorKind::Timeout => 408,
647            ErrorKind::RateLimited => 429,
648            ErrorKind::Cancelled => 499,
649            // 5xx Server errors
650            ErrorKind::Internal
651            | ErrorKind::Configuration
652            | ErrorKind::Serialization
653            | ErrorKind::ToolExecutionFailed
654            | ErrorKind::CapabilityNotSupported
655            | ErrorKind::ProtocolVersionMismatch => 500,
656            ErrorKind::Transport
657            | ErrorKind::ExternalService
658            | ErrorKind::Unavailable
659            | ErrorKind::ServerOverloaded => 503,
660        }
661    }
662}
663
664impl ErrorKind {
665    /// Create ErrorKind from a JSON-RPC error code.
666    ///
667    /// Includes standard JSON-RPC codes and MCP-specific codes per 2025-11-25 spec.
668    #[must_use]
669    pub fn from_i32(code: i32) -> Self {
670        match code {
671            // MCP-specific
672            -1 => Self::UserRejected,
673            -32001 => Self::ToolNotFound,
674            -32002 => Self::ToolExecutionFailed,
675            -32003 => Self::PromptNotFound,
676            -32004 => Self::ResourceNotFound,
677            -32005 => Self::ResourceAccessDenied,
678            -32006 => Self::CapabilityNotSupported,
679            -32007 => Self::ProtocolVersionMismatch,
680            -32008 => Self::Authentication,
681            -32009 => Self::RateLimited,
682            -32010 => Self::ServerOverloaded,
683            // MCP 2025-11-25: URL elicitation required
684            -32042 => Self::UrlElicitationRequired,
685            // Standard JSON-RPC
686            -32600 => Self::InvalidRequest,
687            -32601 => Self::MethodNotFound,
688            -32602 => Self::InvalidParams,
689            -32603 => Self::Internal,
690            -32700 => Self::ParseError,
691            _ => Self::Internal,
692        }
693    }
694
695    /// Get a human-readable description
696    #[must_use]
697    pub const fn description(self) -> &'static str {
698        match self {
699            Self::ToolNotFound => "Tool not found",
700            Self::ToolExecutionFailed => "Tool execution failed",
701            Self::PromptNotFound => "Prompt not found",
702            Self::ResourceNotFound => "Resource not found",
703            Self::ResourceAccessDenied => "Resource access denied",
704            Self::CapabilityNotSupported => "Capability not supported",
705            Self::ProtocolVersionMismatch => "Protocol version mismatch",
706            Self::UrlElicitationRequired => "URL elicitation required",
707            Self::UserRejected => "User rejected request",
708            Self::ParseError => "Parse error",
709            Self::InvalidRequest => "Invalid request",
710            Self::MethodNotFound => "Method not found",
711            Self::InvalidParams => "Invalid parameters",
712            Self::Internal => "Internal error",
713            Self::Authentication => "Authentication failed",
714            Self::PermissionDenied => "Permission denied",
715            Self::Transport => "Transport error",
716            Self::Timeout => "Operation timed out",
717            Self::Unavailable => "Service unavailable",
718            Self::RateLimited => "Rate limit exceeded",
719            Self::ServerOverloaded => "Server overloaded",
720            Self::Configuration => "Configuration error",
721            Self::ExternalService => "External service error",
722            Self::Cancelled => "Operation cancelled",
723            Self::Security => "Security violation",
724            Self::Serialization => "Serialization error",
725        }
726    }
727}
728
729impl fmt::Display for McpError {
730    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
731        write!(f, "{}", self.message)?;
732        if let Some(ctx) = &self.context {
733            if let Some(op) = &ctx.operation {
734                write!(f, " (operation: {})", op)?;
735            }
736            if let Some(comp) = &ctx.component {
737                write!(f, " (component: {})", comp)?;
738            }
739        }
740        Ok(())
741    }
742}
743
744impl fmt::Display for ErrorKind {
745    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
746        write!(f, "{}", self.description())
747    }
748}
749
750#[cfg(feature = "std")]
751impl std::error::Error for McpError {}
752
753// =========================================================================
754// From implementations for common error types
755// =========================================================================
756
757impl From<Box<McpError>> for McpError {
758    fn from(boxed: Box<McpError>) -> Self {
759        *boxed
760    }
761}
762
763impl From<serde_json::Error> for McpError {
764    fn from(err: serde_json::Error) -> Self {
765        // Categorize serde_json errors
766        let kind = if err.is_syntax() || err.is_eof() {
767            ErrorKind::ParseError
768        } else if err.is_data() {
769            ErrorKind::InvalidParams
770        } else {
771            ErrorKind::Serialization
772        };
773        Self::new(kind, alloc::format!("JSON error: {}", err))
774    }
775}
776
777#[cfg(feature = "std")]
778impl From<std::io::Error> for McpError {
779    fn from(err: std::io::Error) -> Self {
780        use std::io::ErrorKind as IoKind;
781        let kind = match err.kind() {
782            IoKind::NotFound => ErrorKind::ResourceNotFound,
783            IoKind::PermissionDenied => ErrorKind::PermissionDenied,
784            IoKind::ConnectionRefused
785            | IoKind::ConnectionReset
786            | IoKind::ConnectionAborted
787            | IoKind::NotConnected
788            | IoKind::BrokenPipe => ErrorKind::Transport,
789            IoKind::TimedOut => ErrorKind::Timeout,
790            _ => ErrorKind::Internal,
791        };
792        Self::new(kind, alloc::format!("IO error: {}", err))
793    }
794}
795
796/// Convenience macro for creating errors with location
797#[macro_export]
798macro_rules! mcp_err {
799    ($kind:expr, $msg:expr) => {
800        $crate::error::McpError::new($kind, $msg)
801            .with_source_location(concat!(file!(), ":", line!()))
802    };
803    ($kind:expr, $fmt:expr, $($arg:tt)*) => {
804        $crate::error::McpError::new($kind, alloc::format!($fmt, $($arg)*))
805            .with_source_location(concat!(file!(), ":", line!()))
806    };
807}
808
809#[cfg(test)]
810mod tests {
811    use super::*;
812    use alloc::string::ToString;
813
814    #[test]
815    fn test_error_creation() {
816        let err = McpError::invalid_params("missing field");
817        assert_eq!(err.kind, ErrorKind::InvalidParams);
818        assert!(err.message.contains("missing field"));
819    }
820
821    #[test]
822    fn test_error_context() {
823        let err = McpError::internal("test")
824            .with_operation("test_op")
825            .with_component("test_comp")
826            .with_request_id("req-123");
827
828        let ctx = err.context.unwrap();
829        assert_eq!(ctx.operation, Some("test_op".to_string()));
830        assert_eq!(ctx.component, Some("test_comp".to_string()));
831        assert_eq!(ctx.request_id, Some("req-123".to_string()));
832    }
833
834    #[test]
835    fn test_jsonrpc_codes() {
836        assert_eq!(McpError::tool_not_found("x").jsonrpc_code(), -32001);
837        assert_eq!(McpError::invalid_params("x").jsonrpc_code(), -32602);
838        assert_eq!(McpError::internal("x").jsonrpc_code(), -32603);
839    }
840
841    #[test]
842    fn test_retryable() {
843        assert!(McpError::timeout("x").is_retryable());
844        assert!(McpError::rate_limited("x").is_retryable());
845        assert!(!McpError::invalid_params("x").is_retryable());
846    }
847
848    #[test]
849    fn test_http_status() {
850        assert_eq!(McpError::tool_not_found("x").http_status(), 404);
851        assert_eq!(McpError::authentication("x").http_status(), 401);
852        assert_eq!(McpError::internal("x").http_status(), 500);
853    }
854
855    #[test]
856    fn test_error_size_reasonable() {
857        // McpError should fit in 2 cache lines (128 bytes) for efficient Result<T, E>
858        assert!(
859            core::mem::size_of::<McpError>() <= 128,
860            "McpError size: {} bytes (should be ≤128)",
861            core::mem::size_of::<McpError>()
862        );
863    }
864
865    // H-15: ErrorKind::from_i32 maps all known codes
866    #[test]
867    fn test_error_kind_from_i32() {
868        // MCP-specific codes
869        assert_eq!(ErrorKind::from_i32(-32001), ErrorKind::ToolNotFound);
870        assert_eq!(ErrorKind::from_i32(-32002), ErrorKind::ToolExecutionFailed);
871        assert_eq!(ErrorKind::from_i32(-32003), ErrorKind::PromptNotFound);
872        assert_eq!(ErrorKind::from_i32(-32004), ErrorKind::ResourceNotFound);
873        assert_eq!(ErrorKind::from_i32(-32005), ErrorKind::ResourceAccessDenied);
874        assert_eq!(
875            ErrorKind::from_i32(-32006),
876            ErrorKind::CapabilityNotSupported
877        );
878        assert_eq!(
879            ErrorKind::from_i32(-32007),
880            ErrorKind::ProtocolVersionMismatch
881        );
882        assert_eq!(ErrorKind::from_i32(-32008), ErrorKind::Authentication);
883        assert_eq!(ErrorKind::from_i32(-32009), ErrorKind::RateLimited);
884        assert_eq!(ErrorKind::from_i32(-32010), ErrorKind::ServerOverloaded);
885        // MCP 2025-11-25: URL elicitation required has its own variant
886        assert_eq!(
887            ErrorKind::from_i32(-32042),
888            ErrorKind::UrlElicitationRequired
889        );
890        // Standard JSON-RPC codes
891        assert_eq!(ErrorKind::from_i32(-32600), ErrorKind::InvalidRequest);
892        assert_eq!(ErrorKind::from_i32(-32601), ErrorKind::MethodNotFound);
893        assert_eq!(ErrorKind::from_i32(-32602), ErrorKind::InvalidParams);
894        assert_eq!(ErrorKind::from_i32(-32603), ErrorKind::Internal);
895        assert_eq!(ErrorKind::from_i32(-32700), ErrorKind::ParseError);
896        // Unknown codes fall back to Internal
897        assert_eq!(ErrorKind::from_i32(-99999), ErrorKind::Internal);
898        assert_eq!(ErrorKind::from_i32(0), ErrorKind::Internal);
899    }
900}