use std::time::Duration;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum McpError {
#[error("Transport error: {0}")]
Transport(#[from] TransportError),
#[error("Protocol error: {0}")]
Protocol(#[from] ProtocolError),
#[error("Validation error: {0}")]
Validation(#[from] ValidationError),
#[error("Authentication error: {0}")]
Auth(#[from] AuthError),
#[error("Operation timed out after {duration_ms}ms: {operation}")]
Timeout {
operation: String,
duration_ms: u64,
},
#[error("Configuration error: {0}")]
Config(#[from] ConfigError),
#[error("Serialization error: {source}")]
Serialization {
#[from]
source: serde_json::Error,
},
#[error("IO error: {source}")]
Io {
#[from]
source: std::io::Error,
},
#[error("Internal error: {message}")]
Internal {
message: String,
},
}
#[derive(Error, Debug, Clone)]
#[allow(missing_docs)]
pub enum TransportError {
#[error("Failed to connect to {transport_type} server: {reason}")]
ConnectionFailed {
transport_type: String,
reason: String,
},
#[error("Connection lost to {transport_type} server: {reason}")]
ConnectionLost {
transport_type: String,
reason: String,
},
#[error("Failed to send message via {transport_type}: {reason}")]
SendFailed {
transport_type: String,
reason: String,
},
#[error("Failed to receive message via {transport_type}: {reason}")]
ReceiveFailed {
transport_type: String,
reason: String,
},
#[error("Invalid {transport_type} configuration: {reason}")]
InvalidConfig {
transport_type: String,
reason: String,
},
#[error("Process error: {reason}")]
ProcessError { reason: String },
#[error("HTTP error: {status_code} - {reason}")]
HttpError { status_code: u16, reason: String },
#[error("SSE error: {reason}")]
SseError { reason: String },
#[error("Streaming error: {reason}")]
StreamingError { reason: String },
#[error("Transport not connected ({transport_type}): {reason}")]
NotConnected {
transport_type: String,
reason: String,
},
#[error("Network error ({transport_type}): {reason}")]
NetworkError {
transport_type: String,
reason: String,
},
#[error("Serialization error ({transport_type}): {reason}")]
SerializationError {
transport_type: String,
reason: String,
},
#[error("Operation timed out ({transport_type}): {reason}")]
TimeoutError {
transport_type: String,
reason: String,
},
#[error("Transport disconnected ({transport_type}): {reason}")]
DisconnectedError {
transport_type: String,
reason: String,
},
#[error("Connection error ({transport_type}): {reason}")]
ConnectionError {
transport_type: String,
reason: String,
},
}
#[derive(Error, Debug, Clone)]
#[allow(missing_docs)]
pub enum ProtocolError {
#[error("Invalid JSON-RPC message: {reason}")]
InvalidJsonRpc { reason: String },
#[error("Unsupported protocol version: {version}, supported versions: {supported:?}")]
UnsupportedVersion {
version: String,
supported: Vec<String>,
},
#[error("Message ID mismatch: expected {expected}, got {actual}")]
MessageIdMismatch { expected: String, actual: String },
#[error("Unexpected message type: expected {expected}, got {actual}")]
UnexpectedMessageType { expected: String, actual: String },
#[error("Missing required field '{field}' in {message_type}")]
MissingField { field: String, message_type: String },
#[error("Invalid method name: {method}")]
InvalidMethod { method: String },
#[error("Server error {code}: {message}")]
ServerError { code: i32, message: String },
#[error("Protocol state violation: {reason}")]
StateViolation { reason: String },
#[error("Protocol initialization failed: {reason}")]
InitializationFailed { reason: String },
#[error("Protocol not initialized: {reason}")]
NotInitialized { reason: String },
#[error("Invalid response: {reason}")]
InvalidResponse { reason: String },
#[error("Protocol configuration error: {reason}")]
InvalidConfig { reason: String },
#[error("Protocol operation '{operation}' timed out after {timeout:?}")]
TimeoutError {
operation: String,
timeout: std::time::Duration,
},
#[error("Request failed: {reason}")]
RequestFailed { reason: String },
#[error("Request timed out after {timeout:?}")]
RequestTimeout { timeout: Duration },
}
#[derive(Error, Debug, Clone)]
#[allow(missing_docs)]
pub enum ValidationError {
#[error("Schema validation failed for {object_type}: {reason}")]
SchemaValidation { object_type: String, reason: String },
#[error("Capability '{capability}' not supported by server")]
UnsupportedCapability { capability: String },
#[error("Invalid parameter '{parameter}' for tool '{tool}': {reason}")]
InvalidToolParameter {
tool: String,
parameter: String,
reason: String,
},
#[error("Invalid resource '{resource}': {reason}")]
InvalidResource { resource: String, reason: String },
#[error("Invalid prompt '{prompt}': {reason}")]
InvalidPrompt { prompt: String, reason: String },
#[error("Constraint violation: {constraint} - {reason}")]
ConstraintViolation { constraint: String, reason: String },
}
#[derive(Error, Debug, Clone)]
#[allow(missing_docs)]
pub enum AuthError {
#[error("Missing authentication credentials for {auth_type}")]
MissingCredentials { auth_type: String },
#[error("Invalid {auth_type} credentials: {reason}")]
InvalidCredentials { auth_type: String, reason: String },
#[error("Authentication expired for {auth_type}")]
Expired { auth_type: String },
#[error("Access denied: {reason}")]
AccessDenied { reason: String },
#[error("OAuth error: {error_code} - {description}")]
OAuth {
error_code: String,
description: String,
},
#[error("JWT error: {reason}")]
Jwt { reason: String },
}
#[derive(Error, Debug, Clone)]
#[allow(missing_docs)]
pub enum ConfigError {
#[error("Configuration file not found: {path}")]
FileNotFound { path: String },
#[error("Invalid configuration format in {path}: {reason}")]
InvalidFormat { path: String, reason: String },
#[error("Missing required configuration parameter: {parameter}")]
MissingParameter { parameter: String },
#[error("Invalid value for parameter '{parameter}': {value} - {reason}")]
InvalidValue {
parameter: String,
value: String,
reason: String,
},
#[error("Conflicting configuration: {reason}")]
Conflict { reason: String },
}
pub type McpResult<T> = Result<T, McpError>;
impl McpError {
pub fn internal(message: impl Into<String>) -> Self {
Self::Internal {
message: message.into(),
}
}
pub fn timeout(operation: impl Into<String>, duration: std::time::Duration) -> Self {
Self::Timeout {
operation: operation.into(),
duration_ms: duration.as_millis() as u64,
}
}
pub fn is_retryable(&self) -> bool {
match self {
McpError::Transport(transport_err) => transport_err.is_retryable(),
McpError::Timeout { .. } => true,
McpError::Io { .. } => true,
McpError::Auth(_) => false,
McpError::Protocol(_) => false,
McpError::Validation(_) => false,
McpError::Config(_) => false,
McpError::Serialization { .. } => false,
McpError::Internal { .. } => false,
}
}
pub fn category(&self) -> &'static str {
match self {
McpError::Transport(_) => "transport",
McpError::Protocol(_) => "protocol",
McpError::Validation(_) => "validation",
McpError::Auth(_) => "auth",
McpError::Timeout { .. } => "timeout",
McpError::Config(_) => "config",
McpError::Serialization { .. } => "serialization",
McpError::Io { .. } => "io",
McpError::Internal { .. } => "internal",
}
}
}
impl TransportError {
pub fn is_retryable(&self) -> bool {
match self {
TransportError::ConnectionFailed { .. } => true,
TransportError::ConnectionLost { .. } => true,
TransportError::ConnectionError { .. } => true,
TransportError::SendFailed { .. } => true,
TransportError::ReceiveFailed { .. } => true,
TransportError::NetworkError { .. } => true,
TransportError::TimeoutError { .. } => true,
TransportError::DisconnectedError { .. } => true,
TransportError::HttpError { status_code, .. } => {
*status_code >= 500
}
TransportError::SseError { .. } => true,
TransportError::StreamingError { .. } => true,
TransportError::ProcessError { .. } => false,
TransportError::InvalidConfig { .. } => false,
TransportError::NotConnected { .. } => false,
TransportError::SerializationError { .. } => false,
}
}
}
impl From<reqwest::Error> for McpError {
fn from(err: reqwest::Error) -> Self {
if err.is_timeout() {
McpError::timeout("HTTP request", std::time::Duration::from_secs(30))
} else if err.is_connect() {
McpError::Transport(TransportError::ConnectionFailed {
transport_type: "http".to_string(),
reason: err.to_string(),
})
} else if let Some(status) = err.status() {
McpError::Transport(TransportError::HttpError {
status_code: status.as_u16(),
reason: err.to_string(),
})
} else {
McpError::Transport(TransportError::HttpError {
status_code: 0,
reason: err.to_string(),
})
}
}
}
impl From<url::ParseError> for McpError {
fn from(err: url::ParseError) -> Self {
McpError::Config(ConfigError::InvalidValue {
parameter: "url".to_string(),
value: err.to_string(),
reason: "Invalid URL format".to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_error_display() {
let error = McpError::timeout("test operation", Duration::from_secs(30));
assert_eq!(
error.to_string(),
"Operation timed out after 30000ms: test operation"
);
}
#[test]
fn test_retryable_errors() {
let timeout = McpError::timeout("test", Duration::from_secs(30));
assert!(timeout.is_retryable());
let auth_error = McpError::Auth(AuthError::InvalidCredentials {
auth_type: "Bearer".to_string(),
reason: "Invalid token".to_string(),
});
assert!(!auth_error.is_retryable());
}
#[test]
fn test_error_categories() {
let timeout = McpError::timeout("test", Duration::from_secs(30));
assert_eq!(timeout.category(), "timeout");
let transport_error = McpError::Transport(TransportError::ConnectionFailed {
transport_type: "stdio".to_string(),
reason: "Process failed".to_string(),
});
assert_eq!(transport_error.category(), "transport");
}
#[test]
fn test_transport_error_retryable() {
let connection_failed = TransportError::ConnectionFailed {
transport_type: "stdio".to_string(),
reason: "Process failed".to_string(),
};
assert!(connection_failed.is_retryable());
let invalid_config = TransportError::InvalidConfig {
transport_type: "stdio".to_string(),
reason: "Missing command".to_string(),
};
assert!(!invalid_config.is_retryable());
}
}