use rmcp::model::{ErrorCode, ErrorData};
use schemars::JsonSchema;
use serde::Serialize;
use serde_json::{Value, json};
use std::fmt;
use thiserror::Error;
fn find_similar_strings(unknown: &str, known_strings: &[&str]) -> Vec<String> {
use strsim::jaro;
let mut candidates = Vec::new();
for string in known_strings {
let confidence = jaro(unknown, string);
if confidence > 0.7 {
candidates.push((confidence, string.to_string()));
}
}
candidates.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
candidates.into_iter().map(|(_, name)| name).collect()
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum ValidationConstraint {
Minimum {
value: f64,
exclusive: bool,
},
Maximum {
value: f64,
exclusive: bool,
},
MinLength {
value: usize,
},
MaxLength {
value: usize,
},
Pattern {
pattern: String,
},
EnumValues {
values: Vec<Value>,
},
Format {
format: String,
},
MultipleOf {
value: f64,
},
MinItems {
value: usize,
},
MaxItems {
value: usize,
},
UniqueItems,
MinProperties {
value: usize,
},
MaxProperties {
value: usize,
},
ConstValue {
value: Value,
},
Required {
properties: Vec<String>,
},
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum ValidationError {
InvalidParameter {
parameter: String,
suggestions: Vec<String>,
valid_parameters: Vec<String>,
},
MissingRequiredParameter {
parameter: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
expected_type: String,
},
ConstraintViolation {
parameter: String,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
field_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
actual_value: Option<Box<Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
expected_type: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
constraints: Vec<ValidationConstraint>,
},
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ValidationError::InvalidParameter {
parameter,
suggestions,
..
} => {
if suggestions.is_empty() {
write!(f, "'{parameter}'")
} else {
write!(f, "'{parameter}' (suggestions: {})", suggestions.join(", "))
}
}
ValidationError::MissingRequiredParameter {
parameter,
expected_type,
..
} => {
write!(f, "'{parameter}' is required (expected: {expected_type})")
}
ValidationError::ConstraintViolation {
parameter, message, ..
} => {
write!(f, "'{parameter}': {message}")
}
}
}
}
fn format_validation_errors(violations: &[ValidationError]) -> String {
match violations.len() {
0 => "Validation failed".to_string(),
1 => {
let error = &violations[0];
match error {
ValidationError::InvalidParameter { .. } => {
format!("Validation failed - invalid parameter {error}")
}
ValidationError::MissingRequiredParameter { .. } => {
format!("Validation failed - missing required parameter: {error}")
}
ValidationError::ConstraintViolation { .. } => {
format!("Validation failed - parameter {error}")
}
}
}
_ => {
let mut invalid_params = Vec::new();
let mut missing_params = Vec::new();
let mut constraint_violations = Vec::new();
for error in violations {
match error {
ValidationError::InvalidParameter { .. } => {
invalid_params.push(error.to_string());
}
ValidationError::MissingRequiredParameter { .. } => {
missing_params.push(error.to_string());
}
ValidationError::ConstraintViolation { .. } => {
constraint_violations.push(error.to_string());
}
}
}
let mut parts = Vec::new();
if !invalid_params.is_empty() {
let params_str = invalid_params.join(", ");
parts.push(format!("invalid parameters: {params_str}"));
}
if !missing_params.is_empty() {
let params_str = missing_params.join(", ");
parts.push(format!("missing parameters: {params_str}"));
}
if !constraint_violations.is_empty() {
let violations_str = constraint_violations.join("; ");
parts.push(format!("constraint violations: {violations_str}"));
}
format!("Validation failed - {}", parts.join("; "))
}
}
}
#[derive(Debug, Error)]
pub enum CliError {
#[error("Invalid header format in '{header}': expected 'name: value' format")]
InvalidHeaderFormat { header: String },
#[error("Invalid header name in '{header}': {source}")]
InvalidHeaderName {
header: String,
#[source]
source: http::header::InvalidHeaderName,
},
#[error("Invalid header value in '{header}': {source}")]
InvalidHeaderValue {
header: String,
#[source]
source: http::header::InvalidHeaderValue,
},
}
#[derive(Debug, Error)]
pub enum Error {
#[error("CLI error: {0}")]
Cli(#[from] CliError),
#[error("Environment variable error: {0}")]
EnvVar(#[from] std::env::VarError),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("OpenAPI spec error: {0}")]
Spec(String),
#[error("Tool generation error: {0}")]
ToolGeneration(String),
#[error("Invalid parameter location: {0}")]
InvalidParameterLocation(String),
#[error("Invalid URL: {0}")]
InvalidUrl(String),
#[error("File not found: {0}")]
FileNotFound(String),
#[error("MCP error: {0}")]
McpError(String),
#[error("Invalid path: {0}")]
InvalidPath(String),
#[error("Validation error: {0}")]
Validation(String),
#[error("HTTP error: {0}")]
Http(String),
#[error("HTTP request error: {0}")]
HttpRequest(#[from] reqwest::Error),
#[error("JSON error at {path}: {source}")]
JsonAtPath {
path: String,
source: serde_json::Error,
},
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error(transparent)]
ToolCall(#[from] ToolCallError),
#[error("Tool not found: {0}")]
ToolNotFound(String),
}
impl From<ToolCallValidationError> for ErrorData {
fn from(err: ToolCallValidationError) -> Self {
match err {
ToolCallValidationError::ToolNotFound {
ref tool_name,
ref suggestions,
} => {
let data = if suggestions.is_empty() {
None
} else {
Some(json!({
"suggestions": suggestions
}))
};
ErrorData::new(
ErrorCode(-32601),
format!("Tool '{tool_name}' not found"),
data,
)
}
ToolCallValidationError::InvalidParameters { ref violations } => {
let data = Some(json!({
"type": "validation-errors",
"violations": violations
}));
ErrorData::new(ErrorCode(-32602), err.to_string(), data)
}
ToolCallValidationError::RequestConstructionError { ref reason } => {
let data = Some(json!({
"type": "request-construction-error",
"reason": reason
}));
ErrorData::new(ErrorCode(-32602), err.to_string(), data)
}
}
}
}
impl From<ToolCallError> for ErrorData {
fn from(err: ToolCallError) -> Self {
match err {
ToolCallError::Validation(validation_err) => validation_err.into(),
ToolCallError::Execution(execution_err) => {
match execution_err {
ToolCallExecutionError::HttpError {
status,
ref message,
..
} => {
let data = Some(json!({
"type": "http-error",
"status": status,
"message": message
}));
ErrorData::new(ErrorCode(-32000), execution_err.to_string(), data)
}
ToolCallExecutionError::NetworkError {
ref message,
ref category,
} => {
let data = Some(json!({
"type": "network-error",
"message": message,
"category": category
}));
ErrorData::new(ErrorCode(-32000), execution_err.to_string(), data)
}
ToolCallExecutionError::ResponseParsingError { ref reason, .. } => {
let data = Some(json!({
"type": "response-parsing-error",
"reason": reason
}));
ErrorData::new(ErrorCode(-32700), execution_err.to_string(), data)
}
}
}
}
}
}
impl From<Error> for ErrorData {
fn from(err: Error) -> Self {
match err {
Error::Spec(msg) => ErrorData::new(
ErrorCode(-32700),
format!("OpenAPI spec error: {msg}"),
None,
),
Error::Validation(msg) => {
ErrorData::new(ErrorCode(-32602), format!("Validation error: {msg}"), None)
}
Error::HttpRequest(e) => {
ErrorData::new(ErrorCode(-32000), format!("HTTP request failed: {e}"), None)
}
Error::Http(msg) => {
ErrorData::new(ErrorCode(-32000), format!("HTTP error: {msg}"), None)
}
Error::Json(e) => {
ErrorData::new(ErrorCode(-32700), format!("JSON parsing error: {e}"), None)
}
Error::ToolCall(e) => e.into(),
_ => ErrorData::new(ErrorCode(-32000), err.to_string(), None),
}
}
}
#[derive(Debug, Error, Serialize)]
#[serde(untagged)]
pub enum ToolCallError {
#[error(transparent)]
Validation(#[from] ToolCallValidationError),
#[error(transparent)]
Execution(#[from] ToolCallExecutionError),
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct ErrorResponse {
pub error: ToolCallExecutionError,
}
#[derive(Debug, Error, Serialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum ToolCallValidationError {
#[error("Tool '{tool_name}' not found")]
#[serde(rename = "tool-not-found")]
ToolNotFound {
tool_name: String,
suggestions: Vec<String>,
},
#[error("{}", format_validation_errors(violations))]
#[serde(rename = "validation-errors")]
InvalidParameters {
violations: Vec<ValidationError>,
},
#[error("Failed to construct request: {reason}")]
#[serde(rename = "request-construction-error")]
RequestConstructionError {
reason: String,
},
}
#[derive(Debug, Error, Serialize, JsonSchema)]
#[serde(tag = "type", rename_all = "kebab-case")]
#[schemars(tag = "type", rename_all = "kebab-case")]
pub enum ToolCallExecutionError {
#[error("HTTP {status} error: {message}")]
#[serde(rename = "http-error")]
HttpError {
status: u16,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
details: Option<Value>,
},
#[error("Network error: {message}")]
#[serde(rename = "network-error")]
NetworkError {
message: String,
category: NetworkErrorCategory,
},
#[error("Failed to parse response: {reason}")]
#[serde(rename = "response-parsing-error")]
ResponseParsingError {
reason: String,
#[serde(skip_serializing_if = "Option::is_none")]
raw_response: Option<String>,
},
}
impl ToolCallValidationError {
pub fn tool_not_found(tool_name: String, available_tools: &[&str]) -> Self {
let suggestions = find_similar_strings(&tool_name, available_tools);
Self::ToolNotFound {
tool_name,
suggestions,
}
}
}
impl ValidationError {
pub fn invalid_parameter(parameter: String, valid_parameters: &[String]) -> Self {
let valid_params_refs: Vec<&str> = valid_parameters.iter().map(|s| s.as_str()).collect();
let suggestions = find_similar_strings(¶meter, &valid_params_refs);
Self::InvalidParameter {
parameter,
suggestions,
valid_parameters: valid_parameters.to_vec(),
}
}
}
#[derive(Debug, Serialize, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum NetworkErrorCategory {
Timeout,
Connect,
Request,
Body,
Decode,
Other,
}
#[cfg(test)]
mod tests {
use super::*;
use insta::assert_json_snapshot;
use serde_json::json;
#[test]
fn test_tool_call_error_serialization_with_details() {
let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
violations: vec![ValidationError::InvalidParameter {
parameter: "pet_id".to_string(),
suggestions: vec!["petId".to_string()],
valid_parameters: vec!["petId".to_string(), "timeout_seconds".to_string()],
}],
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_serialization_without_details() {
let error = ToolCallError::Validation(ToolCallValidationError::ToolNotFound {
tool_name: "unknownTool".to_string(),
suggestions: vec![],
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_serialization_with_suggestions() {
let error = ToolCallError::Validation(ToolCallValidationError::ToolNotFound {
tool_name: "getPetByID".to_string(),
suggestions: vec!["getPetById".to_string(), "getPetsByStatus".to_string()],
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_multiple_suggestions() {
let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
violations: vec![ValidationError::InvalidParameter {
parameter: "pet_i".to_string(),
suggestions: vec!["petId".to_string(), "petInfo".to_string()],
valid_parameters: vec![
"petId".to_string(),
"petInfo".to_string(),
"timeout".to_string(),
],
}],
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_no_suggestions() {
let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
violations: vec![ValidationError::InvalidParameter {
parameter: "completely_wrong".to_string(),
suggestions: vec![],
valid_parameters: vec!["petId".to_string(), "timeout".to_string()],
}],
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_validation() {
let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
violations: vec![ValidationError::MissingRequiredParameter {
parameter: "field".to_string(),
description: Some("Missing required field".to_string()),
expected_type: "string".to_string(),
}],
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_validation_detailed() {
let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
violations: vec![ValidationError::ConstraintViolation {
parameter: "age".to_string(),
message: "Parameter 'age' must be between 0 and 150".to_string(),
field_path: Some("age".to_string()),
actual_value: Some(Box::new(json!(200))),
expected_type: Some("integer".to_string()),
constraints: vec![
ValidationConstraint::Minimum {
value: 0.0,
exclusive: false,
},
ValidationConstraint::Maximum {
value: 150.0,
exclusive: false,
},
],
}],
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_validation_enum() {
let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
violations: vec![ValidationError::ConstraintViolation {
parameter: "status".to_string(),
message: "Parameter 'status' must be one of: available, pending, sold".to_string(),
field_path: Some("status".to_string()),
actual_value: Some(Box::new(json!("unknown"))),
expected_type: Some("string".to_string()),
constraints: vec![ValidationConstraint::EnumValues {
values: vec![json!("available"), json!("pending"), json!("sold")],
}],
}],
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_validation_format() {
let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
violations: vec![ValidationError::ConstraintViolation {
parameter: "email".to_string(),
message: "Invalid email format".to_string(),
field_path: Some("contact.email".to_string()),
actual_value: Some(Box::new(json!("not-an-email"))),
expected_type: Some("string".to_string()),
constraints: vec![ValidationConstraint::Format {
format: "email".to_string(),
}],
}],
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_http_error() {
let error = ToolCallError::Execution(ToolCallExecutionError::HttpError {
status: 404,
message: "Not found".to_string(),
details: None,
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_http_request() {
let error = ToolCallError::Execution(ToolCallExecutionError::NetworkError {
message: "Connection timeout".to_string(),
category: NetworkErrorCategory::Timeout,
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_json() {
let error = ToolCallError::Execution(ToolCallExecutionError::ResponseParsingError {
reason: "Invalid JSON".to_string(),
raw_response: None,
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_request_construction() {
let error = ToolCallError::Validation(ToolCallValidationError::RequestConstructionError {
reason: "Invalid parameter location: body".to_string(),
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_error_response_serialization() {
let error = ToolCallExecutionError::HttpError {
status: 400,
message: "Bad Request".to_string(),
details: Some(json!({
"error": "Invalid parameter",
"parameter": "test_param"
})),
};
let response = ErrorResponse { error };
let serialized = serde_json::to_value(&response).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_validation_multiple_of() {
let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
violations: vec![ValidationError::ConstraintViolation {
parameter: "price".to_string(),
message: "10.5 is not a multiple of 3".to_string(),
field_path: Some("price".to_string()),
actual_value: Some(Box::new(json!(10.5))),
expected_type: Some("number".to_string()),
constraints: vec![ValidationConstraint::MultipleOf { value: 3.0 }],
}],
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_validation_min_items() {
let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
violations: vec![ValidationError::ConstraintViolation {
parameter: "tags".to_string(),
message: "Array has 1 items but minimum is 2".to_string(),
field_path: Some("tags".to_string()),
actual_value: Some(Box::new(json!(["tag1"]))),
expected_type: Some("array".to_string()),
constraints: vec![ValidationConstraint::MinItems { value: 2 }],
}],
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_validation_max_items() {
let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
violations: vec![ValidationError::ConstraintViolation {
parameter: "categories".to_string(),
message: "Array has 4 items but maximum is 3".to_string(),
field_path: Some("categories".to_string()),
actual_value: Some(Box::new(json!(["a", "b", "c", "d"]))),
expected_type: Some("array".to_string()),
constraints: vec![ValidationConstraint::MaxItems { value: 3 }],
}],
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_validation_unique_items() {
let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
violations: vec![ValidationError::ConstraintViolation {
parameter: "numbers".to_string(),
message: "Array items [1, 2, 2, 3] are not unique".to_string(),
field_path: Some("numbers".to_string()),
actual_value: Some(Box::new(json!([1, 2, 2, 3]))),
expected_type: Some("array".to_string()),
constraints: vec![ValidationConstraint::UniqueItems],
}],
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_validation_min_properties() {
let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
violations: vec![ValidationError::ConstraintViolation {
parameter: "metadata".to_string(),
message: "Object has 2 properties but minimum is 3".to_string(),
field_path: Some("metadata".to_string()),
actual_value: Some(Box::new(json!({"name": "test", "version": "1.0"}))),
expected_type: Some("object".to_string()),
constraints: vec![ValidationConstraint::MinProperties { value: 3 }],
}],
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_validation_max_properties() {
let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
violations: vec![ValidationError::ConstraintViolation {
parameter: "config".to_string(),
message: "Object has 3 properties but maximum is 2".to_string(),
field_path: Some("config".to_string()),
actual_value: Some(Box::new(json!({"a": 1, "b": 2, "c": 3}))),
expected_type: Some("object".to_string()),
constraints: vec![ValidationConstraint::MaxProperties { value: 2 }],
}],
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_tool_call_error_validation_const() {
let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
violations: vec![ValidationError::ConstraintViolation {
parameter: "environment".to_string(),
message: r#""staging" is not equal to const "production""#.to_string(),
field_path: Some("environment".to_string()),
actual_value: Some(Box::new(json!("staging"))),
expected_type: Some("string".to_string()),
constraints: vec![ValidationConstraint::ConstValue {
value: json!("production"),
}],
}],
});
let serialized = serde_json::to_value(&error).unwrap();
assert_json_snapshot!(serialized);
}
#[test]
fn test_error_data_conversion_preserves_details() {
let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
violations: vec![ValidationError::InvalidParameter {
parameter: "page".to_string(),
suggestions: vec!["page_number".to_string()],
valid_parameters: vec!["page_number".to_string(), "page_size".to_string()],
}],
});
let error_data: ErrorData = error.into();
let error_json = serde_json::to_value(&error_data).unwrap();
assert!(error_json["data"].is_object(), "Should have data field");
assert_eq!(
error_json["data"]["type"].as_str(),
Some("validation-errors"),
"Should have validation-errors type"
);
let network_error = ToolCallError::Execution(ToolCallExecutionError::NetworkError {
message: "SSL/TLS connection failed - certificate verification error".to_string(),
category: NetworkErrorCategory::Connect,
});
let error_data: ErrorData = network_error.into();
let error_json = serde_json::to_value(&error_data).unwrap();
assert!(error_json["data"].is_object(), "Should have data field");
assert_eq!(
error_json["data"]["type"].as_str(),
Some("network-error"),
"Should have network-error type"
);
assert!(
error_json["data"]["message"]
.as_str()
.unwrap()
.contains("SSL/TLS"),
"Should preserve error message"
);
}
#[test]
fn test_find_similar_strings() {
let known = vec!["page_size", "user_id", "status"];
let suggestions = find_similar_strings("page_sixe", &known);
assert_eq!(suggestions, vec!["page_size"]);
let suggestions = find_similar_strings("xyz123", &known);
assert!(suggestions.is_empty());
let known = vec!["limit", "offset"];
let suggestions = find_similar_strings("lmiit", &known);
assert_eq!(suggestions, vec!["limit"]);
let known = vec!["project_id", "merge_request_id"];
let suggestions = find_similar_strings("projct_id", &known);
assert_eq!(suggestions, vec!["project_id"]);
let known = vec!["name", "email"];
let suggestions = find_similar_strings("namee", &known);
assert_eq!(suggestions, vec!["name"]);
}
}