use serde::{Deserialize, Serialize};
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
#[non_exhaustive]
pub enum ErrorCode {
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
#[non_exhaustive]
pub enum McpErrorCode {
ConnectionClosed = -32000,
HeaderMismatch = -32001,
#[deprecated(
since = "0.12.0",
note = "SEP-2164 reassigned resource-not-found to InvalidParams (-32602). \
Use JsonRpcError::resource_not_found or ErrorCode::InvalidParams."
)]
ResourceNotFound = -32002,
MissingRequiredClientCapability = -32003,
UnsupportedProtocolVersion = -32004,
SessionNotFound = -32005,
SessionRequired = -32006,
Forbidden = -32007,
AlreadySubscribed = -32008,
NotSubscribed = -32009,
RequestTimeout = -32010,
UrlElicitationRequired = -32042,
}
impl McpErrorCode {
pub fn code(self) -> i32 {
self as i32
}
}
impl ErrorCode {
pub fn code(self) -> i32 {
self as i32
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonRpcError {
pub code: i32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
}
impl JsonRpcError {
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code: code.code(),
message: message.into(),
data: None,
}
}
pub fn with_data(mut self, data: serde_json::Value) -> Self {
self.data = Some(data);
self
}
pub fn parse_error(message: impl Into<String>) -> Self {
Self::new(ErrorCode::ParseError, message)
}
pub fn invalid_request(message: impl Into<String>) -> Self {
Self::new(ErrorCode::InvalidRequest, message)
}
pub fn method_not_found(method: &str) -> Self {
Self::new(
ErrorCode::MethodNotFound,
format!("Method not found: {}", method),
)
}
pub fn invalid_params(message: impl Into<String>) -> Self {
Self::new(ErrorCode::InvalidParams, message)
}
pub fn internal_error(message: impl Into<String>) -> Self {
Self::new(ErrorCode::InternalError, message)
}
pub fn mcp_error(code: McpErrorCode, message: impl Into<String>) -> Self {
Self {
code: code.code(),
message: message.into(),
data: None,
}
}
pub fn connection_closed(message: impl Into<String>) -> Self {
Self::mcp_error(McpErrorCode::ConnectionClosed, message)
}
pub fn request_timeout(message: impl Into<String>) -> Self {
Self::mcp_error(McpErrorCode::RequestTimeout, message)
}
pub fn resource_not_found(uri: &str) -> Self {
Self::new(
ErrorCode::InvalidParams,
format!("Resource not found: {}", uri),
)
}
pub fn already_subscribed(uri: &str) -> Self {
Self::mcp_error(
McpErrorCode::AlreadySubscribed,
format!("Already subscribed to: {}", uri),
)
}
pub fn not_subscribed(uri: &str) -> Self {
Self::mcp_error(
McpErrorCode::NotSubscribed,
format!("Not subscribed to: {}", uri),
)
}
pub fn session_not_found() -> Self {
Self::mcp_error(
McpErrorCode::SessionNotFound,
"Session not found or expired. Please re-initialize the connection.",
)
}
pub fn session_not_found_with_id(session_id: &str) -> Self {
Self::mcp_error(
McpErrorCode::SessionNotFound,
format!(
"Session '{}' not found or expired. Please re-initialize the connection.",
session_id
),
)
}
pub fn session_required() -> Self {
Self::mcp_error(
McpErrorCode::SessionRequired,
"MCP-Session-Id header is required for this request.",
)
}
pub fn forbidden(message: impl Into<String>) -> Self {
Self::mcp_error(McpErrorCode::Forbidden, message)
}
pub fn url_elicitation_required(message: impl Into<String>) -> Self {
Self::mcp_error(McpErrorCode::UrlElicitationRequired, message)
}
pub fn header_mismatch(message: impl Into<String>) -> Self {
Self::mcp_error(McpErrorCode::HeaderMismatch, message)
}
pub fn unsupported_protocol_version(
requested: impl Into<String>,
supported: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
let data = UnsupportedProtocolVersionData {
supported: supported.into_iter().map(Into::into).collect(),
requested: requested.into(),
};
Self {
code: McpErrorCode::UnsupportedProtocolVersion.code(),
message: format!(
"Unsupported protocol version: {} (supported: {})",
data.requested,
data.supported.join(", "),
),
data: Some(
serde_json::to_value(&data)
.expect("UnsupportedProtocolVersionData is serializable"),
),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnsupportedProtocolVersionData {
pub supported: Vec<String>,
pub requested: String,
}
#[derive(Debug)]
pub struct ToolError {
pub tool: Option<String>,
pub message: String,
pub source: Option<BoxError>,
}
impl std::fmt::Display for ToolError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(tool) = &self.tool {
write!(f, "Tool '{}' error: {}", tool, self.message)
} else {
write!(f, "Tool error: {}", self.message)
}
}
}
impl std::error::Error for ToolError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_ref()
.map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
}
}
impl ToolError {
pub fn new(message: impl Into<String>) -> Self {
Self {
tool: None,
message: message.into(),
source: None,
}
}
pub fn with_tool(tool: impl Into<String>, message: impl Into<String>) -> Self {
Self {
tool: Some(tool.into()),
message: message.into(),
source: None,
}
}
pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
self.source = Some(Box::new(source));
self
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("JSON-RPC error: {0:?}")]
JsonRpc(JsonRpcError),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("{0}")]
Tool(#[from] ToolError),
#[error("Transport error: {0}")]
Transport(String),
#[error("Session expired")]
SessionExpired,
#[error("Internal error: {0}")]
Internal(String),
}
impl Error {
pub fn tool(message: impl Into<String>) -> Self {
Error::Tool(ToolError::new(message))
}
pub fn tool_with_name(tool: impl Into<String>, message: impl Into<String>) -> Self {
Error::Tool(ToolError::with_tool(tool, message))
}
pub fn tool_from<E: std::fmt::Display>(err: E) -> Self {
Error::Tool(ToolError::new(err.to_string()))
}
pub fn tool_context<E: std::fmt::Display>(context: impl Into<String>, err: E) -> Self {
Error::Tool(ToolError::new(format!("{}: {}", context.into(), err)))
}
pub fn invalid_params(message: impl Into<String>) -> Self {
Error::JsonRpc(JsonRpcError::invalid_params(message))
}
pub fn internal(message: impl Into<String>) -> Self {
Error::JsonRpc(JsonRpcError::internal_error(message))
}
}
pub trait ResultExt<T> {
fn tool_err(self) -> std::result::Result<T, Error>;
fn tool_context(self, context: impl Into<String>) -> std::result::Result<T, Error>;
}
impl<T, E: std::fmt::Display> ResultExt<T> for std::result::Result<T, E> {
fn tool_err(self) -> std::result::Result<T, Error> {
self.map_err(Error::tool_from)
}
fn tool_context(self, context: impl Into<String>) -> std::result::Result<T, Error> {
self.map_err(|e| Error::tool_context(context, e))
}
}
impl From<JsonRpcError> for Error {
fn from(err: JsonRpcError) -> Self {
Error::JsonRpc(err)
}
}
impl From<std::convert::Infallible> for Error {
fn from(err: std::convert::Infallible) -> Self {
match err {}
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unsupported_protocol_version_code_is_negative_32004() {
assert_eq!(McpErrorCode::UnsupportedProtocolVersion.code(), -32004);
}
#[test]
fn unsupported_protocol_version_constructor_has_spec_shape() {
let err =
JsonRpcError::unsupported_protocol_version("2027-01-01", ["2026-07-28", "2025-11-25"]);
assert_eq!(err.code, -32004);
let data = err.data.expect("data must be present");
assert_eq!(
data["supported"],
serde_json::json!(["2026-07-28", "2025-11-25"])
);
assert_eq!(data["requested"], "2027-01-01");
assert!(
data.get("supportedVersions").is_none(),
"spec field name is 'supported', not 'supportedVersions'"
);
}
#[test]
fn unsupported_protocol_version_data_round_trip() {
let original = UnsupportedProtocolVersionData {
supported: vec!["2026-07-28".into(), "2025-11-25".into()],
requested: "2027-01-01".into(),
};
let json = serde_json::to_value(&original).unwrap();
let parsed: UnsupportedProtocolVersionData = serde_json::from_value(json.clone()).unwrap();
assert_eq!(parsed.supported, original.supported);
assert_eq!(parsed.requested, original.requested);
}
#[test]
fn resource_not_found_constructor_uses_invalid_params() {
let err = JsonRpcError::resource_not_found("file:///gone.txt");
assert_eq!(err.code, ErrorCode::InvalidParams.code());
assert_eq!(err.code, -32602);
assert!(err.message.contains("file:///gone.txt"));
}
#[test]
fn resource_not_found_serializes_with_spec_code() {
let err = JsonRpcError::resource_not_found("urn:test:x");
let json = serde_json::to_value(&err).unwrap();
assert_eq!(json["code"], -32602);
}
#[test]
fn subscribe_codes_moved_off_spec_assignments() {
assert_eq!(McpErrorCode::AlreadySubscribed.code(), -32008);
assert_eq!(McpErrorCode::NotSubscribed.code(), -32009);
assert_eq!(McpErrorCode::MissingRequiredClientCapability.code(), -32003);
assert_eq!(McpErrorCode::UnsupportedProtocolVersion.code(), -32004);
}
#[test]
fn header_mismatch_code_is_negative_32001() {
assert_eq!(McpErrorCode::HeaderMismatch.code(), -32001);
}
#[test]
fn header_mismatch_constructor_uses_spec_code() {
let err = JsonRpcError::header_mismatch(
"Mcp-Name header value 'foo' does not match body value 'bar'",
);
assert_eq!(err.code, -32001);
assert_eq!(err.code, McpErrorCode::HeaderMismatch.code());
assert!(err.message.contains("Mcp-Name"));
}
#[test]
fn request_timeout_moved_off_spec_assignment() {
assert_eq!(McpErrorCode::RequestTimeout.code(), -32010);
assert_eq!(McpErrorCode::HeaderMismatch.code(), -32001);
}
#[test]
#[allow(deprecated)] fn no_two_mcp_codes_share_a_value() {
let all = [
McpErrorCode::ConnectionClosed.code(),
McpErrorCode::HeaderMismatch.code(),
McpErrorCode::RequestTimeout.code(),
McpErrorCode::ResourceNotFound.code(),
McpErrorCode::MissingRequiredClientCapability.code(),
McpErrorCode::UnsupportedProtocolVersion.code(),
McpErrorCode::SessionNotFound.code(),
McpErrorCode::SessionRequired.code(),
McpErrorCode::Forbidden.code(),
McpErrorCode::AlreadySubscribed.code(),
McpErrorCode::NotSubscribed.code(),
McpErrorCode::UrlElicitationRequired.code(),
];
let mut sorted = all.to_vec();
sorted.sort_unstable();
let original_len = sorted.len();
sorted.dedup();
assert_eq!(
sorted.len(),
original_len,
"collision in McpErrorCode values: {:?}",
all
);
}
#[test]
fn test_box_error_from_io_error() {
let io_err = std::io::Error::other("disk full");
let boxed: BoxError = io_err.into();
assert_eq!(boxed.to_string(), "disk full");
}
#[test]
fn test_box_error_from_string() {
let err: BoxError = "something went wrong".into();
assert_eq!(err.to_string(), "something went wrong");
}
#[test]
fn test_box_error_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<BoxError>();
}
#[test]
fn test_tool_error_source_uses_box_error() {
let io_err = std::io::Error::other("timeout");
let tool_err = ToolError::new("failed").with_source(io_err);
assert!(tool_err.source.is_some());
assert_eq!(tool_err.source.unwrap().to_string(), "timeout");
}
#[test]
fn test_result_ext_tool_err() {
let result: std::result::Result<(), std::io::Error> =
Err(std::io::Error::other("disk full"));
let err = result.tool_err().unwrap_err();
assert!(matches!(err, Error::Tool(_)));
assert!(err.to_string().contains("disk full"));
}
#[test]
fn test_result_ext_tool_context() {
let result: std::result::Result<(), std::io::Error> =
Err(std::io::Error::other("connection refused"));
let err = result.tool_context("database query failed").unwrap_err();
assert!(matches!(err, Error::Tool(_)));
assert!(err.to_string().contains("database query failed"));
assert!(err.to_string().contains("connection refused"));
}
#[test]
fn test_result_ext_ok_passes_through() {
let result: std::result::Result<i32, std::io::Error> = Ok(42);
assert_eq!(result.tool_err().unwrap(), 42);
let result: std::result::Result<i32, std::io::Error> = Ok(42);
assert_eq!(result.tool_context("should not appear").unwrap(), 42);
}
}