use std::borrow::Cow;
use serde_json::{Value, json};
use crate::spec::{self, error_codes};
const MAX_ECHO_CHARS: usize = 128;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorKind {
BadRequest,
MethodNotFound,
Failed,
Internal,
Forbidden,
}
#[derive(Debug, Clone, PartialEq)]
pub struct McpError {
code: i64,
kind: ErrorKind,
message: Cow<'static, str>,
data: Option<Box<Value>>,
}
impl McpError {
fn new(code: i64, kind: ErrorKind, message: impl Into<Cow<'static, str>>) -> Self {
Self {
code,
kind,
message: message.into(),
data: None,
}
}
#[must_use]
pub fn parse_error() -> Self {
Self::new(
error_codes::PARSE_ERROR,
ErrorKind::BadRequest,
"Parse error",
)
}
#[must_use]
pub fn invalid_request(reason: &'static str) -> Self {
Self::new(
error_codes::INVALID_REQUEST_ERROR,
ErrorKind::BadRequest,
reason,
)
}
#[must_use]
pub fn method_not_found(method: &str) -> Self {
Self::new(
error_codes::METHOD_NOT_FOUND_ERROR,
ErrorKind::MethodNotFound,
format!("Method not found: {}", truncate(method)),
)
}
#[must_use]
pub fn method_not_found_explained(explanation: impl Into<Cow<'static, str>>) -> Self {
Self::new(
error_codes::METHOD_NOT_FOUND_ERROR,
ErrorKind::MethodNotFound,
explanation,
)
}
#[must_use]
pub fn invalid_params(message: impl Into<Cow<'static, str>>) -> Self {
Self::new(
error_codes::INVALID_PARAMS_ERROR,
ErrorKind::BadRequest,
message,
)
}
#[must_use]
pub fn unknown_tool(name: &str) -> Self {
Self::new(
error_codes::INVALID_PARAMS_ERROR,
ErrorKind::Failed,
format!("Unknown tool: {}", truncate(name)),
)
}
#[must_use]
pub fn request_failed(message: impl Into<Cow<'static, str>>) -> Self {
Self::new(
error_codes::INVALID_PARAMS_ERROR,
ErrorKind::Failed,
message,
)
}
#[must_use]
pub fn resource_not_found(uri: &str) -> Self {
let mut err = Self::new(
error_codes::INVALID_PARAMS_ERROR,
ErrorKind::Failed,
format!("Resource not found: {}", truncate(uri)),
);
err.data = Some(Box::new(json!({ "uri": uri })));
err
}
#[must_use]
pub fn unknown_prompt(name: &str) -> Self {
Self::new(
error_codes::INVALID_PARAMS_ERROR,
ErrorKind::Failed,
format!("Unknown prompt: {}", truncate(name)),
)
}
#[must_use]
pub fn internal_with_message(message: &'static str) -> Self {
Self::new(error_codes::INTERNAL_ERROR, ErrorKind::Internal, message)
}
#[must_use]
pub fn internal() -> Self {
Self::new(
error_codes::INTERNAL_ERROR,
ErrorKind::Internal,
"Internal error",
)
}
#[must_use]
pub fn insufficient_scope(required: &[String]) -> Self {
let mut err = Self::new(
error_codes::INVALID_REQUEST_ERROR,
ErrorKind::Forbidden,
"Insufficient scope",
);
err.data = Some(Box::new(json!({ "requiredScopes": required })));
err
}
#[cfg_attr(not(feature = "http"), allow(dead_code))]
pub(crate) fn required_scopes(&self) -> Vec<&str> {
self.data
.as_deref()
.and_then(|d| d.get("requiredScopes"))
.and_then(Value::as_array)
.map(|scopes| scopes.iter().filter_map(Value::as_str).collect())
.unwrap_or_default()
}
#[must_use]
pub fn header_mismatch(message: impl Into<Cow<'static, str>>) -> Self {
Self::new(
error_codes::HEADER_MISMATCH_ERROR,
ErrorKind::BadRequest,
message,
)
}
#[must_use]
pub fn missing_client_capability(required: &spec::ClientCapabilities) -> Self {
let mut err = Self::new(
error_codes::MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR,
ErrorKind::BadRequest,
"Missing required client capability",
);
err.data = Some(Box::new(json!({ "requiredCapabilities": required })));
err
}
#[must_use]
pub fn unsupported_protocol_version(requested: &str, supported: &[String]) -> Self {
let mut err = Self::new(
error_codes::UNSUPPORTED_PROTOCOL_VERSION_ERROR,
ErrorKind::BadRequest,
"Unsupported protocol version",
);
err.data = Some(Box::new(
json!({ "supported": supported, "requested": truncate(requested) }),
));
err
}
#[must_use]
pub(crate) fn with_code(mut self, code: i64) -> Self {
self.code = code;
self
}
#[must_use]
pub fn code(&self) -> i64 {
self.code
}
#[must_use]
pub fn kind(&self) -> ErrorKind {
self.kind
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
#[must_use]
pub fn data(&self) -> Option<&Value> {
self.data.as_deref()
}
#[must_use]
pub fn into_spec(self) -> spec::Error {
spec::Error {
code: self.code,
message: self.message.into_owned(),
data: self.data.map(|d| *d),
}
}
}
impl std::fmt::Display for McpError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", self.message, self.code)
}
}
impl std::error::Error for McpError {}
const MAX_MESSAGE_CHARS: usize = 512;
const MAX_QUOTED_CHARS: usize = 32;
pub(crate) fn sanitize(message: &str) -> String {
let mut out = String::with_capacity(message.len().min(MAX_MESSAGE_CHARS));
let mut in_quote = false;
let mut quoted = 0usize;
let mut escaped = false;
for c in message.chars() {
if in_quote {
if escaped {
escaped = false;
} else if c == '\\' {
escaped = true;
} else if c == '"' {
in_quote = false;
if quoted > MAX_QUOTED_CHARS {
out.push('…');
}
out.push('"');
continue;
}
quoted += 1;
if quoted <= MAX_QUOTED_CHARS {
out.push(c);
}
continue;
}
if c == '"' {
in_quote = true;
quoted = 0;
}
out.push(c);
if out.chars().count() >= MAX_MESSAGE_CHARS {
out.push('…');
break;
}
}
out
}
pub(crate) fn truncate(text: &str) -> Cow<'_, str> {
match text.char_indices().nth(MAX_ECHO_CHARS) {
Some((idx, _)) => Cow::Owned(format!("{}…", text.get(..idx).unwrap_or_default())),
None => Cow::Borrowed(text),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unsupported_version_carries_data() {
let err = McpError::unsupported_protocol_version("1900-01-01", &["2026-07-28".into()]);
assert_eq!(err.code(), -32022);
assert_eq!(
err.data(),
Some(&json!({ "supported": ["2026-07-28"], "requested": "1900-01-01" }))
);
}
#[test]
fn sanitize_shortens_quoted_input() {
let long = format!(
"invalid type: string \"{}\", expected f64 at line 1 column 9",
"A".repeat(10_000)
);
let clean = sanitize(&long);
assert!(clean.len() < 200, "{clean}");
assert!(clean.contains("expected f64"));
assert_eq!(sanitize("missing field `a`"), "missing field `a`");
assert!(sanitize(&"x".repeat(10_000)).chars().count() <= MAX_MESSAGE_CHARS + 1);
}
#[test]
fn echoed_text_is_truncated() {
let long = "x".repeat(1000);
let err = McpError::method_not_found(&long);
assert!(err.message().chars().count() < 200);
}
}