1use alloc::boxed::Box;
29use alloc::string::String;
30use core::fmt;
31use serde::{Deserialize, Serialize};
32
33pub type McpResult<T> = core::result::Result<T, McpError>;
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct McpError {
46 #[cfg(feature = "rich-errors")]
48 pub id: uuid::Uuid,
49 pub kind: ErrorKind,
51 pub message: String,
53 #[serde(skip_serializing)]
56 pub source_location: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub context: Option<alloc::boxed::Box<ErrorContext>>,
60 #[cfg(feature = "rich-errors")]
62 pub timestamp: chrono::DateTime<chrono::Utc>,
63}
64
65#[derive(Debug, Clone, Default, Serialize, Deserialize)]
67pub struct ErrorContext {
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub operation: Option<String>,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub component: Option<String>,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub request_id: Option<String>,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
84#[serde(rename_all = "snake_case")]
85#[non_exhaustive]
86pub enum ErrorKind {
87 ToolNotFound,
90 ToolExecutionFailed,
92 PromptNotFound,
94 ResourceNotFound,
96 ResourceAccessDenied,
98 CapabilityNotSupported,
100 ProtocolVersionMismatch,
102 UserRejected,
104
105 ParseError,
108 InvalidRequest,
110 MethodNotFound,
112 InvalidParams,
114 Internal,
116
117 Authentication,
120 PermissionDenied,
122 Transport,
124 Timeout,
126 Unavailable,
128 RateLimited,
130 ServerOverloaded,
132 Configuration,
134 ExternalService,
136 Cancelled,
138 Security,
140 Serialization,
142}
143
144impl McpError {
145 #[must_use]
147 pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
148 Self {
149 #[cfg(feature = "rich-errors")]
150 id: uuid::Uuid::new_v4(),
151 kind,
152 message: message.into(),
153 source_location: None,
154 context: None,
155 #[cfg(feature = "rich-errors")]
156 timestamp: chrono::Utc::now(),
157 }
158 }
159
160 #[cfg(feature = "rich-errors")]
162 #[must_use]
163 pub const fn id(&self) -> uuid::Uuid {
164 self.id
165 }
166
167 #[cfg(feature = "rich-errors")]
169 #[must_use]
170 pub const fn timestamp(&self) -> chrono::DateTime<chrono::Utc> {
171 self.timestamp
172 }
173
174 #[must_use]
176 pub fn invalid_params(message: impl Into<String>) -> Self {
177 Self::new(ErrorKind::InvalidParams, message)
178 }
179
180 #[must_use]
182 pub fn internal(message: impl Into<String>) -> Self {
183 Self::new(ErrorKind::Internal, message)
184 }
185
186 #[must_use]
202 pub fn safe_internal(message: impl Into<String>) -> Self {
203 let sanitized = crate::security::sanitize_error_message(&message.into());
204 Self::new(ErrorKind::Internal, sanitized)
205 }
206
207 #[must_use]
211 pub fn safe_tool_execution_failed(
212 tool_name: impl Into<String>,
213 reason: impl Into<String>,
214 ) -> Self {
215 let name = tool_name.into();
216 let sanitized_reason = crate::security::sanitize_error_message(&reason.into());
217 Self::new(
218 ErrorKind::ToolExecutionFailed,
219 alloc::format!("Tool '{}' failed: {}", name, sanitized_reason),
220 )
221 .with_operation("tool_execution")
222 }
223
224 #[must_use]
229 pub fn sanitized(mut self) -> Self {
230 self.message = crate::security::sanitize_error_message(&self.message);
231 self
232 }
233
234 #[must_use]
236 pub fn parse_error(message: impl Into<String>) -> Self {
237 Self::new(ErrorKind::ParseError, message)
238 }
239
240 #[must_use]
242 pub fn invalid_request(message: impl Into<String>) -> Self {
243 Self::new(ErrorKind::InvalidRequest, message)
244 }
245
246 #[must_use]
248 pub fn method_not_found(method: impl Into<String>) -> Self {
249 let method = method.into();
250 Self::new(
251 ErrorKind::MethodNotFound,
252 alloc::format!("Method not found: {}", method),
253 )
254 }
255
256 #[must_use]
258 pub fn tool_not_found(tool_name: impl Into<String>) -> Self {
259 let name = tool_name.into();
260 Self::new(
261 ErrorKind::ToolNotFound,
262 alloc::format!("Tool not found: {}", name),
263 )
264 .with_operation("tool_lookup")
265 .with_component("tool_registry")
266 }
267
268 #[must_use]
270 pub fn tool_execution_failed(tool_name: impl Into<String>, reason: impl Into<String>) -> Self {
271 let name = tool_name.into();
272 let reason = reason.into();
273 Self::new(
274 ErrorKind::ToolExecutionFailed,
275 alloc::format!("Tool '{}' failed: {}", name, reason),
276 )
277 .with_operation("tool_execution")
278 }
279
280 #[must_use]
282 pub fn prompt_not_found(prompt_name: impl Into<String>) -> Self {
283 let name = prompt_name.into();
284 Self::new(
285 ErrorKind::PromptNotFound,
286 alloc::format!("Prompt not found: {}", name),
287 )
288 .with_operation("prompt_lookup")
289 .with_component("prompt_registry")
290 }
291
292 #[must_use]
294 pub fn resource_not_found(uri: impl Into<String>) -> Self {
295 let uri = uri.into();
296 Self::new(
297 ErrorKind::ResourceNotFound,
298 alloc::format!("Resource not found: {}", uri),
299 )
300 .with_operation("resource_lookup")
301 .with_component("resource_provider")
302 }
303
304 #[must_use]
306 pub fn resource_access_denied(uri: impl Into<String>, reason: impl Into<String>) -> Self {
307 let uri = uri.into();
308 let reason = reason.into();
309 Self::new(
310 ErrorKind::ResourceAccessDenied,
311 alloc::format!("Access denied to '{}': {}", uri, reason),
312 )
313 .with_operation("resource_access")
314 .with_component("resource_security")
315 }
316
317 #[must_use]
319 pub fn capability_not_supported(capability: impl Into<String>) -> Self {
320 let cap = capability.into();
321 Self::new(
322 ErrorKind::CapabilityNotSupported,
323 alloc::format!("Capability not supported: {}", cap),
324 )
325 }
326
327 #[must_use]
329 pub fn protocol_version_mismatch(
330 client_version: impl Into<String>,
331 server_version: impl Into<String>,
332 ) -> Self {
333 let client = client_version.into();
334 let server = server_version.into();
335 Self::new(
336 ErrorKind::ProtocolVersionMismatch,
337 alloc::format!(
338 "Protocol version mismatch: client={}, server={}",
339 client,
340 server
341 ),
342 )
343 }
344
345 #[must_use]
347 pub fn timeout(message: impl Into<String>) -> Self {
348 Self::new(ErrorKind::Timeout, message)
349 }
350
351 #[must_use]
353 pub fn transport(message: impl Into<String>) -> Self {
354 Self::new(ErrorKind::Transport, message)
355 }
356
357 #[must_use]
359 pub fn authentication(message: impl Into<String>) -> Self {
360 Self::new(ErrorKind::Authentication, message)
361 }
362
363 #[must_use]
365 pub fn permission_denied(message: impl Into<String>) -> Self {
366 Self::new(ErrorKind::PermissionDenied, message)
367 }
368
369 #[must_use]
371 pub fn rate_limited(message: impl Into<String>) -> Self {
372 Self::new(ErrorKind::RateLimited, message)
373 }
374
375 #[must_use]
377 pub fn cancelled(message: impl Into<String>) -> Self {
378 Self::new(ErrorKind::Cancelled, message)
379 }
380
381 #[must_use]
383 pub fn user_rejected(message: impl Into<String>) -> Self {
384 Self::new(ErrorKind::UserRejected, message)
385 }
386
387 #[must_use]
389 pub fn serialization(message: impl Into<String>) -> Self {
390 Self::new(ErrorKind::Serialization, message)
391 }
392
393 #[must_use]
395 pub fn security(message: impl Into<String>) -> Self {
396 Self::new(ErrorKind::Security, message)
397 }
398
399 #[must_use]
401 pub fn unavailable(message: impl Into<String>) -> Self {
402 Self::new(ErrorKind::Unavailable, message)
403 }
404
405 #[must_use]
407 pub fn configuration(message: impl Into<String>) -> Self {
408 Self::new(ErrorKind::Configuration, message)
409 }
410
411 #[must_use]
413 pub fn external_service(message: impl Into<String>) -> Self {
414 Self::new(ErrorKind::ExternalService, message)
415 }
416
417 #[must_use]
419 pub fn server_overloaded() -> Self {
420 Self::new(
421 ErrorKind::ServerOverloaded,
422 "Server is currently overloaded",
423 )
424 }
425
426 #[must_use]
428 pub fn from_rpc_code(code: i32, message: impl Into<String>) -> Self {
429 let kind = match code {
430 -1 => ErrorKind::UserRejected,
431 -32001 => ErrorKind::ToolNotFound,
432 -32002 => ErrorKind::ToolExecutionFailed,
433 -32003 => ErrorKind::PromptNotFound,
434 -32004 => ErrorKind::ResourceNotFound,
435 -32005 => ErrorKind::ResourceAccessDenied,
436 -32006 => ErrorKind::CapabilityNotSupported,
437 -32007 => ErrorKind::ProtocolVersionMismatch,
438 -32008 => ErrorKind::Authentication,
439 -32009 => ErrorKind::RateLimited,
440 -32010 => ErrorKind::ServerOverloaded,
441 -32600 => ErrorKind::InvalidRequest,
442 -32601 => ErrorKind::MethodNotFound,
443 -32602 => ErrorKind::InvalidParams,
444 -32603 => ErrorKind::Internal,
445 -32700 => ErrorKind::ParseError,
446 _ => ErrorKind::Internal,
447 };
448 Self::new(kind, message)
449 }
450
451 #[must_use]
453 pub fn with_operation(mut self, operation: impl Into<String>) -> Self {
454 let ctx = self
455 .context
456 .get_or_insert_with(|| alloc::boxed::Box::new(ErrorContext::default()));
457 ctx.operation = Some(operation.into());
458 self
459 }
460
461 #[must_use]
463 pub fn with_component(mut self, component: impl Into<String>) -> Self {
464 let ctx = self
465 .context
466 .get_or_insert_with(|| alloc::boxed::Box::new(ErrorContext::default()));
467 ctx.component = Some(component.into());
468 self
469 }
470
471 #[must_use]
473 pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
474 let ctx = self
475 .context
476 .get_or_insert_with(|| alloc::boxed::Box::new(ErrorContext::default()));
477 ctx.request_id = Some(request_id.into());
478 self
479 }
480
481 #[must_use]
483 pub fn with_source_location(mut self, location: impl Into<String>) -> Self {
484 self.source_location = Some(location.into());
485 self
486 }
487
488 #[must_use]
490 pub const fn is_retryable(&self) -> bool {
491 matches!(
492 self.kind,
493 ErrorKind::Timeout
494 | ErrorKind::Unavailable
495 | ErrorKind::Transport
496 | ErrorKind::ExternalService
497 | ErrorKind::RateLimited
498 )
499 }
500
501 #[must_use]
503 pub const fn is_temporary(&self) -> bool {
504 matches!(
505 self.kind,
506 ErrorKind::Timeout
507 | ErrorKind::Unavailable
508 | ErrorKind::RateLimited
509 | ErrorKind::ExternalService
510 | ErrorKind::ServerOverloaded
511 )
512 }
513
514 #[must_use]
516 pub const fn jsonrpc_code(&self) -> i32 {
517 self.jsonrpc_error_code()
518 }
519
520 #[must_use]
522 pub const fn jsonrpc_error_code(&self) -> i32 {
523 match self.kind {
524 ErrorKind::ParseError => -32700,
526 ErrorKind::InvalidRequest => -32600,
527 ErrorKind::MethodNotFound => -32601,
528 ErrorKind::InvalidParams | ErrorKind::Serialization => -32602,
529 ErrorKind::Internal => -32603,
530 ErrorKind::UserRejected => -1,
532 ErrorKind::ToolNotFound => -32001,
533 ErrorKind::ToolExecutionFailed => -32002,
534 ErrorKind::PromptNotFound => -32003,
535 ErrorKind::ResourceNotFound => -32004,
536 ErrorKind::ResourceAccessDenied => -32005,
537 ErrorKind::CapabilityNotSupported => -32006,
538 ErrorKind::ProtocolVersionMismatch => -32007,
539 ErrorKind::Authentication => -32008,
540 ErrorKind::RateLimited => -32009,
541 ErrorKind::ServerOverloaded => -32010,
542 ErrorKind::PermissionDenied => -32011,
544 ErrorKind::Timeout => -32012,
545 ErrorKind::Unavailable => -32013,
546 ErrorKind::Transport => -32014,
547 ErrorKind::Configuration => -32015,
548 ErrorKind::ExternalService => -32016,
549 ErrorKind::Cancelled => -32017,
550 ErrorKind::Security => -32018,
551 }
552 }
553
554 #[must_use]
556 pub const fn http_status(&self) -> u16 {
557 match self.kind {
558 ErrorKind::InvalidParams
560 | ErrorKind::InvalidRequest
561 | ErrorKind::UserRejected
562 | ErrorKind::ParseError => 400,
563 ErrorKind::Authentication => 401,
564 ErrorKind::PermissionDenied | ErrorKind::Security | ErrorKind::ResourceAccessDenied => {
565 403
566 }
567 ErrorKind::ToolNotFound
568 | ErrorKind::PromptNotFound
569 | ErrorKind::ResourceNotFound
570 | ErrorKind::MethodNotFound => 404,
571 ErrorKind::Timeout => 408,
572 ErrorKind::RateLimited => 429,
573 ErrorKind::Cancelled => 499,
574 ErrorKind::Internal
576 | ErrorKind::Configuration
577 | ErrorKind::Serialization
578 | ErrorKind::ToolExecutionFailed
579 | ErrorKind::CapabilityNotSupported
580 | ErrorKind::ProtocolVersionMismatch => 500,
581 ErrorKind::Transport
582 | ErrorKind::ExternalService
583 | ErrorKind::Unavailable
584 | ErrorKind::ServerOverloaded => 503,
585 }
586 }
587}
588
589impl ErrorKind {
590 #[must_use]
594 pub fn from_i32(code: i32) -> Self {
595 match code {
596 -1 => Self::UserRejected,
598 -32001 => Self::ToolNotFound,
599 -32002 => Self::ToolExecutionFailed,
600 -32003 => Self::PromptNotFound,
601 -32004 => Self::ResourceNotFound,
602 -32005 => Self::ResourceAccessDenied,
603 -32006 => Self::CapabilityNotSupported,
604 -32007 => Self::ProtocolVersionMismatch,
605 -32008 => Self::Authentication,
606 -32009 => Self::RateLimited,
607 -32010 => Self::ServerOverloaded,
608 -32042 => Self::CapabilityNotSupported,
610 -32600 => Self::InvalidRequest,
612 -32601 => Self::MethodNotFound,
613 -32602 => Self::InvalidParams,
614 -32603 => Self::Internal,
615 -32700 => Self::ParseError,
616 _ => Self::Internal,
617 }
618 }
619
620 #[must_use]
622 pub const fn description(self) -> &'static str {
623 match self {
624 Self::ToolNotFound => "Tool not found",
625 Self::ToolExecutionFailed => "Tool execution failed",
626 Self::PromptNotFound => "Prompt not found",
627 Self::ResourceNotFound => "Resource not found",
628 Self::ResourceAccessDenied => "Resource access denied",
629 Self::CapabilityNotSupported => "Capability not supported",
630 Self::ProtocolVersionMismatch => "Protocol version mismatch",
631 Self::UserRejected => "User rejected request",
632 Self::ParseError => "Parse error",
633 Self::InvalidRequest => "Invalid request",
634 Self::MethodNotFound => "Method not found",
635 Self::InvalidParams => "Invalid parameters",
636 Self::Internal => "Internal error",
637 Self::Authentication => "Authentication failed",
638 Self::PermissionDenied => "Permission denied",
639 Self::Transport => "Transport error",
640 Self::Timeout => "Operation timed out",
641 Self::Unavailable => "Service unavailable",
642 Self::RateLimited => "Rate limit exceeded",
643 Self::ServerOverloaded => "Server overloaded",
644 Self::Configuration => "Configuration error",
645 Self::ExternalService => "External service error",
646 Self::Cancelled => "Operation cancelled",
647 Self::Security => "Security violation",
648 Self::Serialization => "Serialization error",
649 }
650 }
651}
652
653impl fmt::Display for McpError {
654 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
655 write!(f, "{}", self.message)?;
656 if let Some(ctx) = &self.context {
657 if let Some(op) = &ctx.operation {
658 write!(f, " (operation: {})", op)?;
659 }
660 if let Some(comp) = &ctx.component {
661 write!(f, " (component: {})", comp)?;
662 }
663 }
664 Ok(())
665 }
666}
667
668impl fmt::Display for ErrorKind {
669 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
670 write!(f, "{}", self.description())
671 }
672}
673
674#[cfg(feature = "std")]
675impl std::error::Error for McpError {}
676
677impl From<Box<McpError>> for McpError {
682 fn from(boxed: Box<McpError>) -> Self {
683 *boxed
684 }
685}
686
687impl From<serde_json::Error> for McpError {
688 fn from(err: serde_json::Error) -> Self {
689 let kind = if err.is_syntax() || err.is_eof() {
691 ErrorKind::ParseError
692 } else if err.is_data() {
693 ErrorKind::InvalidParams
694 } else {
695 ErrorKind::Serialization
696 };
697 Self::new(kind, alloc::format!("JSON error: {}", err))
698 }
699}
700
701#[cfg(feature = "std")]
702impl From<std::io::Error> for McpError {
703 fn from(err: std::io::Error) -> Self {
704 use std::io::ErrorKind as IoKind;
705 let kind = match err.kind() {
706 IoKind::NotFound => ErrorKind::ResourceNotFound,
707 IoKind::PermissionDenied => ErrorKind::PermissionDenied,
708 IoKind::ConnectionRefused
709 | IoKind::ConnectionReset
710 | IoKind::ConnectionAborted
711 | IoKind::NotConnected
712 | IoKind::BrokenPipe => ErrorKind::Transport,
713 IoKind::TimedOut => ErrorKind::Timeout,
714 _ => ErrorKind::Internal,
715 };
716 Self::new(kind, alloc::format!("IO error: {}", err))
717 }
718}
719
720#[macro_export]
722macro_rules! mcp_err {
723 ($kind:expr, $msg:expr) => {
724 $crate::error::McpError::new($kind, $msg)
725 .with_source_location(concat!(file!(), ":", line!()))
726 };
727 ($kind:expr, $fmt:expr, $($arg:tt)*) => {
728 $crate::error::McpError::new($kind, alloc::format!($fmt, $($arg)*))
729 .with_source_location(concat!(file!(), ":", line!()))
730 };
731}
732
733#[cfg(test)]
734mod tests {
735 use super::*;
736 use alloc::string::ToString;
737
738 #[test]
739 fn test_error_creation() {
740 let err = McpError::invalid_params("missing field");
741 assert_eq!(err.kind, ErrorKind::InvalidParams);
742 assert!(err.message.contains("missing field"));
743 }
744
745 #[test]
746 fn test_error_context() {
747 let err = McpError::internal("test")
748 .with_operation("test_op")
749 .with_component("test_comp")
750 .with_request_id("req-123");
751
752 let ctx = err.context.unwrap();
753 assert_eq!(ctx.operation, Some("test_op".to_string()));
754 assert_eq!(ctx.component, Some("test_comp".to_string()));
755 assert_eq!(ctx.request_id, Some("req-123".to_string()));
756 }
757
758 #[test]
759 fn test_jsonrpc_codes() {
760 assert_eq!(McpError::tool_not_found("x").jsonrpc_code(), -32001);
761 assert_eq!(McpError::invalid_params("x").jsonrpc_code(), -32602);
762 assert_eq!(McpError::internal("x").jsonrpc_code(), -32603);
763 }
764
765 #[test]
766 fn test_retryable() {
767 assert!(McpError::timeout("x").is_retryable());
768 assert!(McpError::rate_limited("x").is_retryable());
769 assert!(!McpError::invalid_params("x").is_retryable());
770 }
771
772 #[test]
773 fn test_http_status() {
774 assert_eq!(McpError::tool_not_found("x").http_status(), 404);
775 assert_eq!(McpError::authentication("x").http_status(), 401);
776 assert_eq!(McpError::internal("x").http_status(), 500);
777 }
778
779 #[test]
780 fn test_error_size_reasonable() {
781 assert!(
783 core::mem::size_of::<McpError>() <= 128,
784 "McpError size: {} bytes (should be ≤128)",
785 core::mem::size_of::<McpError>()
786 );
787 }
788
789 #[test]
791 fn test_error_kind_from_i32() {
792 assert_eq!(ErrorKind::from_i32(-32001), ErrorKind::ToolNotFound);
794 assert_eq!(ErrorKind::from_i32(-32002), ErrorKind::ToolExecutionFailed);
795 assert_eq!(ErrorKind::from_i32(-32003), ErrorKind::PromptNotFound);
796 assert_eq!(ErrorKind::from_i32(-32004), ErrorKind::ResourceNotFound);
797 assert_eq!(ErrorKind::from_i32(-32005), ErrorKind::ResourceAccessDenied);
798 assert_eq!(
799 ErrorKind::from_i32(-32006),
800 ErrorKind::CapabilityNotSupported
801 );
802 assert_eq!(
803 ErrorKind::from_i32(-32007),
804 ErrorKind::ProtocolVersionMismatch
805 );
806 assert_eq!(ErrorKind::from_i32(-32008), ErrorKind::Authentication);
807 assert_eq!(ErrorKind::from_i32(-32009), ErrorKind::RateLimited);
808 assert_eq!(ErrorKind::from_i32(-32010), ErrorKind::ServerOverloaded);
809 assert_eq!(
811 ErrorKind::from_i32(-32042),
812 ErrorKind::CapabilityNotSupported
813 );
814 assert_eq!(ErrorKind::from_i32(-32600), ErrorKind::InvalidRequest);
816 assert_eq!(ErrorKind::from_i32(-32601), ErrorKind::MethodNotFound);
817 assert_eq!(ErrorKind::from_i32(-32602), ErrorKind::InvalidParams);
818 assert_eq!(ErrorKind::from_i32(-32603), ErrorKind::Internal);
819 assert_eq!(ErrorKind::from_i32(-32700), ErrorKind::ParseError);
820 assert_eq!(ErrorKind::from_i32(-99999), ErrorKind::Internal);
822 assert_eq!(ErrorKind::from_i32(0), ErrorKind::Internal);
823 }
824}