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}
78
79/// Error classification for programmatic handling.
80///
81/// This enum is `#[non_exhaustive]` — new variants may be added in future
82/// minor releases without a breaking change.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
84#[serde(rename_all = "snake_case")]
85#[non_exhaustive]
86pub enum ErrorKind {
87    // === MCP-Specific Errors ===
88    /// Tool not found (MCP -32001)
89    ToolNotFound,
90    /// Tool execution failed (MCP -32002)
91    ToolExecutionFailed,
92    /// Prompt not found (MCP -32003)
93    PromptNotFound,
94    /// Resource not found (MCP -32004)
95    ResourceNotFound,
96    /// Resource access denied (MCP -32005)
97    ResourceAccessDenied,
98    /// Capability not supported (MCP -32006)
99    CapabilityNotSupported,
100    /// Protocol version mismatch (MCP -32007)
101    ProtocolVersionMismatch,
102    /// URL elicitation required (MCP -32042)
103    UrlElicitationRequired,
104    /// User rejected the request (MCP -1)
105    UserRejected,
106
107    // === JSON-RPC Standard Errors ===
108    /// Parse error (-32700)
109    ParseError,
110    /// Invalid request (-32600)
111    InvalidRequest,
112    /// Method not found (-32601)
113    MethodNotFound,
114    /// Invalid params (-32602)
115    InvalidParams,
116    /// Internal error (-32603)
117    Internal,
118
119    // === General Application Errors ===
120    /// Authentication failed
121    Authentication,
122    /// Permission denied
123    PermissionDenied,
124    /// Transport/network error
125    Transport,
126    /// Operation timed out
127    Timeout,
128    /// Service unavailable
129    Unavailable,
130    /// Rate limited (-32009)
131    RateLimited,
132    /// Server overloaded (-32010)
133    ServerOverloaded,
134    /// Configuration error
135    Configuration,
136    /// External service failed
137    ExternalService,
138    /// Operation cancelled
139    Cancelled,
140    /// Security violation
141    Security,
142    /// Serialization error
143    Serialization,
144}
145
146impl McpError {
147    /// Create a new error with kind and message
148    #[must_use]
149    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
150        Self {
151            #[cfg(feature = "rich-errors")]
152            id: uuid::Uuid::new_v4(),
153            kind,
154            message: message.into(),
155            source_location: None,
156            context: None,
157            #[cfg(feature = "rich-errors")]
158            timestamp: chrono::Utc::now(),
159        }
160    }
161
162    /// Get the error ID (only available with `rich-errors` feature)
163    #[cfg(feature = "rich-errors")]
164    #[must_use]
165    pub const fn id(&self) -> uuid::Uuid {
166        self.id
167    }
168
169    /// Get the error timestamp (only available with `rich-errors` feature)
170    #[cfg(feature = "rich-errors")]
171    #[must_use]
172    pub const fn timestamp(&self) -> chrono::DateTime<chrono::Utc> {
173        self.timestamp
174    }
175
176    /// Create a validation/invalid params error
177    #[must_use]
178    pub fn invalid_params(message: impl Into<String>) -> Self {
179        Self::new(ErrorKind::InvalidParams, message)
180    }
181
182    /// Create an internal error
183    #[must_use]
184    pub fn internal(message: impl Into<String>) -> Self {
185        Self::new(ErrorKind::Internal, message)
186    }
187
188    /// Create a safe internal error with sanitized message.
189    ///
190    /// Use this for errors that may contain sensitive information (file paths,
191    /// IP addresses, connection strings, etc.). The message is automatically
192    /// sanitized to prevent information leakage per OWASP guidelines.
193    ///
194    /// # Example
195    ///
196    /// ```rust
197    /// use turbomcp_core::error::McpError;
198    ///
199    /// let err = McpError::safe_internal("Failed: postgres://admin:secret@192.168.1.1/db");
200    /// assert!(!err.message.contains("secret"));
201    /// assert!(!err.message.contains("192.168.1.1"));
202    /// ```
203    #[must_use]
204    pub fn safe_internal(message: impl Into<String>) -> Self {
205        let sanitized = crate::security::sanitize_error_message(&message.into());
206        Self::new(ErrorKind::Internal, sanitized)
207    }
208
209    /// Create a safe tool execution error with sanitized message.
210    ///
211    /// Like [`safe_internal`](Self::safe_internal), but specifically for tool execution failures.
212    #[must_use]
213    pub fn safe_tool_execution_failed(
214        tool_name: impl Into<String>,
215        reason: impl Into<String>,
216    ) -> Self {
217        let name = tool_name.into();
218        let sanitized_reason = crate::security::sanitize_error_message(&reason.into());
219        Self::new(
220            ErrorKind::ToolExecutionFailed,
221            alloc::format!("Tool '{}' failed: {}", name, sanitized_reason),
222        )
223        .with_operation("tool_execution")
224    }
225
226    /// Sanitize this error's message in-place.
227    ///
228    /// Call this before returning errors to clients in production to ensure
229    /// no sensitive information is leaked.
230    #[must_use]
231    pub fn sanitized(mut self) -> Self {
232        self.message = crate::security::sanitize_error_message(&self.message);
233        self
234    }
235
236    /// Create a parse error
237    #[must_use]
238    pub fn parse_error(message: impl Into<String>) -> Self {
239        Self::new(ErrorKind::ParseError, message)
240    }
241
242    /// Create an invalid request error
243    #[must_use]
244    pub fn invalid_request(message: impl Into<String>) -> Self {
245        Self::new(ErrorKind::InvalidRequest, message)
246    }
247
248    /// Create a method not found error
249    #[must_use]
250    pub fn method_not_found(method: impl Into<String>) -> Self {
251        let method = method.into();
252        Self::new(
253            ErrorKind::MethodNotFound,
254            alloc::format!("Method not found: {}", method),
255        )
256    }
257
258    /// Create a tool not found error
259    #[must_use]
260    pub fn tool_not_found(tool_name: impl Into<String>) -> Self {
261        let name = tool_name.into();
262        Self::new(
263            ErrorKind::ToolNotFound,
264            alloc::format!("Tool not found: {}", name),
265        )
266        .with_operation("tool_lookup")
267        .with_component("tool_registry")
268    }
269
270    /// Create a tool execution failed error
271    #[must_use]
272    pub fn tool_execution_failed(tool_name: impl Into<String>, reason: impl Into<String>) -> Self {
273        let name = tool_name.into();
274        let reason = reason.into();
275        Self::new(
276            ErrorKind::ToolExecutionFailed,
277            alloc::format!("Tool '{}' failed: {}", name, reason),
278        )
279        .with_operation("tool_execution")
280    }
281
282    /// Create a prompt not found error
283    #[must_use]
284    pub fn prompt_not_found(prompt_name: impl Into<String>) -> Self {
285        let name = prompt_name.into();
286        Self::new(
287            ErrorKind::PromptNotFound,
288            alloc::format!("Prompt not found: {}", name),
289        )
290        .with_operation("prompt_lookup")
291        .with_component("prompt_registry")
292    }
293
294    /// Create a resource not found error
295    #[must_use]
296    pub fn resource_not_found(uri: impl Into<String>) -> Self {
297        let uri = uri.into();
298        Self::new(
299            ErrorKind::ResourceNotFound,
300            alloc::format!("Resource not found: {}", uri),
301        )
302        .with_operation("resource_lookup")
303        .with_component("resource_provider")
304    }
305
306    /// Create a resource access denied error
307    #[must_use]
308    pub fn resource_access_denied(uri: impl Into<String>, reason: impl Into<String>) -> Self {
309        let uri = uri.into();
310        let reason = reason.into();
311        Self::new(
312            ErrorKind::ResourceAccessDenied,
313            alloc::format!("Access denied to '{}': {}", uri, reason),
314        )
315        .with_operation("resource_access")
316        .with_component("resource_security")
317    }
318
319    /// Create a capability not supported error
320    #[must_use]
321    pub fn capability_not_supported(capability: impl Into<String>) -> Self {
322        let cap = capability.into();
323        Self::new(
324            ErrorKind::CapabilityNotSupported,
325            alloc::format!("Capability not supported: {}", cap),
326        )
327    }
328
329    /// Create a protocol version mismatch error
330    #[must_use]
331    pub fn protocol_version_mismatch(
332        client_version: impl Into<String>,
333        server_version: impl Into<String>,
334    ) -> Self {
335        let client = client_version.into();
336        let server = server_version.into();
337        Self::new(
338            ErrorKind::ProtocolVersionMismatch,
339            alloc::format!(
340                "Protocol version mismatch: client={}, server={}",
341                client,
342                server
343            ),
344        )
345    }
346
347    /// Create a timeout error
348    #[must_use]
349    pub fn timeout(message: impl Into<String>) -> Self {
350        Self::new(ErrorKind::Timeout, message)
351    }
352
353    /// Create a transport error
354    #[must_use]
355    pub fn transport(message: impl Into<String>) -> Self {
356        Self::new(ErrorKind::Transport, message)
357    }
358
359    /// Create an authentication error
360    #[must_use]
361    pub fn authentication(message: impl Into<String>) -> Self {
362        Self::new(ErrorKind::Authentication, message)
363    }
364
365    /// Create a permission denied error
366    #[must_use]
367    pub fn permission_denied(message: impl Into<String>) -> Self {
368        Self::new(ErrorKind::PermissionDenied, message)
369    }
370
371    /// Create a rate limited error
372    #[must_use]
373    pub fn rate_limited(message: impl Into<String>) -> Self {
374        Self::new(ErrorKind::RateLimited, message)
375    }
376
377    /// Create a cancelled error
378    #[must_use]
379    pub fn cancelled(message: impl Into<String>) -> Self {
380        Self::new(ErrorKind::Cancelled, message)
381    }
382
383    /// Create a user rejected error
384    #[must_use]
385    pub fn user_rejected(message: impl Into<String>) -> Self {
386        Self::new(ErrorKind::UserRejected, message)
387    }
388
389    /// Create a serialization error
390    #[must_use]
391    pub fn serialization(message: impl Into<String>) -> Self {
392        Self::new(ErrorKind::Serialization, message)
393    }
394
395    /// Create a security error
396    #[must_use]
397    pub fn security(message: impl Into<String>) -> Self {
398        Self::new(ErrorKind::Security, message)
399    }
400
401    /// Create an unavailable error
402    #[must_use]
403    pub fn unavailable(message: impl Into<String>) -> Self {
404        Self::new(ErrorKind::Unavailable, message)
405    }
406
407    /// Create a configuration error
408    #[must_use]
409    pub fn configuration(message: impl Into<String>) -> Self {
410        Self::new(ErrorKind::Configuration, message)
411    }
412
413    /// Create an external service error
414    #[must_use]
415    pub fn external_service(message: impl Into<String>) -> Self {
416        Self::new(ErrorKind::ExternalService, message)
417    }
418
419    /// Create a server overloaded error
420    #[must_use]
421    pub fn server_overloaded() -> Self {
422        Self::new(
423            ErrorKind::ServerOverloaded,
424            "Server is currently overloaded",
425        )
426    }
427
428    /// Create an error from a JSON-RPC error code
429    #[must_use]
430    pub fn from_rpc_code(code: i32, message: impl Into<String>) -> Self {
431        Self::new(ErrorKind::from_i32(code), message)
432    }
433
434    /// Set the operation context
435    #[must_use]
436    pub fn with_operation(mut self, operation: impl Into<String>) -> Self {
437        let ctx = self
438            .context
439            .get_or_insert_with(|| alloc::boxed::Box::new(ErrorContext::default()));
440        ctx.operation = Some(operation.into());
441        self
442    }
443
444    /// Set the component context
445    #[must_use]
446    pub fn with_component(mut self, component: impl Into<String>) -> Self {
447        let ctx = self
448            .context
449            .get_or_insert_with(|| alloc::boxed::Box::new(ErrorContext::default()));
450        ctx.component = Some(component.into());
451        self
452    }
453
454    /// Set the request ID context
455    #[must_use]
456    pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
457        let ctx = self
458            .context
459            .get_or_insert_with(|| alloc::boxed::Box::new(ErrorContext::default()));
460        ctx.request_id = Some(request_id.into());
461        self
462    }
463
464    /// Set the source location (typically file:line)
465    #[must_use]
466    pub fn with_source_location(mut self, location: impl Into<String>) -> Self {
467        self.source_location = Some(location.into());
468        self
469    }
470
471    /// Check if this error is retryable
472    #[must_use]
473    pub const fn is_retryable(&self) -> bool {
474        matches!(
475            self.kind,
476            ErrorKind::Timeout
477                | ErrorKind::Unavailable
478                | ErrorKind::Transport
479                | ErrorKind::ExternalService
480                | ErrorKind::RateLimited
481        )
482    }
483
484    /// Check if this error is temporary
485    #[must_use]
486    pub const fn is_temporary(&self) -> bool {
487        matches!(
488            self.kind,
489            ErrorKind::Timeout
490                | ErrorKind::Unavailable
491                | ErrorKind::RateLimited
492                | ErrorKind::ExternalService
493                | ErrorKind::ServerOverloaded
494        )
495    }
496
497    /// Get the JSON-RPC error code for this error
498    #[must_use]
499    pub const fn jsonrpc_code(&self) -> i32 {
500        self.jsonrpc_error_code()
501    }
502
503    /// Get the JSON-RPC error code (canonical name)
504    #[must_use]
505    pub const fn jsonrpc_error_code(&self) -> i32 {
506        match self.kind {
507            // JSON-RPC standard
508            ErrorKind::ParseError => -32700,
509            ErrorKind::InvalidRequest => -32600,
510            ErrorKind::MethodNotFound => -32601,
511            ErrorKind::InvalidParams => -32602,
512            // Serialization is a server-side bug; map to Internal so it doesn't
513            // collide on the wire with user-visible parameter validation errors.
514            ErrorKind::Internal | ErrorKind::Serialization => -32603,
515            // MCP specific
516            ErrorKind::UserRejected => -1,
517            ErrorKind::ToolNotFound => -32001,
518            ErrorKind::ToolExecutionFailed => -32002,
519            ErrorKind::PromptNotFound => -32003,
520            ErrorKind::ResourceNotFound => -32004,
521            ErrorKind::ResourceAccessDenied => -32005,
522            ErrorKind::CapabilityNotSupported => -32006,
523            ErrorKind::ProtocolVersionMismatch => -32007,
524            ErrorKind::UrlElicitationRequired => -32042,
525            ErrorKind::Authentication => -32008,
526            ErrorKind::RateLimited => -32009,
527            ErrorKind::ServerOverloaded => -32010,
528            // Application specific
529            ErrorKind::PermissionDenied => -32011,
530            ErrorKind::Timeout => -32012,
531            ErrorKind::Unavailable => -32013,
532            ErrorKind::Transport => -32014,
533            ErrorKind::Configuration => -32015,
534            ErrorKind::ExternalService => -32016,
535            ErrorKind::Cancelled => -32017,
536            ErrorKind::Security => -32018,
537        }
538    }
539
540    /// Get the HTTP status code equivalent
541    #[must_use]
542    pub const fn http_status(&self) -> u16 {
543        match self.kind {
544            // 4xx Client errors
545            ErrorKind::InvalidParams
546            | ErrorKind::InvalidRequest
547            | ErrorKind::UserRejected
548            | ErrorKind::ParseError => 400,
549            ErrorKind::Authentication => 401,
550            ErrorKind::PermissionDenied | ErrorKind::Security | ErrorKind::ResourceAccessDenied => {
551                403
552            }
553            ErrorKind::ToolNotFound
554            | ErrorKind::PromptNotFound
555            | ErrorKind::ResourceNotFound
556            | ErrorKind::MethodNotFound => 404,
557            // URL elicitation: server requests client open a URL to continue auth/consent
558            ErrorKind::UrlElicitationRequired => 403,
559            ErrorKind::Timeout => 408,
560            ErrorKind::RateLimited => 429,
561            ErrorKind::Cancelled => 499,
562            // 5xx Server errors
563            ErrorKind::Internal
564            | ErrorKind::Configuration
565            | ErrorKind::Serialization
566            | ErrorKind::ToolExecutionFailed
567            | ErrorKind::CapabilityNotSupported
568            | ErrorKind::ProtocolVersionMismatch => 500,
569            ErrorKind::Transport
570            | ErrorKind::ExternalService
571            | ErrorKind::Unavailable
572            | ErrorKind::ServerOverloaded => 503,
573        }
574    }
575}
576
577impl ErrorKind {
578    /// Create ErrorKind from a JSON-RPC error code.
579    ///
580    /// Includes standard JSON-RPC codes and MCP-specific codes per 2025-11-25 spec.
581    #[must_use]
582    pub fn from_i32(code: i32) -> Self {
583        match code {
584            // MCP-specific
585            -1 => Self::UserRejected,
586            -32001 => Self::ToolNotFound,
587            -32002 => Self::ToolExecutionFailed,
588            -32003 => Self::PromptNotFound,
589            -32004 => Self::ResourceNotFound,
590            -32005 => Self::ResourceAccessDenied,
591            -32006 => Self::CapabilityNotSupported,
592            -32007 => Self::ProtocolVersionMismatch,
593            -32008 => Self::Authentication,
594            -32009 => Self::RateLimited,
595            -32010 => Self::ServerOverloaded,
596            // MCP 2025-11-25: URL elicitation required
597            -32042 => Self::UrlElicitationRequired,
598            // Standard JSON-RPC
599            -32600 => Self::InvalidRequest,
600            -32601 => Self::MethodNotFound,
601            -32602 => Self::InvalidParams,
602            -32603 => Self::Internal,
603            -32700 => Self::ParseError,
604            _ => Self::Internal,
605        }
606    }
607
608    /// Get a human-readable description
609    #[must_use]
610    pub const fn description(self) -> &'static str {
611        match self {
612            Self::ToolNotFound => "Tool not found",
613            Self::ToolExecutionFailed => "Tool execution failed",
614            Self::PromptNotFound => "Prompt not found",
615            Self::ResourceNotFound => "Resource not found",
616            Self::ResourceAccessDenied => "Resource access denied",
617            Self::CapabilityNotSupported => "Capability not supported",
618            Self::ProtocolVersionMismatch => "Protocol version mismatch",
619            Self::UrlElicitationRequired => "URL elicitation required",
620            Self::UserRejected => "User rejected request",
621            Self::ParseError => "Parse error",
622            Self::InvalidRequest => "Invalid request",
623            Self::MethodNotFound => "Method not found",
624            Self::InvalidParams => "Invalid parameters",
625            Self::Internal => "Internal error",
626            Self::Authentication => "Authentication failed",
627            Self::PermissionDenied => "Permission denied",
628            Self::Transport => "Transport error",
629            Self::Timeout => "Operation timed out",
630            Self::Unavailable => "Service unavailable",
631            Self::RateLimited => "Rate limit exceeded",
632            Self::ServerOverloaded => "Server overloaded",
633            Self::Configuration => "Configuration error",
634            Self::ExternalService => "External service error",
635            Self::Cancelled => "Operation cancelled",
636            Self::Security => "Security violation",
637            Self::Serialization => "Serialization error",
638        }
639    }
640}
641
642impl fmt::Display for McpError {
643    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
644        write!(f, "{}", self.message)?;
645        if let Some(ctx) = &self.context {
646            if let Some(op) = &ctx.operation {
647                write!(f, " (operation: {})", op)?;
648            }
649            if let Some(comp) = &ctx.component {
650                write!(f, " (component: {})", comp)?;
651            }
652        }
653        Ok(())
654    }
655}
656
657impl fmt::Display for ErrorKind {
658    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
659        write!(f, "{}", self.description())
660    }
661}
662
663#[cfg(feature = "std")]
664impl std::error::Error for McpError {}
665
666// =========================================================================
667// From implementations for common error types
668// =========================================================================
669
670impl From<Box<McpError>> for McpError {
671    fn from(boxed: Box<McpError>) -> Self {
672        *boxed
673    }
674}
675
676impl From<serde_json::Error> for McpError {
677    fn from(err: serde_json::Error) -> Self {
678        // Categorize serde_json errors
679        let kind = if err.is_syntax() || err.is_eof() {
680            ErrorKind::ParseError
681        } else if err.is_data() {
682            ErrorKind::InvalidParams
683        } else {
684            ErrorKind::Serialization
685        };
686        Self::new(kind, alloc::format!("JSON error: {}", err))
687    }
688}
689
690#[cfg(feature = "std")]
691impl From<std::io::Error> for McpError {
692    fn from(err: std::io::Error) -> Self {
693        use std::io::ErrorKind as IoKind;
694        let kind = match err.kind() {
695            IoKind::NotFound => ErrorKind::ResourceNotFound,
696            IoKind::PermissionDenied => ErrorKind::PermissionDenied,
697            IoKind::ConnectionRefused
698            | IoKind::ConnectionReset
699            | IoKind::ConnectionAborted
700            | IoKind::NotConnected
701            | IoKind::BrokenPipe => ErrorKind::Transport,
702            IoKind::TimedOut => ErrorKind::Timeout,
703            _ => ErrorKind::Internal,
704        };
705        Self::new(kind, alloc::format!("IO error: {}", err))
706    }
707}
708
709/// Convenience macro for creating errors with location
710#[macro_export]
711macro_rules! mcp_err {
712    ($kind:expr, $msg:expr) => {
713        $crate::error::McpError::new($kind, $msg)
714            .with_source_location(concat!(file!(), ":", line!()))
715    };
716    ($kind:expr, $fmt:expr, $($arg:tt)*) => {
717        $crate::error::McpError::new($kind, alloc::format!($fmt, $($arg)*))
718            .with_source_location(concat!(file!(), ":", line!()))
719    };
720}
721
722#[cfg(test)]
723mod tests {
724    use super::*;
725    use alloc::string::ToString;
726
727    #[test]
728    fn test_error_creation() {
729        let err = McpError::invalid_params("missing field");
730        assert_eq!(err.kind, ErrorKind::InvalidParams);
731        assert!(err.message.contains("missing field"));
732    }
733
734    #[test]
735    fn test_error_context() {
736        let err = McpError::internal("test")
737            .with_operation("test_op")
738            .with_component("test_comp")
739            .with_request_id("req-123");
740
741        let ctx = err.context.unwrap();
742        assert_eq!(ctx.operation, Some("test_op".to_string()));
743        assert_eq!(ctx.component, Some("test_comp".to_string()));
744        assert_eq!(ctx.request_id, Some("req-123".to_string()));
745    }
746
747    #[test]
748    fn test_jsonrpc_codes() {
749        assert_eq!(McpError::tool_not_found("x").jsonrpc_code(), -32001);
750        assert_eq!(McpError::invalid_params("x").jsonrpc_code(), -32602);
751        assert_eq!(McpError::internal("x").jsonrpc_code(), -32603);
752    }
753
754    #[test]
755    fn test_retryable() {
756        assert!(McpError::timeout("x").is_retryable());
757        assert!(McpError::rate_limited("x").is_retryable());
758        assert!(!McpError::invalid_params("x").is_retryable());
759    }
760
761    #[test]
762    fn test_http_status() {
763        assert_eq!(McpError::tool_not_found("x").http_status(), 404);
764        assert_eq!(McpError::authentication("x").http_status(), 401);
765        assert_eq!(McpError::internal("x").http_status(), 500);
766    }
767
768    #[test]
769    fn test_error_size_reasonable() {
770        // McpError should fit in 2 cache lines (128 bytes) for efficient Result<T, E>
771        assert!(
772            core::mem::size_of::<McpError>() <= 128,
773            "McpError size: {} bytes (should be ≤128)",
774            core::mem::size_of::<McpError>()
775        );
776    }
777
778    // H-15: ErrorKind::from_i32 maps all known codes
779    #[test]
780    fn test_error_kind_from_i32() {
781        // MCP-specific codes
782        assert_eq!(ErrorKind::from_i32(-32001), ErrorKind::ToolNotFound);
783        assert_eq!(ErrorKind::from_i32(-32002), ErrorKind::ToolExecutionFailed);
784        assert_eq!(ErrorKind::from_i32(-32003), ErrorKind::PromptNotFound);
785        assert_eq!(ErrorKind::from_i32(-32004), ErrorKind::ResourceNotFound);
786        assert_eq!(ErrorKind::from_i32(-32005), ErrorKind::ResourceAccessDenied);
787        assert_eq!(
788            ErrorKind::from_i32(-32006),
789            ErrorKind::CapabilityNotSupported
790        );
791        assert_eq!(
792            ErrorKind::from_i32(-32007),
793            ErrorKind::ProtocolVersionMismatch
794        );
795        assert_eq!(ErrorKind::from_i32(-32008), ErrorKind::Authentication);
796        assert_eq!(ErrorKind::from_i32(-32009), ErrorKind::RateLimited);
797        assert_eq!(ErrorKind::from_i32(-32010), ErrorKind::ServerOverloaded);
798        // MCP 2025-11-25: URL elicitation required has its own variant
799        assert_eq!(
800            ErrorKind::from_i32(-32042),
801            ErrorKind::UrlElicitationRequired
802        );
803        // Standard JSON-RPC codes
804        assert_eq!(ErrorKind::from_i32(-32600), ErrorKind::InvalidRequest);
805        assert_eq!(ErrorKind::from_i32(-32601), ErrorKind::MethodNotFound);
806        assert_eq!(ErrorKind::from_i32(-32602), ErrorKind::InvalidParams);
807        assert_eq!(ErrorKind::from_i32(-32603), ErrorKind::Internal);
808        assert_eq!(ErrorKind::from_i32(-32700), ErrorKind::ParseError);
809        // Unknown codes fall back to Internal
810        assert_eq!(ErrorKind::from_i32(-99999), ErrorKind::Internal);
811        assert_eq!(ErrorKind::from_i32(0), ErrorKind::Internal);
812    }
813}