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 #[serde(skip_serializing_if = "Option::is_none")]
83 pub data: Option<serde_json::Value>,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
91#[serde(rename_all = "snake_case")]
92#[non_exhaustive]
93pub enum ErrorKind {
94 ToolNotFound,
97 ToolExecutionFailed,
99 PromptNotFound,
101 ResourceNotFound,
103 ResourceAccessDenied,
105 CapabilityNotSupported,
107 ProtocolVersionMismatch,
109 UrlElicitationRequired,
111 UserRejected,
113
114 ParseError,
117 InvalidRequest,
119 MethodNotFound,
121 InvalidParams,
123 Internal,
125
126 Authentication,
129 PermissionDenied,
131 Transport,
133 Timeout,
135 Unavailable,
137 RateLimited,
139 ServerOverloaded,
141 Configuration,
143 ExternalService,
145 Cancelled,
147 Security,
149 Serialization,
151}
152
153impl McpError {
154 #[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 #[cfg(feature = "rich-errors")]
171 #[must_use]
172 pub const fn id(&self) -> uuid::Uuid {
173 self.id
174 }
175
176 #[cfg(feature = "rich-errors")]
178 #[must_use]
179 pub const fn timestamp(&self) -> chrono::DateTime<chrono::Utc> {
180 self.timestamp
181 }
182
183 #[must_use]
185 pub fn invalid_params(message: impl Into<String>) -> Self {
186 Self::new(ErrorKind::InvalidParams, message)
187 }
188
189 #[must_use]
191 pub fn internal(message: impl Into<String>) -> Self {
192 Self::new(ErrorKind::Internal, message)
193 }
194
195 #[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 #[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 #[must_use]
238 pub fn sanitized(mut self) -> Self {
239 self.message = crate::security::sanitize_error_message(&self.message);
240 self
241 }
242
243 #[must_use]
245 pub fn parse_error(message: impl Into<String>) -> Self {
246 Self::new(ErrorKind::ParseError, message)
247 }
248
249 #[must_use]
251 pub fn invalid_request(message: impl Into<String>) -> Self {
252 Self::new(ErrorKind::InvalidRequest, message)
253 }
254
255 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
356 pub fn timeout(message: impl Into<String>) -> Self {
357 Self::new(ErrorKind::Timeout, message)
358 }
359
360 #[must_use]
362 pub fn transport(message: impl Into<String>) -> Self {
363 Self::new(ErrorKind::Transport, message)
364 }
365
366 #[must_use]
368 pub fn authentication(message: impl Into<String>) -> Self {
369 Self::new(ErrorKind::Authentication, message)
370 }
371
372 #[must_use]
374 pub fn permission_denied(message: impl Into<String>) -> Self {
375 Self::new(ErrorKind::PermissionDenied, message)
376 }
377
378 #[must_use]
380 pub fn rate_limited(message: impl Into<String>) -> Self {
381 Self::new(ErrorKind::RateLimited, message)
382 }
383
384 #[must_use]
386 pub fn cancelled(message: impl Into<String>) -> Self {
387 Self::new(ErrorKind::Cancelled, message)
388 }
389
390 #[must_use]
392 pub fn user_rejected(message: impl Into<String>) -> Self {
393 Self::new(ErrorKind::UserRejected, message)
394 }
395
396 #[must_use]
398 pub fn serialization(message: impl Into<String>) -> Self {
399 Self::new(ErrorKind::Serialization, message)
400 }
401
402 #[must_use]
404 pub fn security(message: impl Into<String>) -> Self {
405 Self::new(ErrorKind::Security, message)
406 }
407
408 #[must_use]
410 pub fn unavailable(message: impl Into<String>) -> Self {
411 Self::new(ErrorKind::Unavailable, message)
412 }
413
414 #[must_use]
416 pub fn configuration(message: impl Into<String>) -> Self {
417 Self::new(ErrorKind::Configuration, message)
418 }
419
420 #[must_use]
422 pub fn external_service(message: impl Into<String>) -> Self {
423 Self::new(ErrorKind::ExternalService, message)
424 }
425
426 #[must_use]
428 pub fn server_overloaded() -> Self {
429 Self::new(
430 ErrorKind::ServerOverloaded,
431 "Server is currently overloaded",
432 )
433 }
434
435 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
586 pub const fn jsonrpc_code(&self) -> i32 {
587 self.jsonrpc_error_code()
588 }
589
590 #[must_use]
592 pub const fn jsonrpc_error_code(&self) -> i32 {
593 match self.kind {
594 ErrorKind::ParseError => -32700,
596 ErrorKind::InvalidRequest => -32600,
597 ErrorKind::MethodNotFound => -32601,
598 ErrorKind::InvalidParams => -32602,
599 ErrorKind::Internal | ErrorKind::Serialization => -32603,
602 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 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 #[must_use]
629 pub const fn http_status(&self) -> u16 {
630 match self.kind {
631 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 ErrorKind::UrlElicitationRequired => 403,
646 ErrorKind::Timeout => 408,
647 ErrorKind::RateLimited => 429,
648 ErrorKind::Cancelled => 499,
649 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 #[must_use]
669 pub fn from_i32(code: i32) -> Self {
670 match code {
671 -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 -32042 => Self::UrlElicitationRequired,
685 -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 #[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
753impl 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 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#[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 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 #[test]
867 fn test_error_kind_from_i32() {
868 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 assert_eq!(
887 ErrorKind::from_i32(-32042),
888 ErrorKind::UrlElicitationRequired
889 );
890 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 assert_eq!(ErrorKind::from_i32(-99999), ErrorKind::Internal);
898 assert_eq!(ErrorKind::from_i32(0), ErrorKind::Internal);
899 }
900}