Skip to main content

rmcp_openapi/
error.rs

1//! Error handling for the OpenAPI MCP server.
2//!
3//! This module provides structured error types that distinguish between validation errors
4//! (which return as MCP protocol errors) and execution errors (which appear in tool output schemas).
5//!
6//! # Error Categories
7//!
8//! ## Validation Errors (MCP Protocol Errors)
9//! These errors occur before tool execution and are returned as MCP protocol errors (Err(ErrorData)).
10//! They do NOT have JsonSchema derive to prevent them from appearing in tool output schemas.
11//!
12//! - **ToolNotFound**: Requested tool doesn't exist
13//! - **InvalidParameters**: Parameter validation failed (unknown names, missing required, constraint violations)
14//! - **RequestConstructionError**: Failed to construct the HTTP request
15//!
16//! ## Execution Errors (Tool Output Errors)
17//! These errors occur during tool execution and are returned as structured content in the tool response.
18//! They have JsonSchema derive so they can appear in tool output schemas.
19//!
20//! - **HttpError**: HTTP error response from the API (4xx, 5xx status codes)
21//! - **NetworkError**: Network/connection failures (timeout, DNS, connection refused)
22//! - **ResponseParsingError**: Failed to parse the response
23//!
24//! # Error Type Examples
25//!
26//! ## InvalidParameter (Validation Error)
27//! ```json
28//! {
29//!   "type": "invalid-parameter",
30//!   "parameter": "pet_id",
31//!   "suggestions": ["petId"],
32//!   "valid_parameters": ["petId", "status"]
33//! }
34//! ```
35//!
36//! ## ConstraintViolation (Validation Error)
37//! ```json
38//! {
39//!   "type": "constraint-violation",
40//!   "parameter": "age",
41//!   "message": "Parameter 'age' must be between 0 and 150",
42//!   "field_path": "age",
43//!   "actual_value": 200,
44//!   "expected_type": "integer",
45//!   "constraints": [
46//!     {"type": "minimum", "value": 0, "exclusive": false},
47//!     {"type": "maximum", "value": 150, "exclusive": false}
48//!   ]
49//! }
50//! ```
51//!
52//! ## HttpError (Execution Error)
53//! ```json
54//! {
55//!   "type": "http-error",
56//!   "status": 404,
57//!   "message": "Pet not found",
58//!   "details": {"error": "NOT_FOUND", "pet_id": 123}
59//! }
60//! ```
61//!
62//! ## NetworkError (Execution Error)
63//! ```json
64//! {
65//!   "type": "network-error",
66//!   "message": "Request timeout after 30 seconds",
67//!   "category": "timeout"
68//! }
69//! ```
70//!
71//! # Structured Error Responses
72//!
73//! For tools with output schemas, execution errors are wrapped in the standard response structure:
74//! ```json
75//! {
76//!   "status": 404,
77//!   "body": {
78//!     "error": {
79//!       "type": "http-error",
80//!       "status": 404,
81//!       "message": "Pet not found"
82//!     }
83//!   }
84//! }
85//! ```
86//!
87//! Validation errors are returned as MCP protocol errors:
88//! ```json
89//! {
90//!   "code": -32602,
91//!   "message": "Validation failed with 1 error",
92//!   "data": {
93//!     "type": "validation-errors",
94//!     "violations": [
95//!       {
96//!         "type": "invalid-parameter",
97//!         "parameter": "pet_id",
98//!         "suggestions": ["petId"],
99//!         "valid_parameters": ["petId", "status"]
100//!       }
101//!     ]
102//!   }
103//! }
104//! ```
105//!
106//! This consistent structure allows clients to:
107//! - Programmatically handle different error types
108//! - Provide helpful feedback to users
109//! - Automatically fix certain errors (e.g., typos in parameter names)
110//! - Retry requests with corrected parameters
111
112use rmcp::model::{ErrorCode, ErrorData};
113use schemars::JsonSchema;
114use serde::Serialize;
115use serde_json::{Value, json};
116use std::fmt;
117use thiserror::Error;
118
119/// Find similar strings using Jaro distance algorithm
120/// Used for parameter and tool name suggestions in errors
121fn find_similar_strings(unknown: &str, known_strings: &[&str]) -> Vec<String> {
122    use strsim::jaro;
123
124    let mut candidates = Vec::new();
125    for string in known_strings {
126        let confidence = jaro(unknown, string);
127        if confidence > 0.7 {
128            candidates.push((confidence, string.to_string()));
129        }
130    }
131
132    // Sort by confidence (highest first)
133    candidates.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
134    candidates.into_iter().map(|(_, name)| name).collect()
135}
136
137/// Individual validation constraint that was violated
138#[derive(Debug, Serialize, JsonSchema)]
139#[serde(tag = "type", rename_all = "kebab-case")]
140pub enum ValidationConstraint {
141    /// Minimum value constraint (for numbers)
142    Minimum {
143        /// The minimum value
144        value: f64,
145        /// Whether the minimum is exclusive
146        exclusive: bool,
147    },
148    /// Maximum value constraint (for numbers)
149    Maximum {
150        /// The maximum value
151        value: f64,
152        /// Whether the maximum is exclusive
153        exclusive: bool,
154    },
155    /// Minimum length constraint (for strings/arrays)
156    MinLength {
157        /// The minimum length
158        value: usize,
159    },
160    /// Maximum length constraint (for strings/arrays)
161    MaxLength {
162        /// The maximum length
163        value: usize,
164    },
165    /// Pattern constraint (for strings)
166    Pattern {
167        /// The regex pattern that must be matched
168        pattern: String,
169    },
170    /// Enum values constraint
171    EnumValues {
172        /// The allowed enum values
173        values: Vec<Value>,
174    },
175    /// Format constraint (e.g., "date-time", "email", "uri")
176    Format {
177        /// The expected format
178        format: String,
179    },
180    /// Multiple of constraint (for numbers)
181    MultipleOf {
182        /// The value that the number must be a multiple of
183        value: f64,
184    },
185    /// Minimum number of items constraint (for arrays)
186    MinItems {
187        /// The minimum number of items
188        value: usize,
189    },
190    /// Maximum number of items constraint (for arrays)
191    MaxItems {
192        /// The maximum number of items
193        value: usize,
194    },
195    /// Unique items constraint (for arrays)
196    UniqueItems,
197    /// Minimum number of properties constraint (for objects)
198    MinProperties {
199        /// The minimum number of properties
200        value: usize,
201    },
202    /// Maximum number of properties constraint (for objects)
203    MaxProperties {
204        /// The maximum number of properties
205        value: usize,
206    },
207    /// Constant value constraint
208    ConstValue {
209        /// The exact value that must match
210        value: Value,
211    },
212    /// Required properties constraint (for objects)
213    Required {
214        /// The required property names
215        properties: Vec<String>,
216    },
217}
218
219/// Individual validation error types
220#[derive(Debug, Serialize, JsonSchema)]
221#[serde(tag = "type", rename_all = "kebab-case")]
222pub enum ValidationError {
223    /// Invalid parameter error with suggestions
224    InvalidParameter {
225        /// The parameter name that was invalid
226        parameter: String,
227        /// Suggested correct parameter names
228        suggestions: Vec<String>,
229        /// All valid parameter names for this tool
230        valid_parameters: Vec<String>,
231    },
232    /// Missing required parameter
233    MissingRequiredParameter {
234        /// Name of the missing parameter
235        parameter: String,
236        /// Description of the parameter from OpenAPI
237        #[serde(skip_serializing_if = "Option::is_none")]
238        description: Option<String>,
239        /// Expected type of the parameter
240        expected_type: String,
241    },
242    /// Constraint violation (e.g., type mismatches, pattern violations)
243    ConstraintViolation {
244        /// Name of the parameter that failed validation
245        parameter: String,
246        /// Description of what validation failed
247        message: String,
248        /// Path to the field that failed validation (e.g., "address.street")
249        #[serde(skip_serializing_if = "Option::is_none")]
250        field_path: Option<String>,
251        /// The actual value that failed validation
252        #[serde(skip_serializing_if = "Option::is_none")]
253        actual_value: Option<Box<Value>>,
254        /// Expected type or format
255        #[serde(skip_serializing_if = "Option::is_none")]
256        expected_type: Option<String>,
257        /// Specific constraints that were violated
258        #[serde(skip_serializing_if = "Vec::is_empty")]
259        constraints: Vec<ValidationConstraint>,
260    },
261}
262
263impl fmt::Display for ValidationError {
264    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265        match self {
266            ValidationError::InvalidParameter {
267                parameter,
268                suggestions,
269                ..
270            } => {
271                if suggestions.is_empty() {
272                    write!(f, "'{parameter}'")
273                } else {
274                    write!(f, "'{parameter}' (suggestions: {})", suggestions.join(", "))
275                }
276            }
277            ValidationError::MissingRequiredParameter {
278                parameter,
279                expected_type,
280                ..
281            } => {
282                write!(f, "'{parameter}' is required (expected: {expected_type})")
283            }
284            ValidationError::ConstraintViolation {
285                parameter, message, ..
286            } => {
287                write!(f, "'{parameter}': {message}")
288            }
289        }
290    }
291}
292
293/// Helper function to format multiple validation errors into a single message
294fn format_validation_errors(violations: &[ValidationError]) -> String {
295    match violations.len() {
296        0 => "Validation failed".to_string(),
297        1 => {
298            // For single error, we need to add context about what type of error it is
299            let error = &violations[0];
300            match error {
301                ValidationError::InvalidParameter { .. } => {
302                    format!("Validation failed - invalid parameter {error}")
303                }
304                ValidationError::MissingRequiredParameter { .. } => {
305                    format!("Validation failed - missing required parameter: {error}")
306                }
307                ValidationError::ConstraintViolation { .. } => {
308                    format!("Validation failed - parameter {error}")
309                }
310            }
311        }
312        _ => {
313            // For multiple errors, use the new format
314            let mut invalid_params = Vec::new();
315            let mut missing_params = Vec::new();
316            let mut constraint_violations = Vec::new();
317
318            // Group errors by type
319            for error in violations {
320                match error {
321                    ValidationError::InvalidParameter { .. } => {
322                        invalid_params.push(error.to_string());
323                    }
324                    ValidationError::MissingRequiredParameter { .. } => {
325                        missing_params.push(error.to_string());
326                    }
327                    ValidationError::ConstraintViolation { .. } => {
328                        constraint_violations.push(error.to_string());
329                    }
330                }
331            }
332
333            let mut parts = Vec::new();
334
335            // Format invalid parameters
336            if !invalid_params.is_empty() {
337                let params_str = invalid_params.join(", ");
338                parts.push(format!("invalid parameters: {params_str}"));
339            }
340
341            // Format missing parameters
342            if !missing_params.is_empty() {
343                let params_str = missing_params.join(", ");
344                parts.push(format!("missing parameters: {params_str}"));
345            }
346
347            // Format constraint violations
348            if !constraint_violations.is_empty() {
349                let violations_str = constraint_violations.join("; ");
350                parts.push(format!("constraint violations: {violations_str}"));
351            }
352
353            format!("Validation failed - {}", parts.join("; "))
354        }
355    }
356}
357
358/// CLI-specific errors for command-line argument parsing and validation
359#[derive(Debug, Error)]
360pub enum CliError {
361    #[error("Invalid header format in '{header}': expected 'name: value' format")]
362    InvalidHeaderFormat { header: String },
363
364    #[error("Invalid header name in '{header}': {source}")]
365    InvalidHeaderName {
366        header: String,
367        #[source]
368        source: http::header::InvalidHeaderName,
369    },
370
371    #[error("Invalid header value in '{header}': {source}")]
372    InvalidHeaderValue {
373        header: String,
374        #[source]
375        source: http::header::InvalidHeaderValue,
376    },
377}
378
379#[derive(Debug, Error)]
380pub enum Error {
381    #[error("CLI error: {0}")]
382    Cli(#[from] CliError),
383    #[error("Environment variable error: {0}")]
384    EnvVar(#[from] std::env::VarError),
385    #[error("IO error: {0}")]
386    Io(#[from] std::io::Error),
387    #[error("OpenAPI spec error: {0}")]
388    Spec(String),
389    #[error("Tool generation error: {0}")]
390    ToolGeneration(String),
391    #[error("Invalid parameter location: {0}")]
392    InvalidParameterLocation(String),
393    #[error("Invalid URL: {0}")]
394    InvalidUrl(String),
395    #[error("File not found: {0}")]
396    FileNotFound(String),
397    #[error("MCP error: {0}")]
398    McpError(String),
399    #[error("Invalid path: {0}")]
400    InvalidPath(String),
401    #[error("Validation error: {0}")]
402    Validation(String),
403    #[error("HTTP error: {0}")]
404    Http(String),
405    #[error("HTTP request error: {0}")]
406    HttpRequest(#[from] reqwest::Error),
407    #[error("JSON error at {path}: {source}")]
408    JsonAtPath {
409        path: String,
410        source: serde_json::Error,
411    },
412    #[error("JSON error: {0}")]
413    Json(#[from] serde_json::Error),
414    #[error(transparent)]
415    ToolCall(#[from] ToolCallError),
416    #[error("Tool not found: {0}")]
417    ToolNotFound(String),
418}
419
420impl From<ToolCallValidationError> for ErrorData {
421    fn from(err: ToolCallValidationError) -> Self {
422        match err {
423            ToolCallValidationError::ToolNotFound {
424                ref tool_name,
425                ref suggestions,
426            } => {
427                let data = if suggestions.is_empty() {
428                    None
429                } else {
430                    Some(json!({
431                        "suggestions": suggestions
432                    }))
433                };
434                ErrorData::new(
435                    ErrorCode(-32601),
436                    format!("Tool '{tool_name}' not found"),
437                    data,
438                )
439            }
440            ToolCallValidationError::InvalidParameters { ref violations } => {
441                // Include the full validation error details
442                let data = Some(json!({
443                    "type": "validation-errors",
444                    "violations": violations
445                }));
446                ErrorData::new(ErrorCode(-32602), err.to_string(), data)
447            }
448            ToolCallValidationError::RequestConstructionError { ref reason } => {
449                // Include construction error details
450                let data = Some(json!({
451                    "type": "request-construction-error",
452                    "reason": reason
453                }));
454                ErrorData::new(ErrorCode(-32602), err.to_string(), data)
455            }
456        }
457    }
458}
459
460impl From<ToolCallError> for ErrorData {
461    fn from(err: ToolCallError) -> Self {
462        match err {
463            ToolCallError::Validation(validation_err) => validation_err.into(),
464            ToolCallError::Execution(execution_err) => {
465                // Execution errors should not be converted to ErrorData
466                // They should be returned as CallToolResult with is_error: true
467                // But for backward compatibility, we'll convert them
468                match execution_err {
469                    ToolCallExecutionError::HttpError {
470                        status,
471                        ref message,
472                        ..
473                    } => {
474                        let data = Some(json!({
475                            "type": "http-error",
476                            "status": status,
477                            "message": message
478                        }));
479                        ErrorData::new(ErrorCode(-32000), execution_err.to_string(), data)
480                    }
481                    ToolCallExecutionError::NetworkError {
482                        ref message,
483                        ref category,
484                    } => {
485                        let data = Some(json!({
486                            "type": "network-error",
487                            "message": message,
488                            "category": category
489                        }));
490                        ErrorData::new(ErrorCode(-32000), execution_err.to_string(), data)
491                    }
492                    ToolCallExecutionError::ResponseParsingError { ref reason, .. } => {
493                        let data = Some(json!({
494                            "type": "response-parsing-error",
495                            "reason": reason
496                        }));
497                        ErrorData::new(ErrorCode(-32700), execution_err.to_string(), data)
498                    }
499                }
500            }
501        }
502    }
503}
504
505impl From<Error> for ErrorData {
506    fn from(err: Error) -> Self {
507        match err {
508            Error::Spec(msg) => ErrorData::new(
509                ErrorCode(-32700),
510                format!("OpenAPI spec error: {msg}"),
511                None,
512            ),
513            Error::Validation(msg) => {
514                ErrorData::new(ErrorCode(-32602), format!("Validation error: {msg}"), None)
515            }
516            Error::HttpRequest(e) => {
517                ErrorData::new(ErrorCode(-32000), format!("HTTP request failed: {e}"), None)
518            }
519            Error::Http(msg) => {
520                ErrorData::new(ErrorCode(-32000), format!("HTTP error: {msg}"), None)
521            }
522            Error::Json(e) => {
523                ErrorData::new(ErrorCode(-32700), format!("JSON parsing error: {e}"), None)
524            }
525            Error::ToolCall(e) => e.into(),
526            _ => ErrorData::new(ErrorCode(-32000), err.to_string(), None),
527        }
528    }
529}
530
531/// Error that can occur during tool execution
532#[derive(Debug, Error, Serialize)]
533#[serde(untagged)]
534pub enum ToolCallError {
535    /// Validation errors that occur before tool execution
536    #[error(transparent)]
537    Validation(#[from] ToolCallValidationError),
538
539    /// Execution errors that occur during tool execution
540    #[error(transparent)]
541    Execution(#[from] ToolCallExecutionError),
542}
543
544/// Error response structure for tool execution failures
545#[derive(Debug, Serialize, JsonSchema)]
546pub struct ErrorResponse {
547    /// Error information
548    pub error: ToolCallExecutionError,
549}
550
551/// Validation errors that occur before tool execution
552/// These return as Err(ErrorData) with MCP protocol error codes
553#[derive(Debug, Error, Serialize)]
554#[serde(tag = "type", rename_all = "kebab-case")]
555pub enum ToolCallValidationError {
556    /// Tool not found
557    #[error("Tool '{tool_name}' not found")]
558    #[serde(rename = "tool-not-found")]
559    ToolNotFound {
560        /// Name of the tool that was not found
561        tool_name: String,
562        /// Suggested tool names based on similarity
563        suggestions: Vec<String>,
564    },
565
566    /// Invalid parameters (unknown names, missing required, constraints)
567    #[error("{}", format_validation_errors(violations))]
568    #[serde(rename = "validation-errors")]
569    InvalidParameters {
570        /// List of validation errors
571        violations: Vec<ValidationError>,
572    },
573
574    /// Request construction failed (JSON serialization for body)
575    #[error("Failed to construct request: {reason}")]
576    #[serde(rename = "request-construction-error")]
577    RequestConstructionError {
578        /// Description of the construction failure
579        reason: String,
580    },
581}
582
583/// Execution errors that occur during tool execution
584/// These return as Ok(CallToolResult { is_error: true })
585#[derive(Debug, Error, Serialize, JsonSchema)]
586#[serde(tag = "type", rename_all = "kebab-case")]
587#[schemars(tag = "type", rename_all = "kebab-case")]
588pub enum ToolCallExecutionError {
589    /// HTTP error response from the API
590    #[error("HTTP {status} error: {message}")]
591    #[serde(rename = "http-error")]
592    HttpError {
593        /// HTTP status code
594        status: u16,
595        /// Error message or response body
596        message: String,
597        /// Optional structured error details from API
598        #[serde(skip_serializing_if = "Option::is_none")]
599        details: Option<Value>,
600    },
601
602    /// Network/connection failures
603    #[error("Network error: {message}")]
604    #[serde(rename = "network-error")]
605    NetworkError {
606        /// Description of the network failure
607        message: String,
608        /// Error category for better handling
609        category: NetworkErrorCategory,
610    },
611
612    /// Response parsing failed
613    #[error("Failed to parse response: {reason}")]
614    #[serde(rename = "response-parsing-error")]
615    ResponseParsingError {
616        /// Description of the parsing failure
617        reason: String,
618        /// Raw response body for debugging
619        #[serde(skip_serializing_if = "Option::is_none")]
620        raw_response: Option<String>,
621    },
622}
623
624impl ToolCallValidationError {
625    /// Create a ToolNotFound error with suggestions based on available tools
626    pub fn tool_not_found(tool_name: String, available_tools: &[&str]) -> Self {
627        let suggestions = find_similar_strings(&tool_name, available_tools);
628        Self::ToolNotFound {
629            tool_name,
630            suggestions,
631        }
632    }
633}
634
635impl ValidationError {
636    /// Create an InvalidParameter error with suggestions based on valid parameters
637    pub fn invalid_parameter(parameter: String, valid_parameters: &[String]) -> Self {
638        let valid_params_refs: Vec<&str> = valid_parameters.iter().map(|s| s.as_str()).collect();
639        let suggestions = find_similar_strings(&parameter, &valid_params_refs);
640        Self::InvalidParameter {
641            parameter,
642            suggestions,
643            valid_parameters: valid_parameters.to_vec(),
644        }
645    }
646}
647
648/// Network error categories for better error handling
649#[derive(Debug, Serialize, JsonSchema)]
650#[serde(rename_all = "kebab-case")]
651pub enum NetworkErrorCategory {
652    /// Request timeout
653    Timeout,
654    /// Connection error (DNS, refused, unreachable)
655    Connect,
656    /// Request construction/sending error
657    Request,
658    /// Response body error
659    Body,
660    /// Response decoding error
661    Decode,
662    /// Other network errors
663    Other,
664}
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669    use insta::assert_json_snapshot;
670    use serde_json::json;
671
672    #[test]
673    fn test_tool_call_error_serialization_with_details() {
674        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
675            violations: vec![ValidationError::InvalidParameter {
676                parameter: "pet_id".to_string(),
677                suggestions: vec!["petId".to_string()],
678                valid_parameters: vec!["petId".to_string(), "timeout_seconds".to_string()],
679            }],
680        });
681
682        let serialized = serde_json::to_value(&error).unwrap();
683        assert_json_snapshot!(serialized);
684    }
685
686    #[test]
687    fn test_tool_call_error_serialization_without_details() {
688        let error = ToolCallError::Validation(ToolCallValidationError::ToolNotFound {
689            tool_name: "unknownTool".to_string(),
690            suggestions: vec![],
691        });
692
693        let serialized = serde_json::to_value(&error).unwrap();
694        assert_json_snapshot!(serialized);
695    }
696
697    #[test]
698    fn test_tool_call_error_serialization_with_suggestions() {
699        let error = ToolCallError::Validation(ToolCallValidationError::ToolNotFound {
700            tool_name: "getPetByID".to_string(),
701            suggestions: vec!["getPetById".to_string(), "getPetsByStatus".to_string()],
702        });
703
704        let serialized = serde_json::to_value(&error).unwrap();
705        assert_json_snapshot!(serialized);
706    }
707
708    #[test]
709    fn test_tool_call_error_multiple_suggestions() {
710        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
711            violations: vec![ValidationError::InvalidParameter {
712                parameter: "pet_i".to_string(),
713                suggestions: vec!["petId".to_string(), "petInfo".to_string()],
714                valid_parameters: vec![
715                    "petId".to_string(),
716                    "petInfo".to_string(),
717                    "timeout".to_string(),
718                ],
719            }],
720        });
721
722        let serialized = serde_json::to_value(&error).unwrap();
723        assert_json_snapshot!(serialized);
724    }
725
726    #[test]
727    fn test_tool_call_error_no_suggestions() {
728        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
729            violations: vec![ValidationError::InvalidParameter {
730                parameter: "completely_wrong".to_string(),
731                suggestions: vec![],
732                valid_parameters: vec!["petId".to_string(), "timeout".to_string()],
733            }],
734        });
735
736        let serialized = serde_json::to_value(&error).unwrap();
737        assert_json_snapshot!(serialized);
738    }
739
740    #[test]
741    fn test_tool_call_error_validation() {
742        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
743            violations: vec![ValidationError::MissingRequiredParameter {
744                parameter: "field".to_string(),
745                description: Some("Missing required field".to_string()),
746                expected_type: "string".to_string(),
747            }],
748        });
749        let serialized = serde_json::to_value(&error).unwrap();
750        assert_json_snapshot!(serialized);
751    }
752
753    #[test]
754    fn test_tool_call_error_validation_detailed() {
755        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
756            violations: vec![ValidationError::ConstraintViolation {
757                parameter: "age".to_string(),
758                message: "Parameter 'age' must be between 0 and 150".to_string(),
759                field_path: Some("age".to_string()),
760                actual_value: Some(Box::new(json!(200))),
761                expected_type: Some("integer".to_string()),
762                constraints: vec![
763                    ValidationConstraint::Minimum {
764                        value: 0.0,
765                        exclusive: false,
766                    },
767                    ValidationConstraint::Maximum {
768                        value: 150.0,
769                        exclusive: false,
770                    },
771                ],
772            }],
773        });
774
775        let serialized = serde_json::to_value(&error).unwrap();
776        assert_json_snapshot!(serialized);
777    }
778
779    #[test]
780    fn test_tool_call_error_validation_enum() {
781        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
782            violations: vec![ValidationError::ConstraintViolation {
783                parameter: "status".to_string(),
784                message: "Parameter 'status' must be one of: available, pending, sold".to_string(),
785                field_path: Some("status".to_string()),
786                actual_value: Some(Box::new(json!("unknown"))),
787                expected_type: Some("string".to_string()),
788                constraints: vec![ValidationConstraint::EnumValues {
789                    values: vec![json!("available"), json!("pending"), json!("sold")],
790                }],
791            }],
792        });
793
794        let serialized = serde_json::to_value(&error).unwrap();
795        assert_json_snapshot!(serialized);
796    }
797
798    #[test]
799    fn test_tool_call_error_validation_format() {
800        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
801            violations: vec![ValidationError::ConstraintViolation {
802                parameter: "email".to_string(),
803                message: "Invalid email format".to_string(),
804                field_path: Some("contact.email".to_string()),
805                actual_value: Some(Box::new(json!("not-an-email"))),
806                expected_type: Some("string".to_string()),
807                constraints: vec![ValidationConstraint::Format {
808                    format: "email".to_string(),
809                }],
810            }],
811        });
812
813        let serialized = serde_json::to_value(&error).unwrap();
814        assert_json_snapshot!(serialized);
815    }
816
817    #[test]
818    fn test_tool_call_error_http_error() {
819        let error = ToolCallError::Execution(ToolCallExecutionError::HttpError {
820            status: 404,
821            message: "Not found".to_string(),
822            details: None,
823        });
824        let serialized = serde_json::to_value(&error).unwrap();
825        assert_json_snapshot!(serialized);
826    }
827
828    #[test]
829    fn test_tool_call_error_http_request() {
830        let error = ToolCallError::Execution(ToolCallExecutionError::NetworkError {
831            message: "Connection timeout".to_string(),
832            category: NetworkErrorCategory::Timeout,
833        });
834        let serialized = serde_json::to_value(&error).unwrap();
835        assert_json_snapshot!(serialized);
836    }
837
838    #[test]
839    fn test_tool_call_error_json() {
840        let error = ToolCallError::Execution(ToolCallExecutionError::ResponseParsingError {
841            reason: "Invalid JSON".to_string(),
842            raw_response: None,
843        });
844        let serialized = serde_json::to_value(&error).unwrap();
845        assert_json_snapshot!(serialized);
846    }
847
848    #[test]
849    fn test_tool_call_error_request_construction() {
850        let error = ToolCallError::Validation(ToolCallValidationError::RequestConstructionError {
851            reason: "Invalid parameter location: body".to_string(),
852        });
853        let serialized = serde_json::to_value(&error).unwrap();
854        assert_json_snapshot!(serialized);
855    }
856
857    #[test]
858    fn test_error_response_serialization() {
859        let error = ToolCallExecutionError::HttpError {
860            status: 400,
861            message: "Bad Request".to_string(),
862            details: Some(json!({
863                "error": "Invalid parameter",
864                "parameter": "test_param"
865            })),
866        };
867
868        let response = ErrorResponse { error };
869        let serialized = serde_json::to_value(&response).unwrap();
870        assert_json_snapshot!(serialized);
871    }
872
873    #[test]
874    fn test_tool_call_error_validation_multiple_of() {
875        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
876            violations: vec![ValidationError::ConstraintViolation {
877                parameter: "price".to_string(),
878                message: "10.5 is not a multiple of 3".to_string(),
879                field_path: Some("price".to_string()),
880                actual_value: Some(Box::new(json!(10.5))),
881                expected_type: Some("number".to_string()),
882                constraints: vec![ValidationConstraint::MultipleOf { value: 3.0 }],
883            }],
884        });
885
886        let serialized = serde_json::to_value(&error).unwrap();
887        assert_json_snapshot!(serialized);
888    }
889
890    #[test]
891    fn test_tool_call_error_validation_min_items() {
892        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
893            violations: vec![ValidationError::ConstraintViolation {
894                parameter: "tags".to_string(),
895                message: "Array has 1 items but minimum is 2".to_string(),
896                field_path: Some("tags".to_string()),
897                actual_value: Some(Box::new(json!(["tag1"]))),
898                expected_type: Some("array".to_string()),
899                constraints: vec![ValidationConstraint::MinItems { value: 2 }],
900            }],
901        });
902
903        let serialized = serde_json::to_value(&error).unwrap();
904        assert_json_snapshot!(serialized);
905    }
906
907    #[test]
908    fn test_tool_call_error_validation_max_items() {
909        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
910            violations: vec![ValidationError::ConstraintViolation {
911                parameter: "categories".to_string(),
912                message: "Array has 4 items but maximum is 3".to_string(),
913                field_path: Some("categories".to_string()),
914                actual_value: Some(Box::new(json!(["a", "b", "c", "d"]))),
915                expected_type: Some("array".to_string()),
916                constraints: vec![ValidationConstraint::MaxItems { value: 3 }],
917            }],
918        });
919
920        let serialized = serde_json::to_value(&error).unwrap();
921        assert_json_snapshot!(serialized);
922    }
923
924    #[test]
925    fn test_tool_call_error_validation_unique_items() {
926        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
927            violations: vec![ValidationError::ConstraintViolation {
928                parameter: "numbers".to_string(),
929                message: "Array items [1, 2, 2, 3] are not unique".to_string(),
930                field_path: Some("numbers".to_string()),
931                actual_value: Some(Box::new(json!([1, 2, 2, 3]))),
932                expected_type: Some("array".to_string()),
933                constraints: vec![ValidationConstraint::UniqueItems],
934            }],
935        });
936
937        let serialized = serde_json::to_value(&error).unwrap();
938        assert_json_snapshot!(serialized);
939    }
940
941    #[test]
942    fn test_tool_call_error_validation_min_properties() {
943        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
944            violations: vec![ValidationError::ConstraintViolation {
945                parameter: "metadata".to_string(),
946                message: "Object has 2 properties but minimum is 3".to_string(),
947                field_path: Some("metadata".to_string()),
948                actual_value: Some(Box::new(json!({"name": "test", "version": "1.0"}))),
949                expected_type: Some("object".to_string()),
950                constraints: vec![ValidationConstraint::MinProperties { value: 3 }],
951            }],
952        });
953
954        let serialized = serde_json::to_value(&error).unwrap();
955        assert_json_snapshot!(serialized);
956    }
957
958    #[test]
959    fn test_tool_call_error_validation_max_properties() {
960        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
961            violations: vec![ValidationError::ConstraintViolation {
962                parameter: "config".to_string(),
963                message: "Object has 3 properties but maximum is 2".to_string(),
964                field_path: Some("config".to_string()),
965                actual_value: Some(Box::new(json!({"a": 1, "b": 2, "c": 3}))),
966                expected_type: Some("object".to_string()),
967                constraints: vec![ValidationConstraint::MaxProperties { value: 2 }],
968            }],
969        });
970
971        let serialized = serde_json::to_value(&error).unwrap();
972        assert_json_snapshot!(serialized);
973    }
974
975    #[test]
976    fn test_tool_call_error_validation_const() {
977        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
978            violations: vec![ValidationError::ConstraintViolation {
979                parameter: "environment".to_string(),
980                message: r#""staging" is not equal to const "production""#.to_string(),
981                field_path: Some("environment".to_string()),
982                actual_value: Some(Box::new(json!("staging"))),
983                expected_type: Some("string".to_string()),
984                constraints: vec![ValidationConstraint::ConstValue {
985                    value: json!("production"),
986                }],
987            }],
988        });
989
990        let serialized = serde_json::to_value(&error).unwrap();
991        assert_json_snapshot!(serialized);
992    }
993
994    #[test]
995    fn test_error_data_conversion_preserves_details() {
996        // Test InvalidParameter error conversion
997        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
998            violations: vec![ValidationError::InvalidParameter {
999                parameter: "page".to_string(),
1000                suggestions: vec!["page_number".to_string()],
1001                valid_parameters: vec!["page_number".to_string(), "page_size".to_string()],
1002            }],
1003        });
1004
1005        let error_data: ErrorData = error.into();
1006        let error_json = serde_json::to_value(&error_data).unwrap();
1007
1008        // Check that error details are preserved
1009        assert!(error_json["data"].is_object(), "Should have data field");
1010        assert_eq!(
1011            error_json["data"]["type"].as_str(),
1012            Some("validation-errors"),
1013            "Should have validation-errors type"
1014        );
1015
1016        // Test Network error conversion
1017        let network_error = ToolCallError::Execution(ToolCallExecutionError::NetworkError {
1018            message: "SSL/TLS connection failed - certificate verification error".to_string(),
1019            category: NetworkErrorCategory::Connect,
1020        });
1021
1022        let error_data: ErrorData = network_error.into();
1023        let error_json = serde_json::to_value(&error_data).unwrap();
1024
1025        assert!(error_json["data"].is_object(), "Should have data field");
1026        assert_eq!(
1027            error_json["data"]["type"].as_str(),
1028            Some("network-error"),
1029            "Should have network-error type"
1030        );
1031        assert!(
1032            error_json["data"]["message"]
1033                .as_str()
1034                .unwrap()
1035                .contains("SSL/TLS"),
1036            "Should preserve error message"
1037        );
1038    }
1039
1040    #[test]
1041    fn test_find_similar_strings() {
1042        // Test basic similarity
1043        let known = vec!["page_size", "user_id", "status"];
1044        let suggestions = find_similar_strings("page_sixe", &known);
1045        assert_eq!(suggestions, vec!["page_size"]);
1046
1047        // Test no suggestions for very different string
1048        let suggestions = find_similar_strings("xyz123", &known);
1049        assert!(suggestions.is_empty());
1050
1051        // Test transposed characters
1052        let known = vec!["limit", "offset"];
1053        let suggestions = find_similar_strings("lmiit", &known);
1054        assert_eq!(suggestions, vec!["limit"]);
1055
1056        // Test missing character
1057        let known = vec!["project_id", "merge_request_id"];
1058        let suggestions = find_similar_strings("projct_id", &known);
1059        assert_eq!(suggestions, vec!["project_id"]);
1060
1061        // Test extra character
1062        let known = vec!["name", "email"];
1063        let suggestions = find_similar_strings("namee", &known);
1064        assert_eq!(suggestions, vec!["name"]);
1065    }
1066}