use std::fmt;
use serde::Serialize;
#[derive(Debug)]
pub(crate) enum McpError {
CapabilityRefused { capability: &'static str },
InvalidArgument { message: &'static str },
ArgumentTooLarge {
field: &'static str,
limit: usize,
observed: usize,
},
UnknownMethod(String),
UnknownTool(String),
Schema(crate::metadata::SchemaError),
Serialize(serde_json::Error),
}
impl McpError {
pub(crate) fn code(&self) -> i64 {
match self {
Self::CapabilityRefused { .. } => -32010,
Self::InvalidArgument { .. } => -32011,
Self::ArgumentTooLarge { .. } => -32012,
Self::UnknownMethod(_) => -32601,
Self::UnknownTool(_) => -32602,
Self::Schema(_) => -32020,
Self::Serialize(_) => -32603,
}
}
pub(crate) fn message(&self) -> String {
match self {
Self::CapabilityRefused { capability } => {
format!("capability `{capability}` is not enabled on this server")
}
Self::InvalidArgument { message } => format!("invalid tool argument: {message}"),
Self::ArgumentTooLarge {
field,
limit,
observed,
} => {
format!("tool argument `{field}` is too large (limit {limit}, observed {observed})")
}
Self::UnknownMethod(method) => format!("unknown JSON-RPC method: {method}"),
Self::UnknownTool(name) => format!("unknown tool: {name}"),
Self::Schema(_) => "cannot load the application graph".to_owned(),
Self::Serialize(_) => "cannot serialize the MCP response".to_owned(),
}
}
}
impl fmt::Display for McpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message())
}
}
impl std::error::Error for McpError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Schema(err) => Some(err),
Self::Serialize(err) => Some(err),
Self::CapabilityRefused { .. }
| Self::InvalidArgument { .. }
| Self::ArgumentTooLarge { .. }
| Self::UnknownMethod(_)
| Self::UnknownTool(_) => None,
}
}
}
impl From<crate::metadata::SchemaError> for McpError {
fn from(value: crate::metadata::SchemaError) -> Self {
Self::Schema(value)
}
}
impl From<serde_json::Error> for McpError {
fn from(value: serde_json::Error) -> Self {
Self::Serialize(value)
}
}
#[derive(Debug, Serialize)]
pub(crate) struct RpcError {
pub(crate) code: i64,
pub(crate) message: String,
}
impl RpcError {
pub(crate) fn from_mcp(error: &McpError) -> Self {
Self {
code: error.code(),
message: error.message(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capability_refused_carries_capability_name() {
let err = McpError::CapabilityRefused {
capability: "DestructiveWrite",
};
assert_eq!(err.code(), -32010);
assert!(
err.message().contains("DestructiveWrite"),
"{}",
err.message()
);
}
#[test]
fn shell_capability_is_refused_not_special_cased() {
let err = McpError::CapabilityRefused {
capability: "Shell",
};
assert_eq!(err.code(), -32010);
assert!(err.message().contains("Shell"));
}
#[test]
fn argument_too_large_reports_limit_and_observed() {
let err = McpError::ArgumentTooLarge {
field: "route_name",
limit: 256,
observed: 100_000,
};
let msg = err.message();
assert!(msg.contains("256"), "{msg}");
assert!(msg.contains("100000"), "{msg}");
}
#[test]
fn unknown_method_and_tool_carry_the_name() {
let m = McpError::UnknownMethod("foo/bar".to_owned());
assert!(m.message().contains("foo/bar"));
let t = McpError::UnknownTool("nope".to_owned());
assert!(t.message().contains("nope"));
}
#[test]
fn schema_error_message_is_redacted() {
let schema = crate::metadata::SchemaError::IncompatibleSchema {
found: 99,
expected: 1,
};
let err = McpError::from(schema);
assert_eq!(err.message(), "cannot load the application graph");
assert!(std::error::Error::source(&err).is_some());
}
#[test]
fn error_category_is_reused_from_observe() {
use arcature_observe::redact::ErrorCategory;
let _ = ErrorCategory::Backend;
let _ = ErrorCategory::Auth;
assert_eq!(ErrorCategory::Backend.to_string(), "backend");
}
#[test]
fn rpc_error_serializes_code_and_message() {
let err = McpError::UnknownTool("x".to_owned());
let rpc = RpcError::from_mcp(&err);
let json = serde_json::to_string(&rpc).expect("serialize");
assert!(json.contains("\"code\":-32602"));
assert!(json.contains("unknown tool: x"));
}
}