pub mod codes;
mod context;
mod details;
mod jsonrpc;
mod transport;
mod types;
pub use codes::*;
pub use context::McpResultExt;
pub use details::{
BoxError, HandshakeDetails, InvalidParamsDetails, ToolExecutionDetails, TransportDetails,
};
pub use jsonrpc::JsonRpcError;
pub use transport::{TransportContext, TransportErrorKind};
pub use types::McpError;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_size_is_small() {
let size = std::mem::size_of::<McpError>();
assert!(
size <= 64,
"McpError is {size} bytes, should be <= 64 bytes. Consider boxing more variants."
);
let result_size = std::mem::size_of::<Result<(), McpError>>();
assert!(
result_size <= 72,
"Result<(), McpError> is {result_size} bytes, should be <= 72 bytes."
);
}
#[test]
fn test_error_codes() {
assert_eq!(McpError::parse("test").code(), PARSE_ERROR);
assert_eq!(McpError::invalid_request("test").code(), INVALID_REQUEST);
assert_eq!(McpError::method_not_found("test").code(), METHOD_NOT_FOUND);
assert_eq!(McpError::invalid_params("m", "test").code(), INVALID_PARAMS);
assert_eq!(McpError::internal("test").code(), INTERNAL_ERROR);
assert_eq!(
McpError::transport(TransportErrorKind::ConnectionFailed, "test").code(),
SERVER_ERROR_START
);
assert_eq!(
McpError::tool_error("tool", "test").code(),
SERVER_ERROR_START - 1
);
assert_eq!(
McpError::handshake_failed("test").code(),
SERVER_ERROR_START - 5
);
}
#[test]
fn test_context_chaining() {
fn inner() -> Result<(), McpError> {
Err(McpError::resource_not_found("test://resource"))
}
fn outer() -> Result<(), McpError> {
inner().context("Failed in outer")?;
Ok(())
}
let err = outer().unwrap_err();
assert!(err.to_string().contains("Failed in outer"));
assert_eq!(err.code(), RESOURCE_NOT_FOUND);
}
#[test]
fn test_json_rpc_error_conversion() {
let err = McpError::method_not_found_with_suggestions(
"unknown_method",
vec!["tools/list".to_string(), "resources/list".to_string()],
);
let json_err: JsonRpcError = (&err).into();
assert_eq!(json_err.code, METHOD_NOT_FOUND);
assert!(json_err.message.contains("unknown_method"));
assert!(json_err.data.is_some());
}
#[test]
fn test_json_rpc_error_conversion_boxed_variants() {
let err = McpError::invalid_params_detailed(
"test_method",
"invalid value",
Some("args.count".to_string()),
Some("number".to_string()),
Some("string".to_string()),
);
let json_err: JsonRpcError = (&err).into();
assert_eq!(json_err.code, INVALID_PARAMS);
let data = json_err.data.unwrap();
assert_eq!(data["method"], "test_method");
assert_eq!(data["param_path"], "args.count");
let err = McpError::transport_with_context(
TransportErrorKind::ConnectionFailed,
"connection refused",
TransportContext::new("websocket").with_remote_addr("ws://localhost:8080"),
);
let json_err: JsonRpcError = (&err).into();
assert_eq!(json_err.code, SERVER_ERROR_START);
assert!(json_err.data.is_some());
let err = McpError::tool_error_detailed(
"calculator",
"division by zero",
true,
Some(serde_json::json!({"operation": "divide"})),
);
let json_err: JsonRpcError = (&err).into();
assert!(json_err.data.is_some());
let data = json_err.data.unwrap();
assert_eq!(data["operation"], "divide");
let err = McpError::handshake_failed_with_versions(
"version mismatch",
Some("2024-11-05".to_string()),
Some("2025-11-25".to_string()),
);
let json_err: JsonRpcError = (&err).into();
assert!(json_err.data.is_some());
let data = json_err.data.unwrap();
assert_eq!(data["client_version"], "2024-11-05");
assert_eq!(data["server_version"], "2025-11-25");
}
#[test]
fn test_recoverable_errors() {
assert!(McpError::invalid_params("m", "test").is_recoverable());
assert!(McpError::resource_not_found("uri").is_recoverable());
assert!(!McpError::internal("test").is_recoverable());
let recoverable_tool = McpError::tool_error_detailed("tool", "error", true, None);
assert!(recoverable_tool.is_recoverable());
let non_recoverable_tool = McpError::tool_error_detailed("tool", "error", false, None);
assert!(!non_recoverable_tool.is_recoverable());
}
#[test]
fn test_boxed_error_display() {
let err = McpError::invalid_params("method", "bad params");
assert!(err.to_string().contains("method"));
assert!(err.to_string().contains("bad params"));
let err = McpError::transport(TransportErrorKind::Timeout, "connection timed out");
assert!(err.to_string().contains("timeout"));
assert!(err.to_string().contains("connection timed out"));
let err = McpError::tool_error("my_tool", "tool failed");
assert!(err.to_string().contains("my_tool"));
assert!(err.to_string().contains("tool failed"));
let err = McpError::handshake_failed("protocol mismatch");
assert!(err.to_string().contains("protocol mismatch"));
}
#[test]
fn test_io_error_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
let mcp_err: McpError = io_err.into();
if let McpError::Transport(details) = mcp_err {
assert_eq!(details.kind, TransportErrorKind::ConnectionFailed);
assert!(details.message.contains("refused"));
} else {
panic!("Expected Transport error variant");
}
}
}