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: {0}")]
408    Json(#[from] serde_json::Error),
409    #[error(transparent)]
410    ToolCall(#[from] ToolCallError),
411    #[error("Tool not found: {0}")]
412    ToolNotFound(String),
413}
414
415impl From<ToolCallValidationError> for ErrorData {
416    fn from(err: ToolCallValidationError) -> Self {
417        match err {
418            ToolCallValidationError::ToolNotFound {
419                ref tool_name,
420                ref suggestions,
421            } => {
422                let data = if suggestions.is_empty() {
423                    None
424                } else {
425                    Some(json!({
426                        "suggestions": suggestions
427                    }))
428                };
429                ErrorData::new(
430                    ErrorCode(-32601),
431                    format!("Tool '{tool_name}' not found"),
432                    data,
433                )
434            }
435            ToolCallValidationError::InvalidParameters { ref violations } => {
436                // Include the full validation error details
437                let data = Some(json!({
438                    "type": "validation-errors",
439                    "violations": violations
440                }));
441                ErrorData::new(ErrorCode(-32602), err.to_string(), data)
442            }
443            ToolCallValidationError::RequestConstructionError { ref reason } => {
444                // Include construction error details
445                let data = Some(json!({
446                    "type": "request-construction-error",
447                    "reason": reason
448                }));
449                ErrorData::new(ErrorCode(-32602), err.to_string(), data)
450            }
451        }
452    }
453}
454
455impl From<ToolCallError> for ErrorData {
456    fn from(err: ToolCallError) -> Self {
457        match err {
458            ToolCallError::Validation(validation_err) => validation_err.into(),
459            ToolCallError::Execution(execution_err) => {
460                // Execution errors should not be converted to ErrorData
461                // They should be returned as CallToolResult with is_error: true
462                // But for backward compatibility, we'll convert them
463                match execution_err {
464                    ToolCallExecutionError::HttpError {
465                        status,
466                        ref message,
467                        ..
468                    } => {
469                        let data = Some(json!({
470                            "type": "http-error",
471                            "status": status,
472                            "message": message
473                        }));
474                        ErrorData::new(ErrorCode(-32000), execution_err.to_string(), data)
475                    }
476                    ToolCallExecutionError::NetworkError {
477                        ref message,
478                        ref category,
479                    } => {
480                        let data = Some(json!({
481                            "type": "network-error",
482                            "message": message,
483                            "category": category
484                        }));
485                        ErrorData::new(ErrorCode(-32000), execution_err.to_string(), data)
486                    }
487                    ToolCallExecutionError::ResponseParsingError { ref reason, .. } => {
488                        let data = Some(json!({
489                            "type": "response-parsing-error",
490                            "reason": reason
491                        }));
492                        ErrorData::new(ErrorCode(-32700), execution_err.to_string(), data)
493                    }
494                }
495            }
496        }
497    }
498}
499
500impl From<Error> for ErrorData {
501    fn from(err: Error) -> Self {
502        match err {
503            Error::Spec(msg) => ErrorData::new(
504                ErrorCode(-32700),
505                format!("OpenAPI spec error: {msg}"),
506                None,
507            ),
508            Error::Validation(msg) => {
509                ErrorData::new(ErrorCode(-32602), format!("Validation error: {msg}"), None)
510            }
511            Error::HttpRequest(e) => {
512                ErrorData::new(ErrorCode(-32000), format!("HTTP request failed: {e}"), None)
513            }
514            Error::Http(msg) => {
515                ErrorData::new(ErrorCode(-32000), format!("HTTP error: {msg}"), None)
516            }
517            Error::Json(e) => {
518                ErrorData::new(ErrorCode(-32700), format!("JSON parsing error: {e}"), None)
519            }
520            Error::ToolCall(e) => e.into(),
521            _ => ErrorData::new(ErrorCode(-32000), err.to_string(), None),
522        }
523    }
524}
525
526/// Error that can occur during tool execution
527#[derive(Debug, Error, Serialize)]
528#[serde(untagged)]
529pub enum ToolCallError {
530    /// Validation errors that occur before tool execution
531    #[error(transparent)]
532    Validation(#[from] ToolCallValidationError),
533
534    /// Execution errors that occur during tool execution
535    #[error(transparent)]
536    Execution(#[from] ToolCallExecutionError),
537}
538
539/// Error response structure for tool execution failures
540#[derive(Debug, Serialize, JsonSchema)]
541pub struct ErrorResponse {
542    /// Error information
543    pub error: ToolCallExecutionError,
544}
545
546/// Validation errors that occur before tool execution
547/// These return as Err(ErrorData) with MCP protocol error codes
548#[derive(Debug, Error, Serialize)]
549#[serde(tag = "type", rename_all = "kebab-case")]
550pub enum ToolCallValidationError {
551    /// Tool not found
552    #[error("Tool '{tool_name}' not found")]
553    #[serde(rename = "tool-not-found")]
554    ToolNotFound {
555        /// Name of the tool that was not found
556        tool_name: String,
557        /// Suggested tool names based on similarity
558        suggestions: Vec<String>,
559    },
560
561    /// Invalid parameters (unknown names, missing required, constraints)
562    #[error("{}", format_validation_errors(violations))]
563    #[serde(rename = "validation-errors")]
564    InvalidParameters {
565        /// List of validation errors
566        violations: Vec<ValidationError>,
567    },
568
569    /// Request construction failed (JSON serialization for body)
570    #[error("Failed to construct request: {reason}")]
571    #[serde(rename = "request-construction-error")]
572    RequestConstructionError {
573        /// Description of the construction failure
574        reason: String,
575    },
576}
577
578/// Execution errors that occur during tool execution
579/// These return as Ok(CallToolResult { is_error: true })
580#[derive(Debug, Error, Serialize, JsonSchema)]
581#[serde(tag = "type", rename_all = "kebab-case")]
582#[schemars(tag = "type", rename_all = "kebab-case")]
583pub enum ToolCallExecutionError {
584    /// HTTP error response from the API
585    #[error("HTTP {status} error: {message}")]
586    #[serde(rename = "http-error")]
587    HttpError {
588        /// HTTP status code
589        status: u16,
590        /// Error message or response body
591        message: String,
592        /// Optional structured error details from API
593        #[serde(skip_serializing_if = "Option::is_none")]
594        details: Option<Value>,
595    },
596
597    /// Network/connection failures
598    #[error("Network error: {message}")]
599    #[serde(rename = "network-error")]
600    NetworkError {
601        /// Description of the network failure
602        message: String,
603        /// Error category for better handling
604        category: NetworkErrorCategory,
605    },
606
607    /// Response parsing failed
608    #[error("Failed to parse response: {reason}")]
609    #[serde(rename = "response-parsing-error")]
610    ResponseParsingError {
611        /// Description of the parsing failure
612        reason: String,
613        /// Raw response body for debugging
614        #[serde(skip_serializing_if = "Option::is_none")]
615        raw_response: Option<String>,
616    },
617}
618
619impl ToolCallValidationError {
620    /// Create a ToolNotFound error with suggestions based on available tools
621    pub fn tool_not_found(tool_name: String, available_tools: &[&str]) -> Self {
622        let suggestions = find_similar_strings(&tool_name, available_tools);
623        Self::ToolNotFound {
624            tool_name,
625            suggestions,
626        }
627    }
628}
629
630impl ValidationError {
631    /// Create an InvalidParameter error with suggestions based on valid parameters
632    pub fn invalid_parameter(parameter: String, valid_parameters: &[String]) -> Self {
633        let valid_params_refs: Vec<&str> = valid_parameters.iter().map(|s| s.as_str()).collect();
634        let suggestions = find_similar_strings(&parameter, &valid_params_refs);
635        Self::InvalidParameter {
636            parameter,
637            suggestions,
638            valid_parameters: valid_parameters.to_vec(),
639        }
640    }
641}
642
643/// Network error categories for better error handling
644#[derive(Debug, Serialize, JsonSchema)]
645#[serde(rename_all = "kebab-case")]
646pub enum NetworkErrorCategory {
647    /// Request timeout
648    Timeout,
649    /// Connection error (DNS, refused, unreachable)
650    Connect,
651    /// Request construction/sending error
652    Request,
653    /// Response body error
654    Body,
655    /// Response decoding error
656    Decode,
657    /// Other network errors
658    Other,
659}
660
661#[cfg(test)]
662mod tests {
663    use super::*;
664    use insta::assert_json_snapshot;
665    use serde_json::json;
666
667    #[test]
668    fn test_tool_call_error_serialization_with_details() {
669        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
670            violations: vec![ValidationError::InvalidParameter {
671                parameter: "pet_id".to_string(),
672                suggestions: vec!["petId".to_string()],
673                valid_parameters: vec!["petId".to_string(), "timeout_seconds".to_string()],
674            }],
675        });
676
677        let serialized = serde_json::to_value(&error).unwrap();
678        assert_json_snapshot!(serialized);
679    }
680
681    #[test]
682    fn test_tool_call_error_serialization_without_details() {
683        let error = ToolCallError::Validation(ToolCallValidationError::ToolNotFound {
684            tool_name: "unknownTool".to_string(),
685            suggestions: vec![],
686        });
687
688        let serialized = serde_json::to_value(&error).unwrap();
689        assert_json_snapshot!(serialized);
690    }
691
692    #[test]
693    fn test_tool_call_error_serialization_with_suggestions() {
694        let error = ToolCallError::Validation(ToolCallValidationError::ToolNotFound {
695            tool_name: "getPetByID".to_string(),
696            suggestions: vec!["getPetById".to_string(), "getPetsByStatus".to_string()],
697        });
698
699        let serialized = serde_json::to_value(&error).unwrap();
700        assert_json_snapshot!(serialized);
701    }
702
703    #[test]
704    fn test_tool_call_error_multiple_suggestions() {
705        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
706            violations: vec![ValidationError::InvalidParameter {
707                parameter: "pet_i".to_string(),
708                suggestions: vec!["petId".to_string(), "petInfo".to_string()],
709                valid_parameters: vec![
710                    "petId".to_string(),
711                    "petInfo".to_string(),
712                    "timeout".to_string(),
713                ],
714            }],
715        });
716
717        let serialized = serde_json::to_value(&error).unwrap();
718        assert_json_snapshot!(serialized);
719    }
720
721    #[test]
722    fn test_tool_call_error_no_suggestions() {
723        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
724            violations: vec![ValidationError::InvalidParameter {
725                parameter: "completely_wrong".to_string(),
726                suggestions: vec![],
727                valid_parameters: vec!["petId".to_string(), "timeout".to_string()],
728            }],
729        });
730
731        let serialized = serde_json::to_value(&error).unwrap();
732        assert_json_snapshot!(serialized);
733    }
734
735    #[test]
736    fn test_tool_call_error_validation() {
737        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
738            violations: vec![ValidationError::MissingRequiredParameter {
739                parameter: "field".to_string(),
740                description: Some("Missing required field".to_string()),
741                expected_type: "string".to_string(),
742            }],
743        });
744        let serialized = serde_json::to_value(&error).unwrap();
745        assert_json_snapshot!(serialized);
746    }
747
748    #[test]
749    fn test_tool_call_error_validation_detailed() {
750        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
751            violations: vec![ValidationError::ConstraintViolation {
752                parameter: "age".to_string(),
753                message: "Parameter 'age' must be between 0 and 150".to_string(),
754                field_path: Some("age".to_string()),
755                actual_value: Some(Box::new(json!(200))),
756                expected_type: Some("integer".to_string()),
757                constraints: vec![
758                    ValidationConstraint::Minimum {
759                        value: 0.0,
760                        exclusive: false,
761                    },
762                    ValidationConstraint::Maximum {
763                        value: 150.0,
764                        exclusive: false,
765                    },
766                ],
767            }],
768        });
769
770        let serialized = serde_json::to_value(&error).unwrap();
771        assert_json_snapshot!(serialized);
772    }
773
774    #[test]
775    fn test_tool_call_error_validation_enum() {
776        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
777            violations: vec![ValidationError::ConstraintViolation {
778                parameter: "status".to_string(),
779                message: "Parameter 'status' must be one of: available, pending, sold".to_string(),
780                field_path: Some("status".to_string()),
781                actual_value: Some(Box::new(json!("unknown"))),
782                expected_type: Some("string".to_string()),
783                constraints: vec![ValidationConstraint::EnumValues {
784                    values: vec![json!("available"), json!("pending"), json!("sold")],
785                }],
786            }],
787        });
788
789        let serialized = serde_json::to_value(&error).unwrap();
790        assert_json_snapshot!(serialized);
791    }
792
793    #[test]
794    fn test_tool_call_error_validation_format() {
795        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
796            violations: vec![ValidationError::ConstraintViolation {
797                parameter: "email".to_string(),
798                message: "Invalid email format".to_string(),
799                field_path: Some("contact.email".to_string()),
800                actual_value: Some(Box::new(json!("not-an-email"))),
801                expected_type: Some("string".to_string()),
802                constraints: vec![ValidationConstraint::Format {
803                    format: "email".to_string(),
804                }],
805            }],
806        });
807
808        let serialized = serde_json::to_value(&error).unwrap();
809        assert_json_snapshot!(serialized);
810    }
811
812    #[test]
813    fn test_tool_call_error_http_error() {
814        let error = ToolCallError::Execution(ToolCallExecutionError::HttpError {
815            status: 404,
816            message: "Not found".to_string(),
817            details: None,
818        });
819        let serialized = serde_json::to_value(&error).unwrap();
820        assert_json_snapshot!(serialized);
821    }
822
823    #[test]
824    fn test_tool_call_error_http_request() {
825        let error = ToolCallError::Execution(ToolCallExecutionError::NetworkError {
826            message: "Connection timeout".to_string(),
827            category: NetworkErrorCategory::Timeout,
828        });
829        let serialized = serde_json::to_value(&error).unwrap();
830        assert_json_snapshot!(serialized);
831    }
832
833    #[test]
834    fn test_tool_call_error_json() {
835        let error = ToolCallError::Execution(ToolCallExecutionError::ResponseParsingError {
836            reason: "Invalid JSON".to_string(),
837            raw_response: None,
838        });
839        let serialized = serde_json::to_value(&error).unwrap();
840        assert_json_snapshot!(serialized);
841    }
842
843    #[test]
844    fn test_tool_call_error_request_construction() {
845        let error = ToolCallError::Validation(ToolCallValidationError::RequestConstructionError {
846            reason: "Invalid parameter location: body".to_string(),
847        });
848        let serialized = serde_json::to_value(&error).unwrap();
849        assert_json_snapshot!(serialized);
850    }
851
852    #[test]
853    fn test_error_response_serialization() {
854        let error = ToolCallExecutionError::HttpError {
855            status: 400,
856            message: "Bad Request".to_string(),
857            details: Some(json!({
858                "error": "Invalid parameter",
859                "parameter": "test_param"
860            })),
861        };
862
863        let response = ErrorResponse { error };
864        let serialized = serde_json::to_value(&response).unwrap();
865        assert_json_snapshot!(serialized);
866    }
867
868    #[test]
869    fn test_tool_call_error_validation_multiple_of() {
870        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
871            violations: vec![ValidationError::ConstraintViolation {
872                parameter: "price".to_string(),
873                message: "10.5 is not a multiple of 3".to_string(),
874                field_path: Some("price".to_string()),
875                actual_value: Some(Box::new(json!(10.5))),
876                expected_type: Some("number".to_string()),
877                constraints: vec![ValidationConstraint::MultipleOf { value: 3.0 }],
878            }],
879        });
880
881        let serialized = serde_json::to_value(&error).unwrap();
882        assert_json_snapshot!(serialized);
883    }
884
885    #[test]
886    fn test_tool_call_error_validation_min_items() {
887        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
888            violations: vec![ValidationError::ConstraintViolation {
889                parameter: "tags".to_string(),
890                message: "Array has 1 items but minimum is 2".to_string(),
891                field_path: Some("tags".to_string()),
892                actual_value: Some(Box::new(json!(["tag1"]))),
893                expected_type: Some("array".to_string()),
894                constraints: vec![ValidationConstraint::MinItems { value: 2 }],
895            }],
896        });
897
898        let serialized = serde_json::to_value(&error).unwrap();
899        assert_json_snapshot!(serialized);
900    }
901
902    #[test]
903    fn test_tool_call_error_validation_max_items() {
904        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
905            violations: vec![ValidationError::ConstraintViolation {
906                parameter: "categories".to_string(),
907                message: "Array has 4 items but maximum is 3".to_string(),
908                field_path: Some("categories".to_string()),
909                actual_value: Some(Box::new(json!(["a", "b", "c", "d"]))),
910                expected_type: Some("array".to_string()),
911                constraints: vec![ValidationConstraint::MaxItems { value: 3 }],
912            }],
913        });
914
915        let serialized = serde_json::to_value(&error).unwrap();
916        assert_json_snapshot!(serialized);
917    }
918
919    #[test]
920    fn test_tool_call_error_validation_unique_items() {
921        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
922            violations: vec![ValidationError::ConstraintViolation {
923                parameter: "numbers".to_string(),
924                message: "Array items [1, 2, 2, 3] are not unique".to_string(),
925                field_path: Some("numbers".to_string()),
926                actual_value: Some(Box::new(json!([1, 2, 2, 3]))),
927                expected_type: Some("array".to_string()),
928                constraints: vec![ValidationConstraint::UniqueItems],
929            }],
930        });
931
932        let serialized = serde_json::to_value(&error).unwrap();
933        assert_json_snapshot!(serialized);
934    }
935
936    #[test]
937    fn test_tool_call_error_validation_min_properties() {
938        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
939            violations: vec![ValidationError::ConstraintViolation {
940                parameter: "metadata".to_string(),
941                message: "Object has 2 properties but minimum is 3".to_string(),
942                field_path: Some("metadata".to_string()),
943                actual_value: Some(Box::new(json!({"name": "test", "version": "1.0"}))),
944                expected_type: Some("object".to_string()),
945                constraints: vec![ValidationConstraint::MinProperties { value: 3 }],
946            }],
947        });
948
949        let serialized = serde_json::to_value(&error).unwrap();
950        assert_json_snapshot!(serialized);
951    }
952
953    #[test]
954    fn test_tool_call_error_validation_max_properties() {
955        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
956            violations: vec![ValidationError::ConstraintViolation {
957                parameter: "config".to_string(),
958                message: "Object has 3 properties but maximum is 2".to_string(),
959                field_path: Some("config".to_string()),
960                actual_value: Some(Box::new(json!({"a": 1, "b": 2, "c": 3}))),
961                expected_type: Some("object".to_string()),
962                constraints: vec![ValidationConstraint::MaxProperties { value: 2 }],
963            }],
964        });
965
966        let serialized = serde_json::to_value(&error).unwrap();
967        assert_json_snapshot!(serialized);
968    }
969
970    #[test]
971    fn test_tool_call_error_validation_const() {
972        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
973            violations: vec![ValidationError::ConstraintViolation {
974                parameter: "environment".to_string(),
975                message: r#""staging" is not equal to const "production""#.to_string(),
976                field_path: Some("environment".to_string()),
977                actual_value: Some(Box::new(json!("staging"))),
978                expected_type: Some("string".to_string()),
979                constraints: vec![ValidationConstraint::ConstValue {
980                    value: json!("production"),
981                }],
982            }],
983        });
984
985        let serialized = serde_json::to_value(&error).unwrap();
986        assert_json_snapshot!(serialized);
987    }
988
989    #[test]
990    fn test_error_data_conversion_preserves_details() {
991        // Test InvalidParameter error conversion
992        let error = ToolCallError::Validation(ToolCallValidationError::InvalidParameters {
993            violations: vec![ValidationError::InvalidParameter {
994                parameter: "page".to_string(),
995                suggestions: vec!["page_number".to_string()],
996                valid_parameters: vec!["page_number".to_string(), "page_size".to_string()],
997            }],
998        });
999
1000        let error_data: ErrorData = error.into();
1001        let error_json = serde_json::to_value(&error_data).unwrap();
1002
1003        // Check that error details are preserved
1004        assert!(error_json["data"].is_object(), "Should have data field");
1005        assert_eq!(
1006            error_json["data"]["type"].as_str(),
1007            Some("validation-errors"),
1008            "Should have validation-errors type"
1009        );
1010
1011        // Test Network error conversion
1012        let network_error = ToolCallError::Execution(ToolCallExecutionError::NetworkError {
1013            message: "SSL/TLS connection failed - certificate verification error".to_string(),
1014            category: NetworkErrorCategory::Connect,
1015        });
1016
1017        let error_data: ErrorData = network_error.into();
1018        let error_json = serde_json::to_value(&error_data).unwrap();
1019
1020        assert!(error_json["data"].is_object(), "Should have data field");
1021        assert_eq!(
1022            error_json["data"]["type"].as_str(),
1023            Some("network-error"),
1024            "Should have network-error type"
1025        );
1026        assert!(
1027            error_json["data"]["message"]
1028                .as_str()
1029                .unwrap()
1030                .contains("SSL/TLS"),
1031            "Should preserve error message"
1032        );
1033    }
1034
1035    #[test]
1036    fn test_find_similar_strings() {
1037        // Test basic similarity
1038        let known = vec!["page_size", "user_id", "status"];
1039        let suggestions = find_similar_strings("page_sixe", &known);
1040        assert_eq!(suggestions, vec!["page_size"]);
1041
1042        // Test no suggestions for very different string
1043        let suggestions = find_similar_strings("xyz123", &known);
1044        assert!(suggestions.is_empty());
1045
1046        // Test transposed characters
1047        let known = vec!["limit", "offset"];
1048        let suggestions = find_similar_strings("lmiit", &known);
1049        assert_eq!(suggestions, vec!["limit"]);
1050
1051        // Test missing character
1052        let known = vec!["project_id", "merge_request_id"];
1053        let suggestions = find_similar_strings("projct_id", &known);
1054        assert_eq!(suggestions, vec!["project_id"]);
1055
1056        // Test extra character
1057        let known = vec!["name", "email"];
1058        let suggestions = find_similar_strings("namee", &known);
1059        assert_eq!(suggestions, vec!["name"]);
1060    }
1061}