use alloc::boxed::Box;
use alloc::string::String;
use core::fmt;
use serde::{Deserialize, Serialize};
pub type McpResult<T> = core::result::Result<T, McpError>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpError {
#[cfg(feature = "rich-errors")]
pub id: uuid::Uuid,
pub kind: ErrorKind,
pub message: String,
#[serde(skip_serializing)]
pub source_location: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<alloc::boxed::Box<ErrorContext>>,
#[cfg(feature = "rich-errors")]
pub timestamp: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ErrorContext {
#[serde(skip_serializing_if = "Option::is_none")]
pub operation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub component: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ErrorKind {
ToolNotFound,
ToolExecutionFailed,
PromptNotFound,
ResourceNotFound,
ResourceAccessDenied,
CapabilityNotSupported,
ProtocolVersionMismatch,
UserRejected,
ParseError,
InvalidRequest,
MethodNotFound,
InvalidParams,
Internal,
Authentication,
PermissionDenied,
Transport,
Timeout,
Unavailable,
RateLimited,
ServerOverloaded,
Configuration,
ExternalService,
Cancelled,
Security,
Serialization,
}
impl McpError {
#[must_use]
pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
Self {
#[cfg(feature = "rich-errors")]
id: uuid::Uuid::new_v4(),
kind,
message: message.into(),
source_location: None,
context: None,
#[cfg(feature = "rich-errors")]
timestamp: chrono::Utc::now(),
}
}
#[cfg(feature = "rich-errors")]
#[must_use]
pub const fn id(&self) -> uuid::Uuid {
self.id
}
#[cfg(feature = "rich-errors")]
#[must_use]
pub const fn timestamp(&self) -> chrono::DateTime<chrono::Utc> {
self.timestamp
}
#[must_use]
pub fn invalid_params(message: impl Into<String>) -> Self {
Self::new(ErrorKind::InvalidParams, message)
}
#[must_use]
pub fn internal(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Internal, message)
}
#[must_use]
pub fn safe_internal(message: impl Into<String>) -> Self {
let sanitized = crate::security::sanitize_error_message(&message.into());
Self::new(ErrorKind::Internal, sanitized)
}
#[must_use]
pub fn safe_tool_execution_failed(
tool_name: impl Into<String>,
reason: impl Into<String>,
) -> Self {
let name = tool_name.into();
let sanitized_reason = crate::security::sanitize_error_message(&reason.into());
Self::new(
ErrorKind::ToolExecutionFailed,
alloc::format!("Tool '{}' failed: {}", name, sanitized_reason),
)
.with_operation("tool_execution")
}
#[must_use]
pub fn sanitized(mut self) -> Self {
self.message = crate::security::sanitize_error_message(&self.message);
self
}
#[must_use]
pub fn parse_error(message: impl Into<String>) -> Self {
Self::new(ErrorKind::ParseError, message)
}
#[must_use]
pub fn invalid_request(message: impl Into<String>) -> Self {
Self::new(ErrorKind::InvalidRequest, message)
}
#[must_use]
pub fn method_not_found(method: impl Into<String>) -> Self {
let method = method.into();
Self::new(
ErrorKind::MethodNotFound,
alloc::format!("Method not found: {}", method),
)
}
#[must_use]
pub fn tool_not_found(tool_name: impl Into<String>) -> Self {
let name = tool_name.into();
Self::new(
ErrorKind::ToolNotFound,
alloc::format!("Tool not found: {}", name),
)
.with_operation("tool_lookup")
.with_component("tool_registry")
}
#[must_use]
pub fn tool_execution_failed(tool_name: impl Into<String>, reason: impl Into<String>) -> Self {
let name = tool_name.into();
let reason = reason.into();
Self::new(
ErrorKind::ToolExecutionFailed,
alloc::format!("Tool '{}' failed: {}", name, reason),
)
.with_operation("tool_execution")
}
#[must_use]
pub fn prompt_not_found(prompt_name: impl Into<String>) -> Self {
let name = prompt_name.into();
Self::new(
ErrorKind::PromptNotFound,
alloc::format!("Prompt not found: {}", name),
)
.with_operation("prompt_lookup")
.with_component("prompt_registry")
}
#[must_use]
pub fn resource_not_found(uri: impl Into<String>) -> Self {
let uri = uri.into();
Self::new(
ErrorKind::ResourceNotFound,
alloc::format!("Resource not found: {}", uri),
)
.with_operation("resource_lookup")
.with_component("resource_provider")
}
#[must_use]
pub fn resource_access_denied(uri: impl Into<String>, reason: impl Into<String>) -> Self {
let uri = uri.into();
let reason = reason.into();
Self::new(
ErrorKind::ResourceAccessDenied,
alloc::format!("Access denied to '{}': {}", uri, reason),
)
.with_operation("resource_access")
.with_component("resource_security")
}
#[must_use]
pub fn capability_not_supported(capability: impl Into<String>) -> Self {
let cap = capability.into();
Self::new(
ErrorKind::CapabilityNotSupported,
alloc::format!("Capability not supported: {}", cap),
)
}
#[must_use]
pub fn protocol_version_mismatch(
client_version: impl Into<String>,
server_version: impl Into<String>,
) -> Self {
let client = client_version.into();
let server = server_version.into();
Self::new(
ErrorKind::ProtocolVersionMismatch,
alloc::format!(
"Protocol version mismatch: client={}, server={}",
client,
server
),
)
}
#[must_use]
pub fn timeout(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Timeout, message)
}
#[must_use]
pub fn transport(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Transport, message)
}
#[must_use]
pub fn authentication(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Authentication, message)
}
#[must_use]
pub fn permission_denied(message: impl Into<String>) -> Self {
Self::new(ErrorKind::PermissionDenied, message)
}
#[must_use]
pub fn rate_limited(message: impl Into<String>) -> Self {
Self::new(ErrorKind::RateLimited, message)
}
#[must_use]
pub fn cancelled(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Cancelled, message)
}
#[must_use]
pub fn user_rejected(message: impl Into<String>) -> Self {
Self::new(ErrorKind::UserRejected, message)
}
#[must_use]
pub fn serialization(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Serialization, message)
}
#[must_use]
pub fn security(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Security, message)
}
#[must_use]
pub fn unavailable(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Unavailable, message)
}
#[must_use]
pub fn configuration(message: impl Into<String>) -> Self {
Self::new(ErrorKind::Configuration, message)
}
#[must_use]
pub fn external_service(message: impl Into<String>) -> Self {
Self::new(ErrorKind::ExternalService, message)
}
#[must_use]
pub fn server_overloaded() -> Self {
Self::new(
ErrorKind::ServerOverloaded,
"Server is currently overloaded",
)
}
#[must_use]
pub fn from_rpc_code(code: i32, message: impl Into<String>) -> Self {
let kind = match code {
-1 => ErrorKind::UserRejected,
-32001 => ErrorKind::ToolNotFound,
-32002 => ErrorKind::ToolExecutionFailed,
-32003 => ErrorKind::PromptNotFound,
-32004 => ErrorKind::ResourceNotFound,
-32005 => ErrorKind::ResourceAccessDenied,
-32006 => ErrorKind::CapabilityNotSupported,
-32007 => ErrorKind::ProtocolVersionMismatch,
-32008 => ErrorKind::Authentication,
-32009 => ErrorKind::RateLimited,
-32010 => ErrorKind::ServerOverloaded,
-32600 => ErrorKind::InvalidRequest,
-32601 => ErrorKind::MethodNotFound,
-32602 => ErrorKind::InvalidParams,
-32603 => ErrorKind::Internal,
-32700 => ErrorKind::ParseError,
_ => ErrorKind::Internal,
};
Self::new(kind, message)
}
#[must_use]
pub fn with_operation(mut self, operation: impl Into<String>) -> Self {
let ctx = self
.context
.get_or_insert_with(|| alloc::boxed::Box::new(ErrorContext::default()));
ctx.operation = Some(operation.into());
self
}
#[must_use]
pub fn with_component(mut self, component: impl Into<String>) -> Self {
let ctx = self
.context
.get_or_insert_with(|| alloc::boxed::Box::new(ErrorContext::default()));
ctx.component = Some(component.into());
self
}
#[must_use]
pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
let ctx = self
.context
.get_or_insert_with(|| alloc::boxed::Box::new(ErrorContext::default()));
ctx.request_id = Some(request_id.into());
self
}
#[must_use]
pub fn with_source_location(mut self, location: impl Into<String>) -> Self {
self.source_location = Some(location.into());
self
}
#[must_use]
pub const fn is_retryable(&self) -> bool {
matches!(
self.kind,
ErrorKind::Timeout
| ErrorKind::Unavailable
| ErrorKind::Transport
| ErrorKind::ExternalService
| ErrorKind::RateLimited
)
}
#[must_use]
pub const fn is_temporary(&self) -> bool {
matches!(
self.kind,
ErrorKind::Timeout
| ErrorKind::Unavailable
| ErrorKind::RateLimited
| ErrorKind::ExternalService
| ErrorKind::ServerOverloaded
)
}
#[must_use]
pub const fn jsonrpc_code(&self) -> i32 {
self.jsonrpc_error_code()
}
#[must_use]
pub const fn jsonrpc_error_code(&self) -> i32 {
match self.kind {
ErrorKind::ParseError => -32700,
ErrorKind::InvalidRequest => -32600,
ErrorKind::MethodNotFound => -32601,
ErrorKind::InvalidParams | ErrorKind::Serialization => -32602,
ErrorKind::Internal => -32603,
ErrorKind::UserRejected => -1,
ErrorKind::ToolNotFound => -32001,
ErrorKind::ToolExecutionFailed => -32002,
ErrorKind::PromptNotFound => -32003,
ErrorKind::ResourceNotFound => -32004,
ErrorKind::ResourceAccessDenied => -32005,
ErrorKind::CapabilityNotSupported => -32006,
ErrorKind::ProtocolVersionMismatch => -32007,
ErrorKind::Authentication => -32008,
ErrorKind::RateLimited => -32009,
ErrorKind::ServerOverloaded => -32010,
ErrorKind::PermissionDenied => -32011,
ErrorKind::Timeout => -32012,
ErrorKind::Unavailable => -32013,
ErrorKind::Transport => -32014,
ErrorKind::Configuration => -32015,
ErrorKind::ExternalService => -32016,
ErrorKind::Cancelled => -32017,
ErrorKind::Security => -32018,
}
}
#[must_use]
pub const fn http_status(&self) -> u16 {
match self.kind {
ErrorKind::InvalidParams
| ErrorKind::InvalidRequest
| ErrorKind::UserRejected
| ErrorKind::ParseError => 400,
ErrorKind::Authentication => 401,
ErrorKind::PermissionDenied | ErrorKind::Security | ErrorKind::ResourceAccessDenied => {
403
}
ErrorKind::ToolNotFound
| ErrorKind::PromptNotFound
| ErrorKind::ResourceNotFound
| ErrorKind::MethodNotFound => 404,
ErrorKind::Timeout => 408,
ErrorKind::RateLimited => 429,
ErrorKind::Cancelled => 499,
ErrorKind::Internal
| ErrorKind::Configuration
| ErrorKind::Serialization
| ErrorKind::ToolExecutionFailed
| ErrorKind::CapabilityNotSupported
| ErrorKind::ProtocolVersionMismatch => 500,
ErrorKind::Transport
| ErrorKind::ExternalService
| ErrorKind::Unavailable
| ErrorKind::ServerOverloaded => 503,
}
}
}
impl ErrorKind {
#[must_use]
pub fn from_i32(code: i32) -> Self {
match code {
-1 => Self::UserRejected,
-32001 => Self::ToolNotFound,
-32002 => Self::ToolExecutionFailed,
-32003 => Self::PromptNotFound,
-32004 => Self::ResourceNotFound,
-32005 => Self::ResourceAccessDenied,
-32006 => Self::CapabilityNotSupported,
-32007 => Self::ProtocolVersionMismatch,
-32008 => Self::Authentication,
-32009 => Self::RateLimited,
-32010 => Self::ServerOverloaded,
-32042 => Self::CapabilityNotSupported,
-32600 => Self::InvalidRequest,
-32601 => Self::MethodNotFound,
-32602 => Self::InvalidParams,
-32603 => Self::Internal,
-32700 => Self::ParseError,
_ => Self::Internal,
}
}
#[must_use]
pub const fn description(self) -> &'static str {
match self {
Self::ToolNotFound => "Tool not found",
Self::ToolExecutionFailed => "Tool execution failed",
Self::PromptNotFound => "Prompt not found",
Self::ResourceNotFound => "Resource not found",
Self::ResourceAccessDenied => "Resource access denied",
Self::CapabilityNotSupported => "Capability not supported",
Self::ProtocolVersionMismatch => "Protocol version mismatch",
Self::UserRejected => "User rejected request",
Self::ParseError => "Parse error",
Self::InvalidRequest => "Invalid request",
Self::MethodNotFound => "Method not found",
Self::InvalidParams => "Invalid parameters",
Self::Internal => "Internal error",
Self::Authentication => "Authentication failed",
Self::PermissionDenied => "Permission denied",
Self::Transport => "Transport error",
Self::Timeout => "Operation timed out",
Self::Unavailable => "Service unavailable",
Self::RateLimited => "Rate limit exceeded",
Self::ServerOverloaded => "Server overloaded",
Self::Configuration => "Configuration error",
Self::ExternalService => "External service error",
Self::Cancelled => "Operation cancelled",
Self::Security => "Security violation",
Self::Serialization => "Serialization error",
}
}
}
impl fmt::Display for McpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)?;
if let Some(ctx) = &self.context {
if let Some(op) = &ctx.operation {
write!(f, " (operation: {})", op)?;
}
if let Some(comp) = &ctx.component {
write!(f, " (component: {})", comp)?;
}
}
Ok(())
}
}
impl fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.description())
}
}
#[cfg(feature = "std")]
impl std::error::Error for McpError {}
impl From<Box<McpError>> for McpError {
fn from(boxed: Box<McpError>) -> Self {
*boxed
}
}
impl From<serde_json::Error> for McpError {
fn from(err: serde_json::Error) -> Self {
let kind = if err.is_syntax() || err.is_eof() {
ErrorKind::ParseError
} else if err.is_data() {
ErrorKind::InvalidParams
} else {
ErrorKind::Serialization
};
Self::new(kind, alloc::format!("JSON error: {}", err))
}
}
#[cfg(feature = "std")]
impl From<std::io::Error> for McpError {
fn from(err: std::io::Error) -> Self {
use std::io::ErrorKind as IoKind;
let kind = match err.kind() {
IoKind::NotFound => ErrorKind::ResourceNotFound,
IoKind::PermissionDenied => ErrorKind::PermissionDenied,
IoKind::ConnectionRefused
| IoKind::ConnectionReset
| IoKind::ConnectionAborted
| IoKind::NotConnected
| IoKind::BrokenPipe => ErrorKind::Transport,
IoKind::TimedOut => ErrorKind::Timeout,
_ => ErrorKind::Internal,
};
Self::new(kind, alloc::format!("IO error: {}", err))
}
}
#[macro_export]
macro_rules! mcp_err {
($kind:expr, $msg:expr) => {
$crate::error::McpError::new($kind, $msg)
.with_source_location(concat!(file!(), ":", line!()))
};
($kind:expr, $fmt:expr, $($arg:tt)*) => {
$crate::error::McpError::new($kind, alloc::format!($fmt, $($arg)*))
.with_source_location(concat!(file!(), ":", line!()))
};
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
#[test]
fn test_error_creation() {
let err = McpError::invalid_params("missing field");
assert_eq!(err.kind, ErrorKind::InvalidParams);
assert!(err.message.contains("missing field"));
}
#[test]
fn test_error_context() {
let err = McpError::internal("test")
.with_operation("test_op")
.with_component("test_comp")
.with_request_id("req-123");
let ctx = err.context.unwrap();
assert_eq!(ctx.operation, Some("test_op".to_string()));
assert_eq!(ctx.component, Some("test_comp".to_string()));
assert_eq!(ctx.request_id, Some("req-123".to_string()));
}
#[test]
fn test_jsonrpc_codes() {
assert_eq!(McpError::tool_not_found("x").jsonrpc_code(), -32001);
assert_eq!(McpError::invalid_params("x").jsonrpc_code(), -32602);
assert_eq!(McpError::internal("x").jsonrpc_code(), -32603);
}
#[test]
fn test_retryable() {
assert!(McpError::timeout("x").is_retryable());
assert!(McpError::rate_limited("x").is_retryable());
assert!(!McpError::invalid_params("x").is_retryable());
}
#[test]
fn test_http_status() {
assert_eq!(McpError::tool_not_found("x").http_status(), 404);
assert_eq!(McpError::authentication("x").http_status(), 401);
assert_eq!(McpError::internal("x").http_status(), 500);
}
#[test]
fn test_error_size_reasonable() {
assert!(
core::mem::size_of::<McpError>() <= 128,
"McpError size: {} bytes (should be ≤128)",
core::mem::size_of::<McpError>()
);
}
#[test]
fn test_error_kind_from_i32() {
assert_eq!(ErrorKind::from_i32(-32001), ErrorKind::ToolNotFound);
assert_eq!(ErrorKind::from_i32(-32002), ErrorKind::ToolExecutionFailed);
assert_eq!(ErrorKind::from_i32(-32003), ErrorKind::PromptNotFound);
assert_eq!(ErrorKind::from_i32(-32004), ErrorKind::ResourceNotFound);
assert_eq!(ErrorKind::from_i32(-32005), ErrorKind::ResourceAccessDenied);
assert_eq!(
ErrorKind::from_i32(-32006),
ErrorKind::CapabilityNotSupported
);
assert_eq!(
ErrorKind::from_i32(-32007),
ErrorKind::ProtocolVersionMismatch
);
assert_eq!(ErrorKind::from_i32(-32008), ErrorKind::Authentication);
assert_eq!(ErrorKind::from_i32(-32009), ErrorKind::RateLimited);
assert_eq!(ErrorKind::from_i32(-32010), ErrorKind::ServerOverloaded);
assert_eq!(
ErrorKind::from_i32(-32042),
ErrorKind::CapabilityNotSupported
);
assert_eq!(ErrorKind::from_i32(-32600), ErrorKind::InvalidRequest);
assert_eq!(ErrorKind::from_i32(-32601), ErrorKind::MethodNotFound);
assert_eq!(ErrorKind::from_i32(-32602), ErrorKind::InvalidParams);
assert_eq!(ErrorKind::from_i32(-32603), ErrorKind::Internal);
assert_eq!(ErrorKind::from_i32(-32700), ErrorKind::ParseError);
assert_eq!(ErrorKind::from_i32(-99999), ErrorKind::Internal);
assert_eq!(ErrorKind::from_i32(0), ErrorKind::Internal);
}
}