Skip to main content

rmcp_openapi/
tool_generator.rs

1//! # OpenAPI to MCP Tool Generator with Reference Metadata Enhancement
2//!
3//! This module provides comprehensive tooling for converting OpenAPI 3.1 specifications
4//! into Model Context Protocol (MCP) tools with sophisticated reference metadata handling.
5//! The implementation follows OpenAPI 3.1 semantics to ensure contextual information
6//! takes precedence over generic schema documentation.
7//!
8//! ## Reference Metadata Enhancement Strategy
9//!
10//! ### Core Philosophy
11//!
12//! The OpenAPI 3.1 specification introduces reference metadata fields (`summary` and `description`)
13//! that can be attached to `$ref` objects. These fields serve a fundamentally different purpose
14//! than schema-level metadata:
15//!
16//! - **Reference Metadata**: Contextual, usage-specific information about how a schema is used
17//!   in a particular location within the API specification
18//! - **Schema Metadata**: General, reusable documentation about the schema definition itself
19//!
20//! This distinction is crucial for generating meaningful MCP tools that provide contextual
21//! information to AI assistants rather than generic schema documentation.
22//!
23//! ### Implementation Architecture
24//!
25//! The enhancement strategy is implemented through several coordinated components:
26//!
27//! #### 1. ReferenceMetadata Struct
28//! Central data structure that encapsulates OpenAPI 3.1 reference metadata fields and provides
29//! the core precedence logic through helper methods (`best_description()`, `summary()`).
30//!
31//! #### 2. Precedence Hierarchy Implementation
32//! All description enhancement follows the strict precedence hierarchy:
33//! 1. **Reference description** (highest) - Detailed contextual information
34//! 2. **Reference summary** (medium) - Brief contextual information
35//! 3. **Schema description** (lower) - General schema documentation
36//! 4. **Generated fallback** (lowest) - Auto-generated descriptive text
37//!
38//! #### 3. Context-Aware Enhancement Methods
39//! - `merge_with_description()`: General-purpose description merging with optional formatting
40//! - `enhance_parameter_description()`: Parameter-specific enhancement with name integration
41//! - Various schema conversion methods that apply reference metadata throughout tool generation
42//!
43//! ### Usage Throughout Tool Generation Pipeline
44//!
45//! The reference metadata enhancement strategy is applied systematically:
46//!
47//! #### Parameter Processing
48//! - Parameter schemas are enhanced with contextual information from parameter references
49//! - Parameter descriptions include contextual usage information rather than generic field docs
50//! - Special formatting ensures parameter names are clearly associated with contextual descriptions
51//!
52//! #### Request Body Processing
53//! - Request body schemas are enriched with operation-specific documentation
54//! - Content type handling preserves reference metadata through schema conversion
55//! - Complex nested schemas maintain reference context through recursive processing
56//!
57//! #### Response Processing
58//! - Response schemas are augmented with endpoint-specific information
59//! - Unified response structures include contextual descriptions in the response body schemas
60//! - Error handling maintains reference context for comprehensive tool documentation
61//!
62//! #### Tool Metadata Generation
63//! - Tool names, descriptions, and parameter schemas all benefit from reference metadata
64//! - Operation-level documentation is combined with reference-level context for comprehensive tool docs
65//! - Output schemas preserve contextual information for structured MCP responses
66//!
67//! ### Quality Assurance
68//!
69//! The implementation includes comprehensive safeguards:
70//!
71//! - **Precedence Consistency**: All enhancement methods follow identical precedence rules
72//! - **Backward Compatibility**: Systems without reference metadata continue to work with schema-level docs
73//! - **Fallback Robustness**: Multiple fallback levels ensure tools always have meaningful documentation
74//! - **Context Preservation**: Reference metadata is preserved through complex schema transformations
75//!
76//! ### Examples
77//!
78//! ```rust
79//! use rmcp_openapi::tool_generator::{ToolGenerator, ReferenceMetadata};
80//! use oas3::spec::Spec;
81//!
82//! // Reference metadata provides contextual information
83//! let ref_metadata = ReferenceMetadata::new(
84//!     Some("Store pet data".to_string()),      // contextual summary
85//!     Some("Pet information for inventory management".to_string()) // contextual description
86//! );
87//!
88//! // Enhancement follows precedence hierarchy
89//! let enhanced = ref_metadata.merge_with_description(
90//!     Some("Generic animal schema"), // schema description (lower priority)
91//!     false
92//! );
93//! // Result: "Pet information for inventory management" (reference description wins)
94//!
95//! // Parameter enhancement includes contextual formatting
96//! let param_desc = ref_metadata.enhance_parameter_description(
97//!     "petId",
98//!     Some("Database identifier")
99//! );
100//! // Result: "petId: Pet information for inventory management"
101//! ```
102//!
103//! This comprehensive approach ensures that MCP tools generated from OpenAPI specifications
104//! provide meaningful, contextual information to AI assistants rather than generic schema
105//! documentation, significantly improving the quality of human-AI interactions.
106
107use jsonschema::error::{TypeKind, ValidationErrorKind};
108use schemars::schema_for;
109use serde::{Serialize, Serializer};
110use serde_json::{Value, json};
111use std::collections::{BTreeMap, HashMap, HashSet};
112
113use crate::HttpClient;
114use crate::error::{
115    Error, ErrorResponse, ToolCallValidationError, ValidationConstraint, ValidationError,
116};
117use crate::tool::ToolMetadata;
118use oas3::spec::{
119    BooleanSchema, ObjectOrReference, ObjectSchema, Operation, Parameter, ParameterIn,
120    ParameterStyle, RequestBody, Response, Schema, SchemaType, SchemaTypeSet, Spec,
121};
122use tracing::{trace, warn};
123
124// Annotation key constants
125const X_LOCATION: &str = "x-location";
126const X_PARAMETER_LOCATION: &str = "x-parameter-location";
127const X_PARAMETER_REQUIRED: &str = "x-parameter-required";
128const X_CONTENT_TYPE: &str = "x-content-type";
129const X_ORIGINAL_NAME: &str = "x-original-name";
130const X_PARAMETER_EXPLODE: &str = "x-parameter-explode";
131const X_FILE_FIELDS: &str = "x-file-fields";
132
133/// Location type that extends ParameterIn with Body variant
134#[derive(Debug, Clone, Copy, PartialEq)]
135pub enum Location {
136    /// Standard OpenAPI parameter locations
137    Parameter(ParameterIn),
138    /// Request body location
139    Body,
140}
141
142impl Serialize for Location {
143    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
144    where
145        S: Serializer,
146    {
147        let str_value = match self {
148            Location::Parameter(param_in) => match param_in {
149                ParameterIn::Query => "query",
150                ParameterIn::Header => "header",
151                ParameterIn::Path => "path",
152                ParameterIn::Cookie => "cookie",
153            },
154            Location::Body => "body",
155        };
156        serializer.serialize_str(str_value)
157    }
158}
159
160/// Annotation types that can be applied to parameters and request bodies
161#[derive(Debug, Clone, PartialEq)]
162pub enum Annotation {
163    /// Location of the parameter or request body
164    Location(Location),
165    /// Whether a parameter is required
166    Required(bool),
167    /// Content type for request bodies
168    ContentType(String),
169    /// Original name before sanitization
170    OriginalName(String),
171    /// Parameter explode setting for arrays/objects
172    Explode(bool),
173    /// File fields in a multipart/form-data request body
174    FileFields(Vec<String>),
175}
176
177/// Collection of annotations that can be applied to schema objects
178#[derive(Debug, Clone, Default)]
179pub struct Annotations {
180    annotations: Vec<Annotation>,
181}
182
183impl Annotations {
184    /// Create a new empty Annotations collection
185    pub fn new() -> Self {
186        Self {
187            annotations: Vec::new(),
188        }
189    }
190
191    /// Add a location annotation
192    pub fn with_location(mut self, location: Location) -> Self {
193        self.annotations.push(Annotation::Location(location));
194        self
195    }
196
197    /// Add a required annotation
198    pub fn with_required(mut self, required: bool) -> Self {
199        self.annotations.push(Annotation::Required(required));
200        self
201    }
202
203    /// Add a content type annotation
204    pub fn with_content_type(mut self, content_type: String) -> Self {
205        self.annotations.push(Annotation::ContentType(content_type));
206        self
207    }
208
209    /// Add an original name annotation
210    pub fn with_original_name(mut self, original_name: String) -> Self {
211        self.annotations
212            .push(Annotation::OriginalName(original_name));
213        self
214    }
215
216    /// Add an explode annotation
217    pub fn with_explode(mut self, explode: bool) -> Self {
218        self.annotations.push(Annotation::Explode(explode));
219        self
220    }
221
222    /// Add file fields annotation for multipart/form-data requests
223    pub fn with_file_fields(mut self, file_fields: Vec<String>) -> Self {
224        self.annotations.push(Annotation::FileFields(file_fields));
225        self
226    }
227}
228
229impl Serialize for Annotations {
230    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
231    where
232        S: Serializer,
233    {
234        use serde::ser::SerializeMap;
235
236        let mut map = serializer.serialize_map(Some(self.annotations.len()))?;
237
238        for annotation in &self.annotations {
239            match annotation {
240                Annotation::Location(location) => {
241                    // Determine the key based on the location type
242                    let key = match location {
243                        Location::Parameter(param_in) => match param_in {
244                            ParameterIn::Header | ParameterIn::Cookie => X_LOCATION,
245                            _ => X_PARAMETER_LOCATION,
246                        },
247                        Location::Body => X_LOCATION,
248                    };
249                    map.serialize_entry(key, &location)?;
250
251                    // For parameters, also add x-parameter-location
252                    if let Location::Parameter(_) = location {
253                        map.serialize_entry(X_PARAMETER_LOCATION, &location)?;
254                    }
255                }
256                Annotation::Required(required) => {
257                    map.serialize_entry(X_PARAMETER_REQUIRED, required)?;
258                }
259                Annotation::ContentType(content_type) => {
260                    map.serialize_entry(X_CONTENT_TYPE, content_type)?;
261                }
262                Annotation::OriginalName(original_name) => {
263                    map.serialize_entry(X_ORIGINAL_NAME, original_name)?;
264                }
265                Annotation::Explode(explode) => {
266                    map.serialize_entry(X_PARAMETER_EXPLODE, explode)?;
267                }
268                Annotation::FileFields(file_fields) => {
269                    map.serialize_entry(X_FILE_FIELDS, file_fields)?;
270                }
271            }
272        }
273
274        map.end()
275    }
276}
277
278/// Sanitize a property name to match MCP requirements
279///
280/// MCP requires property keys to match the pattern `^[a-zA-Z0-9_.-]{1,64}$`
281/// This function:
282/// - Replaces invalid characters with underscores
283/// - Limits the length to 64 characters
284/// - Ensures the name doesn't start with a number
285/// - Ensures the result is not empty
286fn sanitize_property_name(name: &str) -> String {
287    // Replace invalid characters with underscores
288    let sanitized = name
289        .chars()
290        .map(|c| match c {
291            'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '.' | '-' => c,
292            _ => '_',
293        })
294        .take(64)
295        .collect::<String>();
296
297    // Collapse consecutive underscores into a single underscore
298    let mut collapsed = String::with_capacity(sanitized.len());
299    let mut prev_was_underscore = false;
300
301    for ch in sanitized.chars() {
302        if ch == '_' {
303            if !prev_was_underscore {
304                collapsed.push(ch);
305            }
306            prev_was_underscore = true;
307        } else {
308            collapsed.push(ch);
309            prev_was_underscore = false;
310        }
311    }
312
313    // Trim trailing underscores
314    let trimmed = collapsed.trim_end_matches('_');
315
316    // Ensure not empty and doesn't start with a number
317    if trimmed.is_empty() || trimmed.chars().next().unwrap_or('0').is_numeric() {
318        format!("param_{trimmed}")
319    } else {
320        trimmed.to_string()
321    }
322}
323
324/// Metadata extracted from OpenAPI 3.1 reference objects for MCP tool generation
325///
326/// This struct encapsulates the OpenAPI 3.1 reference metadata fields (summary and description)
327/// that provide contextual, usage-specific documentation for referenced schema objects.
328/// It implements the proper precedence hierarchy as defined by the OpenAPI 3.1 specification.
329///
330/// ## OpenAPI 3.1 Reference Metadata Semantics
331///
332/// In OpenAPI 3.1, reference objects can contain additional metadata fields:
333/// ```yaml
334/// $ref: '#/components/schemas/Pet'
335/// summary: Pet information for store operations
336/// description: Detailed pet data including status and ownership
337/// ```
338///
339/// This metadata serves a different semantic purpose than schema definitions:
340/// - **Reference metadata**: Provides contextual, usage-specific information about how
341///   a schema is used in a particular location within the API specification
342/// - **Schema metadata**: Provides general, reusable documentation about the schema itself
343///
344/// ## Precedence Hierarchy
345///
346/// Following OpenAPI 3.1 semantics, this implementation enforces the precedence:
347/// 1. **Reference description** (highest priority) - Contextual usage description
348/// 2. **Reference summary** (medium priority) - Contextual usage summary
349/// 3. **Schema description** (lowest priority) - General schema description
350/// 4. **Generated fallback** (last resort) - Auto-generated descriptive text
351///
352/// This hierarchy ensures that human-authored contextual information takes precedence
353/// over generic schema documentation, providing more meaningful tool descriptions
354/// for AI assistants consuming the MCP interface.
355///
356/// ## Usage in Tool Generation
357///
358/// Reference metadata is used throughout the tool generation process:
359/// - **Parameter descriptions**: Enhanced with contextual information about parameter usage
360/// - **Request body schemas**: Enriched with operation-specific documentation
361/// - **Response schemas**: Augmented with endpoint-specific response information
362/// - **Tool descriptions**: Combined with operation metadata for comprehensive tool documentation
363///
364/// ## Example
365///
366/// ```rust
367/// use rmcp_openapi::tool_generator::ReferenceMetadata;
368///
369/// let ref_meta = ReferenceMetadata::new(
370///     Some("Pet data".to_string()),
371///     Some("Complete pet information including health records".to_string())
372/// );
373///
374/// // Reference description takes precedence
375/// assert_eq!(
376///     ref_meta.best_description(),
377///     Some("Complete pet information including health records")
378/// );
379///
380/// // Merge with existing schema description (reference wins)
381/// let enhanced = ref_meta.merge_with_description(
382///     Some("Generic pet schema"),
383///     false
384/// );
385/// assert_eq!(enhanced, Some("Complete pet information including health records".to_string()));
386/// ```
387#[derive(Debug, Clone, Default)]
388pub struct ReferenceMetadata {
389    /// Optional contextual summary from the OpenAPI 3.1 reference object
390    ///
391    /// This field captures the `summary` property from a reference object,
392    /// providing a brief, contextual description of how the referenced schema
393    /// is used in this specific location. Takes precedence over schema summaries
394    /// when available.
395    pub summary: Option<String>,
396
397    /// Optional contextual description from the OpenAPI 3.1 reference object
398    ///
399    /// This field captures the `description` property from a reference object,
400    /// providing detailed, contextual documentation about how the referenced schema
401    /// is used in this specific location. This is the highest priority description
402    /// in the precedence hierarchy and overrides any schema-level descriptions.
403    pub description: Option<String>,
404}
405
406impl ReferenceMetadata {
407    /// Create new reference metadata from optional summary and description
408    pub fn new(summary: Option<String>, description: Option<String>) -> Self {
409        Self {
410            summary,
411            description,
412        }
413    }
414
415    /// Check if this metadata contains any useful information
416    pub fn is_empty(&self) -> bool {
417        self.summary.is_none() && self.description.is_none()
418    }
419
420    /// Get the best available description from reference metadata
421    ///
422    /// This helper method implements the core fallback logic for selecting the most
423    /// appropriate description from the available reference metadata fields.
424    /// It follows OpenAPI 3.1 semantics where detailed descriptions take precedence
425    /// over brief summaries.
426    ///
427    /// ## Selection Logic
428    ///
429    /// 1. **Primary**: Returns reference description if available
430    ///    - Source: `$ref.description` field
431    ///    - Rationale: Detailed contextual information is most valuable
432    /// 2. **Fallback**: Returns reference summary if no description available
433    ///    - Source: `$ref.summary` field
434    ///    - Rationale: Brief context is better than no context
435    /// 3. **None**: Returns `None` if neither field is available
436    ///    - Behavior: Caller must handle absence of reference metadata
437    ///
438    /// ## Usage in Precedence Hierarchy
439    ///
440    /// This method provides the first-priority input for all description enhancement
441    /// methods (`merge_with_description()`, `enhance_parameter_description()`).
442    /// It encapsulates the "reference description OR reference summary" logic
443    /// that forms the top of the precedence hierarchy.
444    ///
445    /// ## Examples
446    ///
447    /// ```rust
448    /// use rmcp_openapi::tool_generator::ReferenceMetadata;
449    ///
450    /// // Description takes precedence over summary
451    /// let both = ReferenceMetadata::new(
452    ///     Some("Brief summary".to_string()),
453    ///     Some("Detailed description".to_string())
454    /// );
455    /// assert_eq!(both.best_description(), Some("Detailed description"));
456    ///
457    /// // Summary used when no description
458    /// let summary_only = ReferenceMetadata::new(Some("Brief summary".to_string()), None);
459    /// assert_eq!(summary_only.best_description(), Some("Brief summary"));
460    ///
461    /// // None when no reference metadata
462    /// let empty = ReferenceMetadata::new(None, None);
463    /// assert_eq!(empty.best_description(), None);
464    /// ```
465    ///
466    /// # Returns
467    /// * `Some(&str)` - Best available description (description OR summary)
468    /// * `None` - No reference metadata available
469    pub fn best_description(&self) -> Option<&str> {
470        self.description.as_deref().or(self.summary.as_deref())
471    }
472
473    /// Get the reference summary for targeted access
474    ///
475    /// This helper method provides direct access to the reference summary field
476    /// without fallback logic. It's used when summary-specific behavior is needed,
477    /// such as in `merge_with_description()` for the special prepend functionality.
478    ///
479    /// ## Usage Scenarios
480    ///
481    /// 1. **Summary-specific operations**: When caller needs to distinguish between
482    ///    summary and description for special formatting (e.g., prepend behavior)
483    /// 2. **Metadata inspection**: When caller wants to check what summary information
484    ///    is available without fallback to description
485    /// 3. **Pattern matching**: Used in complex precedence logic where summary
486    ///    and description need separate handling
487    ///
488    /// ## Relationship with best_description()
489    ///
490    /// Unlike `best_description()` which implements fallback logic, this method
491    /// provides raw access to just the summary field. This enables fine-grained
492    /// control in precedence implementations.
493    ///
494    /// ## Examples
495    ///
496    /// ```rust
497    /// use rmcp_openapi::tool_generator::ReferenceMetadata;
498    ///
499    /// let with_summary = ReferenceMetadata::new(Some("API token".to_string()), None);
500    ///
501    /// // Direct summary access
502    /// assert_eq!(with_summary.summary(), Some("API token"));
503    ///
504    /// // Compare with best_description (same result when only summary available)
505    /// assert_eq!(with_summary.best_description(), Some("API token"));
506    ///
507    /// // Different behavior when both are present
508    /// let both = ReferenceMetadata::new(
509    ///     Some("Token".to_string()),      // summary
510    ///     Some("Auth token".to_string())  // description
511    /// );
512    /// assert_eq!(both.summary(), Some("Token"));            // Just summary
513    /// assert_eq!(both.best_description(), Some("Auth token")); // Prefers description
514    /// ```
515    ///
516    /// # Returns
517    /// * `Some(&str)` - Reference summary if available
518    /// * `None` - No summary in reference metadata
519    pub fn summary(&self) -> Option<&str> {
520        self.summary.as_deref()
521    }
522
523    /// Merge reference metadata with existing description using OpenAPI 3.1 precedence rules
524    ///
525    /// This method implements the sophisticated fallback mechanism for combining contextual
526    /// reference metadata with general schema descriptions. It follows the OpenAPI 3.1
527    /// semantic hierarchy where contextual information takes precedence over generic
528    /// schema documentation.
529    ///
530    /// ## Fallback Mechanism
531    ///
532    /// The method implements a strict precedence hierarchy:
533    ///
534    /// ### Priority 1: Reference Description (Highest)
535    /// - **Source**: `$ref.description` field from OpenAPI 3.1 reference object
536    /// - **Semantic**: Contextual, usage-specific description for this particular reference
537    /// - **Behavior**: Always takes precedence, ignoring all other descriptions
538    /// - **Rationale**: Human-authored contextual information is most valuable for tool users
539    ///
540    /// ### Priority 2: Reference Summary (Medium)
541    /// - **Source**: `$ref.summary` field from OpenAPI 3.1 reference object
542    /// - **Semantic**: Brief contextual summary for this particular reference
543    /// - **Behavior**: Used when no reference description is available
544    /// - **Special Case**: When `prepend_summary=true` and existing description differs,
545    ///   combines summary with existing description using double newline separator
546    ///
547    /// ### Priority 3: Schema Description (Lower)
548    /// - **Source**: `description` field from the resolved schema object
549    /// - **Semantic**: General, reusable documentation about the schema itself
550    /// - **Behavior**: Only used as fallback when no reference metadata is available
551    /// - **Rationale**: Generic schema docs are less valuable than contextual reference docs
552    ///
553    /// ### Priority 4: No Description (Lowest)
554    /// - **Behavior**: Returns `None` when no description sources are available
555    /// - **Impact**: Caller should provide appropriate fallback behavior
556    ///
557    /// ## Implementation Details
558    ///
559    /// The method uses pattern matching on a tuple of `(reference_description, reference_summary, schema_description)`
560    /// to implement the precedence hierarchy efficiently. This ensures all possible combinations
561    /// are handled explicitly and correctly.
562    ///
563    /// ## Examples
564    ///
565    /// ```rust
566    /// use rmcp_openapi::tool_generator::ReferenceMetadata;
567    ///
568    /// let ref_meta = ReferenceMetadata::new(
569    ///     Some("API Key".to_string()), // summary
570    ///     Some("Authentication token for secure API access".to_string()) // description
571    /// );
572    ///
573    /// // Reference description wins (Priority 1)
574    /// assert_eq!(
575    ///     ref_meta.merge_with_description(Some("Generic token schema"), false),
576    ///     Some("Authentication token for secure API access".to_string())
577    /// );
578    ///
579    /// // Reference summary used when no description (Priority 2)
580    /// let summary_only = ReferenceMetadata::new(Some("API Key".to_string()), None);
581    /// assert_eq!(
582    ///     summary_only.merge_with_description(Some("Generic token schema"), false),
583    ///     Some("API Key".to_string())
584    /// );
585    ///
586    /// // Schema description as fallback (Priority 3)
587    /// let empty_ref = ReferenceMetadata::new(None, None);
588    /// assert_eq!(
589    ///     empty_ref.merge_with_description(Some("Generic token schema"), false),
590    ///     Some("Generic token schema".to_string())
591    /// );
592    ///
593    /// // Summary takes precedence via best_description() (no prepending when summary is available)
594    /// assert_eq!(
595    ///     summary_only.merge_with_description(Some("Different description"), true),
596    ///     Some("API Key".to_string())
597    /// );
598    /// ```
599    ///
600    /// # Arguments
601    /// * `existing_desc` - Existing description from the resolved schema object
602    /// * `prepend_summary` - Whether to prepend reference summary to existing description
603    ///   when no reference description is available (used for special formatting cases)
604    ///
605    /// # Returns
606    /// * `Some(String)` - Enhanced description following precedence hierarchy
607    /// * `None` - No description sources available (caller should handle fallback)
608    pub fn merge_with_description(
609        &self,
610        existing_desc: Option<&str>,
611        prepend_summary: bool,
612    ) -> Option<String> {
613        match (self.best_description(), self.summary(), existing_desc) {
614            // Reference description takes precedence (OpenAPI 3.1 semantics: contextual > general)
615            (Some(ref_desc), _, _) => Some(ref_desc.to_string()),
616
617            // No reference description, use reference summary if available
618            (None, Some(ref_summary), Some(existing)) if prepend_summary => {
619                if ref_summary != existing {
620                    Some(format!("{}\n\n{}", ref_summary, existing))
621                } else {
622                    Some(existing.to_string())
623                }
624            }
625            (None, Some(ref_summary), _) => Some(ref_summary.to_string()),
626
627            // Fallback to existing schema description only if no reference metadata
628            (None, None, Some(existing)) => Some(existing.to_string()),
629
630            // No useful information available
631            (None, None, None) => None,
632        }
633    }
634
635    /// Create enhanced parameter descriptions following OpenAPI 3.1 precedence hierarchy
636    ///
637    /// This method generates parameter descriptions specifically tailored for MCP tools
638    /// by combining reference metadata with parameter names using the OpenAPI 3.1
639    /// precedence rules. Unlike general description merging, this method always
640    /// includes the parameter name for clarity in tool interfaces.
641    ///
642    /// ## Parameter Description Hierarchy
643    ///
644    /// The method follows the same precedence hierarchy as `merge_with_description()` but
645    /// formats the output specifically for parameter documentation:
646    ///
647    /// ### Priority 1: Reference Description (Highest)
648    /// - **Format**: `"{param_name}: {reference_description}"`
649    /// - **Source**: `$ref.description` field from OpenAPI 3.1 reference object
650    /// - **Example**: `"petId: Unique identifier for the pet in the store"`
651    /// - **Behavior**: Always used when available, providing contextual parameter meaning
652    ///
653    /// ### Priority 2: Reference Summary (Medium)
654    /// - **Format**: `"{param_name}: {reference_summary}"`
655    /// - **Source**: `$ref.summary` field from OpenAPI 3.1 reference object
656    /// - **Example**: `"petId: Pet identifier"`
657    /// - **Behavior**: Used when no reference description is available
658    ///
659    /// ### Priority 3: Schema Description (Lower)
660    /// - **Format**: `"{existing_description}"` (without parameter name prefix)
661    /// - **Source**: `description` field from the parameter's schema object
662    /// - **Example**: `"A unique identifier for database entities"`
663    /// - **Behavior**: Used only when no reference metadata is available
664    /// - **Note**: Does not prepend parameter name to preserve original schema documentation
665    ///
666    /// ### Priority 4: Generated Fallback (Lowest)
667    /// - **Format**: `"{param_name} parameter"`
668    /// - **Source**: Auto-generated from parameter name
669    /// - **Example**: `"petId parameter"`
670    /// - **Behavior**: Always provides a description, ensuring tools have meaningful parameter docs
671    ///
672    /// ## Design Rationale
673    ///
674    /// This method addresses the specific needs of MCP tool parameter documentation:
675    ///
676    /// 1. **Contextual Clarity**: Reference metadata provides usage-specific context
677    ///    rather than generic schema documentation
678    /// 2. **Parameter Name Integration**: Higher priority items include parameter names
679    ///    for immediate clarity in tool interfaces
680    /// 3. **Guaranteed Output**: Always returns a description, ensuring no parameter
681    ///    lacks documentation in the generated MCP tools
682    /// 4. **Semantic Formatting**: Different formatting for different priority levels
683    ///    maintains consistency while respecting original schema documentation
684    ///
685    /// ## Examples
686    ///
687    /// ```rust
688    /// use rmcp_openapi::tool_generator::ReferenceMetadata;
689    ///
690    /// // Reference description takes precedence
691    /// let with_desc = ReferenceMetadata::new(
692    ///     Some("Pet ID".to_string()),
693    ///     Some("Unique identifier for pet in the store".to_string())
694    /// );
695    /// assert_eq!(
696    ///     with_desc.enhance_parameter_description("petId", Some("Generic ID field")),
697    ///     Some("petId: Unique identifier for pet in the store".to_string())
698    /// );
699    ///
700    /// // Reference summary when no description
701    /// let with_summary = ReferenceMetadata::new(Some("Pet ID".to_string()), None);
702    /// assert_eq!(
703    ///     with_summary.enhance_parameter_description("petId", Some("Generic ID field")),
704    ///     Some("petId: Pet ID".to_string())
705    /// );
706    ///
707    /// // Schema description fallback (no name prefix)
708    /// let empty_ref = ReferenceMetadata::new(None, None);
709    /// assert_eq!(
710    ///     empty_ref.enhance_parameter_description("petId", Some("Generic ID field")),
711    ///     Some("Generic ID field".to_string())
712    /// );
713    ///
714    /// // Generated fallback ensures always returns description
715    /// assert_eq!(
716    ///     empty_ref.enhance_parameter_description("petId", None),
717    ///     Some("petId parameter".to_string())
718    /// );
719    /// ```
720    pub fn enhance_parameter_description(
721        &self,
722        param_name: &str,
723        existing_desc: Option<&str>,
724    ) -> Option<String> {
725        match (self.best_description(), self.summary(), existing_desc) {
726            // Reference description takes precedence (OpenAPI 3.1 semantics: contextual > general)
727            (Some(ref_desc), _, _) => Some(format!("{}: {}", param_name, ref_desc)),
728
729            // No reference description, use reference summary if available
730            (None, Some(ref_summary), _) => Some(format!("{}: {}", param_name, ref_summary)),
731
732            // Fallback to existing schema description only if no reference metadata
733            (None, None, Some(existing)) => Some(existing.to_string()),
734
735            // No information available - generate contextual description
736            (None, None, None) => Some(format!("{} parameter", param_name)),
737        }
738    }
739}
740
741/// Tool generator for creating MCP tools from `OpenAPI` operations
742pub struct ToolGenerator;
743
744impl ToolGenerator {
745    /// Generate tool metadata from an `OpenAPI` operation
746    ///
747    /// # Errors
748    ///
749    /// Returns an error if the operation cannot be converted to tool metadata
750    pub fn generate_tool_metadata(
751        operation: &Operation,
752        method: String,
753        path: String,
754        spec: &Spec,
755        skip_tool_description: bool,
756        skip_parameter_descriptions: bool,
757        parameter_examples_in_description: bool,
758    ) -> Result<ToolMetadata, Error> {
759        let name = operation.operation_id.clone().unwrap_or_else(|| {
760            format!(
761                "{}_{}",
762                method,
763                path.replace('/', "_").replace(['{', '}'], "")
764            )
765        });
766
767        // Generate parameter schema first so we can include it in description
768        let (parameters, parameter_mappings) = Self::generate_parameter_schema(
769            &operation.parameters,
770            &method,
771            &operation.request_body,
772            spec,
773            skip_parameter_descriptions,
774            parameter_examples_in_description,
775        )?;
776
777        // Build description from summary, description, and parameters
778        let description =
779            (!skip_tool_description).then(|| Self::build_description(operation, &method, &path));
780
781        // Extract output schema from responses (already returns wrapped Value)
782        let output_schema = Self::extract_output_schema(&operation.responses, spec)?;
783
784        Ok(ToolMetadata {
785            name,
786            title: operation.summary.clone(),
787            description,
788            parameters,
789            output_schema,
790            method,
791            path,
792            security: None, // TODO: Extract security requirements from OpenAPI spec
793            parameter_mappings,
794        })
795    }
796
797    /// Generate OpenApiTool instances from tool metadata with HTTP configuration
798    ///
799    /// # Errors
800    ///
801    /// Returns an error if any OpenApiTool cannot be created
802    pub fn generate_openapi_tools(
803        tools_metadata: Vec<ToolMetadata>,
804        base_url: Option<url::Url>,
805        default_headers: Option<reqwest::header::HeaderMap>,
806        insecure: bool,
807    ) -> Result<Vec<crate::tool::Tool>, Error> {
808        let mut openapi_tools = Vec::with_capacity(tools_metadata.len());
809
810        let mut http_client = HttpClient::new().with_insecure(insecure);
811
812        if let Some(url) = base_url {
813            http_client = http_client.with_base_url(url)?;
814        }
815
816        if let Some(headers) = default_headers {
817            http_client = http_client.with_default_headers(headers);
818        }
819
820        for metadata in tools_metadata {
821            let tool = crate::tool::Tool::new(metadata, http_client.clone())?;
822            openapi_tools.push(tool);
823        }
824
825        Ok(openapi_tools)
826    }
827
828    /// Build a comprehensive description for the tool
829    fn build_description(operation: &Operation, method: &str, path: &str) -> String {
830        match (&operation.summary, &operation.description) {
831            (Some(summary), Some(desc)) => {
832                format!(
833                    "{}\n\n{}\n\nEndpoint: {} {}",
834                    summary,
835                    desc,
836                    method.to_uppercase(),
837                    path
838                )
839            }
840            (Some(summary), None) => {
841                format!(
842                    "{}\n\nEndpoint: {} {}",
843                    summary,
844                    method.to_uppercase(),
845                    path
846                )
847            }
848            (None, Some(desc)) => {
849                format!("{}\n\nEndpoint: {} {}", desc, method.to_uppercase(), path)
850            }
851            (None, None) => {
852                format!("API endpoint: {} {}", method.to_uppercase(), path)
853            }
854        }
855    }
856
857    /// Extract output schema from OpenAPI responses
858    ///
859    /// Prioritizes successful response codes (2XX) and returns the first found schema
860    fn extract_output_schema(
861        responses: &Option<BTreeMap<String, ObjectOrReference<Response>>>,
862        spec: &Spec,
863    ) -> Result<Option<Value>, Error> {
864        let responses = match responses {
865            Some(r) => r,
866            None => return Ok(None),
867        };
868        // Priority order for response codes to check
869        let priority_codes = vec![
870            "200",     // OK
871            "201",     // Created
872            "202",     // Accepted
873            "203",     // Non-Authoritative Information
874            "204",     // No Content (will have no schema)
875            "2XX",     // Any 2XX response
876            "default", // Default response
877        ];
878
879        for status_code in priority_codes {
880            if let Some(response_or_ref) = responses.get(status_code) {
881                // Resolve reference if needed
882                let response = match response_or_ref {
883                    ObjectOrReference::Object(response) => response,
884                    ObjectOrReference::Ref {
885                        ref_path,
886                        summary,
887                        description,
888                    } => {
889                        // Response references are not fully resolvable yet (would need resolve_response_reference)
890                        // But we can use the reference metadata to create a basic response schema
891                        let ref_metadata =
892                            ReferenceMetadata::new(summary.clone(), description.clone());
893
894                        if let Some(ref_desc) = ref_metadata.best_description() {
895                            // Create a unified response schema with reference description
896                            let response_schema = json!({
897                                "type": "object",
898                                "description": "Unified response structure with success and error variants",
899                                "properties": {
900                                    "status_code": {
901                                        "type": "integer",
902                                        "description": "HTTP status code"
903                                    },
904                                    "body": {
905                                        "type": "object",
906                                        "description": ref_desc,
907                                        "additionalProperties": true
908                                    }
909                                },
910                                "required": ["status_code", "body"]
911                            });
912
913                            trace!(
914                                reference_path = %ref_path,
915                                reference_description = %ref_desc,
916                                "Created response schema using reference metadata"
917                            );
918
919                            return Ok(Some(response_schema));
920                        }
921
922                        // No useful metadata, continue to next response
923                        continue;
924                    }
925                };
926
927                // Skip 204 No Content responses as they shouldn't have a body
928                if status_code == "204" {
929                    continue;
930                }
931
932                // Check if response has content
933                if !response.content.is_empty() {
934                    let content = &response.content;
935                    // Look for JSON content type
936                    let json_media_types = vec![
937                        "application/json",
938                        "application/ld+json",
939                        "application/vnd.api+json",
940                    ];
941
942                    for media_type_str in json_media_types {
943                        if let Some(media_type) = content.get(media_type_str)
944                            && let Some(schema_or_ref) = &media_type.schema
945                        {
946                            // Wrap the schema with success/error structure
947                            let wrapped_schema = Self::wrap_output_schema(schema_or_ref, spec)?;
948                            return Ok(Some(wrapped_schema));
949                        }
950                    }
951
952                    // If no JSON media type found, try any media type with a schema
953                    for media_type in content.values() {
954                        if let Some(schema_or_ref) = &media_type.schema {
955                            // Wrap the schema with success/error structure
956                            let wrapped_schema = Self::wrap_output_schema(schema_or_ref, spec)?;
957                            return Ok(Some(wrapped_schema));
958                        }
959                    }
960                }
961            }
962        }
963
964        // No response schema found
965        Ok(None)
966    }
967
968    /// Convert an OpenAPI Schema to JSON Schema format
969    ///
970    /// This is the unified converter for both input and output schemas.
971    /// It handles all OpenAPI schema types and converts them to JSON Schema draft-07 format.
972    ///
973    /// # Arguments
974    /// * `schema` - The OpenAPI Schema to convert
975    /// * `spec` - The full OpenAPI specification for resolving references
976    /// * `visited` - Set of visited references to prevent infinite recursion
977    fn convert_schema_to_json_schema(
978        schema: &Schema,
979        spec: &Spec,
980        visited: &mut HashSet<String>,
981    ) -> Result<Value, Error> {
982        match schema {
983            Schema::Object(obj_schema_or_ref) => match obj_schema_or_ref.as_ref() {
984                ObjectOrReference::Object(obj_schema) => {
985                    Self::convert_object_schema_to_json_schema(obj_schema, spec, visited)
986                }
987                ObjectOrReference::Ref { ref_path, .. } => {
988                    // Restore the full pre-conversion `visited` snapshot so the same
989                    // schema can be referenced again elsewhere (non-circular reuse is
990                    // valid). Removing only `ref_path` itself would leak the
991                    // intermediate hops of an alias ref chain — see
992                    // `convert_member_schema`.
993                    let snapshot = visited.clone();
994                    let result =
995                        Self::resolve_reference(ref_path, spec, visited).and_then(|resolved| {
996                            Self::convert_object_schema_to_json_schema(&resolved, spec, visited)
997                        });
998                    *visited = snapshot;
999                    result
1000                }
1001            },
1002            Schema::Boolean(bool_schema) => {
1003                // Boolean schemas in OpenAPI: true allows any value, false allows no value
1004                if bool_schema.0 {
1005                    Ok(json!({})) // Empty schema allows anything
1006                } else {
1007                    Ok(json!({"not": {}})) // Schema that matches nothing
1008                }
1009            }
1010        }
1011    }
1012
1013    /// Convert ObjectSchema to JSON Schema format
1014    ///
1015    /// This is the core converter that handles all schema types and properties.
1016    /// It processes object properties, arrays, primitives, and all OpenAPI schema attributes.
1017    ///
1018    /// # Arguments
1019    /// * `obj_schema` - The OpenAPI ObjectSchema to convert
1020    /// * `spec` - The full OpenAPI specification for resolving references
1021    /// * `visited` - Set of visited references to prevent infinite recursion
1022    fn convert_object_schema_to_json_schema(
1023        obj_schema: &ObjectSchema,
1024        spec: &Spec,
1025        visited: &mut HashSet<String>,
1026    ) -> Result<Value, Error> {
1027        let mut schema_obj = serde_json::Map::new();
1028
1029        // Add type if specified
1030        if let Some(schema_type) = &obj_schema.schema_type {
1031            match schema_type {
1032                SchemaTypeSet::Single(single_type) => {
1033                    schema_obj.insert(
1034                        "type".to_string(),
1035                        json!(Self::schema_type_to_string(single_type)),
1036                    );
1037                }
1038                SchemaTypeSet::Multiple(type_set) => {
1039                    let types: Vec<String> =
1040                        type_set.iter().map(Self::schema_type_to_string).collect();
1041                    schema_obj.insert("type".to_string(), json!(types));
1042                }
1043            }
1044        }
1045
1046        // Add description if present
1047        if let Some(desc) = &obj_schema.description {
1048            schema_obj.insert("description".to_string(), json!(desc));
1049        }
1050
1051        // Handle allOf composition: convert each member and deep-merge it into this
1052        // schema. Without this, any subschema whose only content is `allOf` (e.g. each
1053        // branch of an internally-tagged enum, which generators like utoipa emit as
1054        // `oneOf` of `allOf: [<variant $ref>, <type discriminator>]`) collapses to an
1055        // empty `{}`. Empty subschemas match everything, so the enclosing strict
1056        // `oneOf` becomes unsatisfiable and the parameter is uncallable. Merging keeps
1057        // the variant's fields and the discriminator, so branches stay disjoint.
1058        for schema_ref in &obj_schema.all_of {
1059            let part = Self::convert_member_schema(schema_ref, spec, visited)?;
1060            Self::merge_json_schema(&mut schema_obj, part);
1061        }
1062
1063        // Handle anyOf composition: convert each member the same way as oneOf members
1064        // and surface them under `anyOf` (match at least one).
1065        if !obj_schema.any_of.is_empty() {
1066            let any_of_schemas = obj_schema
1067                .any_of
1068                .iter()
1069                .map(|schema_ref| Self::convert_member_schema(schema_ref, spec, visited))
1070                .collect::<Result<Vec<_>, _>>()?;
1071            schema_obj.insert("anyOf".to_string(), json!(any_of_schemas));
1072        }
1073
1074        // Handle oneOf schemas - this takes precedence over other schema properties
1075        if !obj_schema.one_of.is_empty() {
1076            let one_of_schemas = obj_schema
1077                .one_of
1078                .iter()
1079                .map(|schema_ref| Self::convert_member_schema(schema_ref, spec, visited))
1080                .collect::<Result<Vec<_>, _>>()?;
1081            schema_obj.insert("oneOf".to_string(), json!(one_of_schemas));
1082            // When oneOf is present, we typically don't include other properties
1083            // that would conflict with the oneOf semantics
1084            return Ok(Value::Object(schema_obj));
1085        }
1086
1087        // Handle object properties
1088        if !obj_schema.properties.is_empty() {
1089            let properties = &obj_schema.properties;
1090            let mut props_map = serde_json::Map::new();
1091            for (prop_name, prop_schema_or_ref) in properties {
1092                let prop_schema = Self::convert_member_schema(prop_schema_or_ref, spec, visited)?;
1093
1094                // Sanitize property name - no longer add annotations
1095                let sanitized_name = sanitize_property_name(prop_name);
1096                props_map.insert(sanitized_name, prop_schema);
1097            }
1098            schema_obj.insert("properties".to_string(), Value::Object(props_map));
1099        }
1100
1101        // Add required fields
1102        if !obj_schema.required.is_empty() {
1103            schema_obj.insert("required".to_string(), json!(&obj_schema.required));
1104        }
1105
1106        // Handle additionalProperties for object schemas
1107        if let Some(schema_type) = &obj_schema.schema_type
1108            && matches!(schema_type, SchemaTypeSet::Single(SchemaType::Object))
1109        {
1110            // Handle additional_properties based on the OpenAPI schema
1111            match &obj_schema.additional_properties {
1112                None => {
1113                    // In OpenAPI 3.0, the default for additionalProperties is true
1114                    schema_obj.insert("additionalProperties".to_string(), json!(true));
1115                }
1116                Some(Schema::Boolean(BooleanSchema(value))) => {
1117                    // Explicit boolean value
1118                    schema_obj.insert("additionalProperties".to_string(), json!(value));
1119                }
1120                Some(Schema::Object(schema_ref)) => {
1121                    // Additional properties must match this schema
1122                    let additional_props_schema = Self::convert_schema_to_json_schema(
1123                        &Schema::Object(schema_ref.clone()),
1124                        spec,
1125                        visited,
1126                    )?;
1127                    schema_obj.insert("additionalProperties".to_string(), additional_props_schema);
1128                }
1129            }
1130        }
1131
1132        // Handle array-specific properties
1133        if let Some(schema_type) = &obj_schema.schema_type {
1134            if matches!(schema_type, SchemaTypeSet::Single(SchemaType::Array)) {
1135                // Handle prefix_items (OpenAPI 3.1 tuple-like arrays)
1136                if !obj_schema.prefix_items.is_empty() {
1137                    // Convert prefix_items to draft-07 compatible format
1138                    Self::convert_prefix_items_to_draft07(
1139                        &obj_schema.prefix_items,
1140                        &obj_schema.items,
1141                        &mut schema_obj,
1142                        spec,
1143                    )?;
1144                } else if let Some(items_schema) = &obj_schema.items {
1145                    // Handle regular items
1146                    let items_json =
1147                        Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
1148                    schema_obj.insert("items".to_string(), items_json);
1149                }
1150
1151                // Add array constraints
1152                if let Some(min_items) = obj_schema.min_items {
1153                    schema_obj.insert("minItems".to_string(), json!(min_items));
1154                }
1155                if let Some(max_items) = obj_schema.max_items {
1156                    schema_obj.insert("maxItems".to_string(), json!(max_items));
1157                }
1158            } else if let Some(items_schema) = &obj_schema.items {
1159                // Non-array types shouldn't have items, but handle it anyway
1160                let items_json = Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
1161                schema_obj.insert("items".to_string(), items_json);
1162            }
1163        }
1164
1165        // Handle other common properties
1166        if let Some(format) = &obj_schema.format {
1167            schema_obj.insert("format".to_string(), json!(format));
1168        }
1169
1170        if let Some(example) = &obj_schema.example {
1171            schema_obj.insert("example".to_string(), example.clone());
1172        }
1173
1174        // OpenAPI 3.1 plural `examples`, read alongside the deprecated singular `example`
1175        // so neither form is dropped from the generated JSON Schema.
1176        if !obj_schema.examples.is_empty() {
1177            schema_obj.insert("examples".to_string(), json!(&obj_schema.examples));
1178        }
1179
1180        if let Some(default) = &obj_schema.default {
1181            schema_obj.insert("default".to_string(), default.clone());
1182        }
1183
1184        if !obj_schema.enum_values.is_empty() {
1185            schema_obj.insert("enum".to_string(), json!(&obj_schema.enum_values));
1186        }
1187
1188        if let Some(min) = &obj_schema.minimum {
1189            schema_obj.insert("minimum".to_string(), json!(min));
1190        }
1191
1192        if let Some(max) = &obj_schema.maximum {
1193            schema_obj.insert("maximum".to_string(), json!(max));
1194        }
1195
1196        if let Some(min_length) = &obj_schema.min_length {
1197            schema_obj.insert("minLength".to_string(), json!(min_length));
1198        }
1199
1200        if let Some(max_length) = &obj_schema.max_length {
1201            schema_obj.insert("maxLength".to_string(), json!(max_length));
1202        }
1203
1204        if let Some(pattern) = &obj_schema.pattern {
1205            schema_obj.insert("pattern".to_string(), json!(pattern));
1206        }
1207
1208        Ok(Value::Object(schema_obj))
1209    }
1210
1211    /// Convert one composition member or property subschema, restoring `visited` to its
1212    /// pre-member state afterwards.
1213    ///
1214    /// `resolve_reference` follows ref chains (e.g. an alias schema `A -> Target`) and
1215    /// leaves every hop in `visited`; removing only the top-level ref after conversion
1216    /// leaks the intermediate hops, so two siblings that legitimately share a deeper
1217    /// target (a DAG diamond, not a cycle) false-trip the circular-reference guard and
1218    /// abort tool generation for the whole server. Restoring the full snapshot keeps
1219    /// cycle detection scoped to a single descent path.
1220    fn convert_member_schema(
1221        schema_ref: &ObjectOrReference<ObjectSchema>,
1222        spec: &Spec,
1223        visited: &mut HashSet<String>,
1224    ) -> Result<Value, Error> {
1225        let snapshot = visited.clone();
1226        let result = match schema_ref {
1227            ObjectOrReference::Object(schema) => {
1228                Self::convert_object_schema_to_json_schema(schema, spec, visited)
1229            }
1230            ObjectOrReference::Ref { ref_path, .. } => {
1231                Self::resolve_reference(ref_path, spec, visited).and_then(|resolved| {
1232                    Self::convert_object_schema_to_json_schema(&resolved, spec, visited)
1233                })
1234            }
1235        };
1236        *visited = snapshot;
1237        result
1238    }
1239
1240    /// Deep-merge a converted `allOf` member schema (`src`) into an accumulator (`dst`).
1241    ///
1242    /// - `properties`: union of keys (existing keys win on conflict — members of an
1243    ///   `allOf` are not expected to redefine the same property differently).
1244    /// - `required`: union, de-duplicated.
1245    /// - `additionalProperties`: `false` is the most restrictive and wins if any member
1246    ///   sets it; otherwise the first value is kept.
1247    /// - `type`: `object` wins if any member is an object; otherwise first-wins.
1248    /// - everything else (`enum`, `format`, `items`, `oneOf`, …): first-wins.
1249    fn merge_json_schema(dst: &mut serde_json::Map<String, Value>, src: Value) {
1250        let Value::Object(src) = src else {
1251            return;
1252        };
1253        for (key, value) in src {
1254            match key.as_str() {
1255                "properties" => {
1256                    let entry = dst
1257                        .entry("properties")
1258                        .or_insert_with(|| Value::Object(serde_json::Map::new()));
1259                    if let (Some(dst_props), Value::Object(src_props)) =
1260                        (entry.as_object_mut(), value)
1261                    {
1262                        for (prop, schema) in src_props {
1263                            dst_props.entry(prop).or_insert(schema);
1264                        }
1265                    }
1266                }
1267                "required" => {
1268                    let entry = dst
1269                        .entry("required")
1270                        .or_insert_with(|| Value::Array(vec![]));
1271                    if let (Some(dst_required), Value::Array(src_required)) =
1272                        (entry.as_array_mut(), value)
1273                    {
1274                        for item in src_required {
1275                            if !dst_required.contains(&item) {
1276                                dst_required.push(item);
1277                            }
1278                        }
1279                    }
1280                }
1281                "additionalProperties" => {
1282                    let restrictive = dst.get("additionalProperties") == Some(&Value::Bool(false))
1283                        || value == Value::Bool(false);
1284                    if restrictive {
1285                        dst.insert("additionalProperties".to_string(), Value::Bool(false));
1286                    } else {
1287                        dst.entry("additionalProperties").or_insert(value);
1288                    }
1289                }
1290                "type" => match dst.get("type") {
1291                    None => {
1292                        dst.insert("type".to_string(), value);
1293                    }
1294                    Some(Value::String(existing))
1295                        if existing != "object" && value == Value::String("object".to_string()) =>
1296                    {
1297                        dst.insert("type".to_string(), value);
1298                    }
1299                    _ => {}
1300                },
1301                _ => {
1302                    dst.entry(key).or_insert(value);
1303                }
1304            }
1305        }
1306    }
1307
1308    /// Convert SchemaType to string representation
1309    fn schema_type_to_string(schema_type: &SchemaType) -> String {
1310        match schema_type {
1311            SchemaType::Boolean => "boolean",
1312            SchemaType::Integer => "integer",
1313            SchemaType::Number => "number",
1314            SchemaType::String => "string",
1315            SchemaType::Array => "array",
1316            SchemaType::Object => "object",
1317            SchemaType::Null => "null",
1318        }
1319        .to_string()
1320    }
1321
1322    /// Resolve a $ref reference to get the actual schema
1323    ///
1324    /// # Arguments
1325    /// * `ref_path` - The reference path (e.g., "#/components/schemas/Pet")
1326    /// * `spec` - The OpenAPI specification
1327    /// * `visited` - Set of already visited references to detect circular references
1328    ///
1329    /// # Returns
1330    /// The resolved ObjectSchema or an error if the reference is invalid or circular
1331    fn resolve_reference(
1332        ref_path: &str,
1333        spec: &Spec,
1334        visited: &mut HashSet<String>,
1335    ) -> Result<ObjectSchema, Error> {
1336        // Check for circular reference
1337        if visited.contains(ref_path) {
1338            return Err(Error::ToolGeneration(format!(
1339                "Circular reference detected: {ref_path}"
1340            )));
1341        }
1342
1343        // Add to visited set
1344        visited.insert(ref_path.to_string());
1345
1346        // Parse the reference path
1347        // Currently only supporting local references like "#/components/schemas/Pet"
1348        if !ref_path.starts_with("#/components/schemas/") {
1349            return Err(Error::ToolGeneration(format!(
1350                "Unsupported reference format: {ref_path}. Only #/components/schemas/ references are supported"
1351            )));
1352        }
1353
1354        let schema_name = ref_path.strip_prefix("#/components/schemas/").unwrap();
1355
1356        // Get the schema from components
1357        let components = spec.components.as_ref().ok_or_else(|| {
1358            Error::ToolGeneration(format!(
1359                "Reference {ref_path} points to components, but spec has no components section"
1360            ))
1361        })?;
1362
1363        let schema_ref = components.schemas.get(schema_name).ok_or_else(|| {
1364            Error::ToolGeneration(format!(
1365                "Schema '{schema_name}' not found in components/schemas"
1366            ))
1367        })?;
1368
1369        // Resolve the schema reference
1370        let resolved_schema = match schema_ref {
1371            ObjectOrReference::Object(obj_schema) => obj_schema.clone(),
1372            ObjectOrReference::Ref {
1373                ref_path: nested_ref,
1374                ..
1375            } => {
1376                // Recursively resolve nested references
1377                Self::resolve_reference(nested_ref, spec, visited)?
1378            }
1379        };
1380
1381        // NOTE: We intentionally do NOT remove from visited here.
1382        // The ref must stay in visited during the entire conversion process
1383        // to detect cycles when the converted schema contains self-references.
1384        // Callers restore their pre-conversion `visited` snapshot once conversion
1385        // completes (see `convert_member_schema`).
1386
1387        Ok(resolved_schema)
1388    }
1389
1390    /// Resolve reference with metadata extraction
1391    ///
1392    /// Extracts summary and description from the reference before resolving,
1393    /// returning both the resolved schema and the preserved metadata.
1394    fn resolve_reference_with_metadata(
1395        ref_path: &str,
1396        summary: Option<String>,
1397        description: Option<String>,
1398        spec: &Spec,
1399        visited: &mut HashSet<String>,
1400    ) -> Result<(ObjectSchema, ReferenceMetadata), Error> {
1401        let resolved_schema = Self::resolve_reference(ref_path, spec, visited)?;
1402        let metadata = ReferenceMetadata::new(summary, description);
1403        Ok((resolved_schema, metadata))
1404    }
1405
1406    /// Generate JSON Schema for tool parameters
1407    fn generate_parameter_schema(
1408        parameters: &[ObjectOrReference<Parameter>],
1409        _method: &str,
1410        request_body: &Option<ObjectOrReference<RequestBody>>,
1411        spec: &Spec,
1412        skip_parameter_descriptions: bool,
1413        parameter_examples_in_description: bool,
1414    ) -> Result<
1415        (
1416            Value,
1417            std::collections::HashMap<String, crate::tool::ParameterMapping>,
1418        ),
1419        Error,
1420    > {
1421        let mut properties = serde_json::Map::new();
1422        let mut required = Vec::new();
1423        let mut parameter_mappings = std::collections::HashMap::new();
1424
1425        // Group parameters by location
1426        let mut path_params = Vec::new();
1427        let mut query_params = Vec::new();
1428        let mut header_params = Vec::new();
1429        let mut cookie_params = Vec::new();
1430
1431        for param_ref in parameters {
1432            let param = match param_ref {
1433                ObjectOrReference::Object(param) => param,
1434                ObjectOrReference::Ref { ref_path, .. } => {
1435                    // Try to resolve parameter reference
1436                    // Note: Parameter references are rare and not supported yet in this implementation
1437                    // For now, we'll continue to skip them but log a warning
1438                    warn!(
1439                        reference_path = %ref_path,
1440                        "Parameter reference not resolved"
1441                    );
1442                    continue;
1443                }
1444            };
1445
1446            match &param.location {
1447                ParameterIn::Query => query_params.push(param),
1448                ParameterIn::Header => header_params.push(param),
1449                ParameterIn::Path => path_params.push(param),
1450                ParameterIn::Cookie => cookie_params.push(param),
1451            }
1452        }
1453
1454        // Process path parameters (always required)
1455        for param in path_params {
1456            let (param_schema, mut annotations) = Self::convert_parameter_schema(
1457                param,
1458                ParameterIn::Path,
1459                spec,
1460                skip_parameter_descriptions,
1461                parameter_examples_in_description,
1462            )?;
1463
1464            // Sanitize parameter name and add original name annotation if needed
1465            let sanitized_name = sanitize_property_name(&param.name);
1466            if sanitized_name != param.name {
1467                annotations = annotations.with_original_name(param.name.clone());
1468            }
1469
1470            // Extract explode setting from annotations
1471            let explode = annotations
1472                .annotations
1473                .iter()
1474                .find_map(|a| {
1475                    if let Annotation::Explode(e) = a {
1476                        Some(*e)
1477                    } else {
1478                        None
1479                    }
1480                })
1481                .unwrap_or(true);
1482
1483            // Store parameter mapping
1484            parameter_mappings.insert(
1485                sanitized_name.clone(),
1486                crate::tool::ParameterMapping {
1487                    sanitized_name: sanitized_name.clone(),
1488                    original_name: param.name.clone(),
1489                    location: "path".to_string(),
1490                    explode,
1491                },
1492            );
1493
1494            // No longer apply annotations to schema - use parameter_mappings instead
1495            properties.insert(sanitized_name.clone(), param_schema);
1496            required.push(sanitized_name);
1497        }
1498
1499        // Process query parameters
1500        for param in &query_params {
1501            let (param_schema, mut annotations) = Self::convert_parameter_schema(
1502                param,
1503                ParameterIn::Query,
1504                spec,
1505                skip_parameter_descriptions,
1506                parameter_examples_in_description,
1507            )?;
1508
1509            // Sanitize parameter name and add original name annotation if needed
1510            let sanitized_name = sanitize_property_name(&param.name);
1511            if sanitized_name != param.name {
1512                annotations = annotations.with_original_name(param.name.clone());
1513            }
1514
1515            // Extract explode setting from annotations
1516            let explode = annotations
1517                .annotations
1518                .iter()
1519                .find_map(|a| {
1520                    if let Annotation::Explode(e) = a {
1521                        Some(*e)
1522                    } else {
1523                        None
1524                    }
1525                })
1526                .unwrap_or(true);
1527
1528            // Store parameter mapping
1529            parameter_mappings.insert(
1530                sanitized_name.clone(),
1531                crate::tool::ParameterMapping {
1532                    sanitized_name: sanitized_name.clone(),
1533                    original_name: param.name.clone(),
1534                    location: "query".to_string(),
1535                    explode,
1536                },
1537            );
1538
1539            // No longer apply annotations to schema - use parameter_mappings instead
1540            properties.insert(sanitized_name.clone(), param_schema);
1541            if param.required.unwrap_or(false) {
1542                required.push(sanitized_name);
1543            }
1544        }
1545
1546        // Process header parameters (optional by default unless explicitly required)
1547        for param in &header_params {
1548            let (param_schema, mut annotations) = Self::convert_parameter_schema(
1549                param,
1550                ParameterIn::Header,
1551                spec,
1552                skip_parameter_descriptions,
1553                parameter_examples_in_description,
1554            )?;
1555
1556            // Sanitize parameter name after prefixing and add original name annotation if needed
1557            let prefixed_name = format!("header_{}", param.name);
1558            let sanitized_name = sanitize_property_name(&prefixed_name);
1559            if sanitized_name != prefixed_name {
1560                annotations = annotations.with_original_name(param.name.clone());
1561            }
1562
1563            // Extract explode setting from annotations
1564            let explode = annotations
1565                .annotations
1566                .iter()
1567                .find_map(|a| {
1568                    if let Annotation::Explode(e) = a {
1569                        Some(*e)
1570                    } else {
1571                        None
1572                    }
1573                })
1574                .unwrap_or(true);
1575
1576            // Store parameter mapping
1577            parameter_mappings.insert(
1578                sanitized_name.clone(),
1579                crate::tool::ParameterMapping {
1580                    sanitized_name: sanitized_name.clone(),
1581                    original_name: param.name.clone(),
1582                    location: "header".to_string(),
1583                    explode,
1584                },
1585            );
1586
1587            // No longer apply annotations to schema - use parameter_mappings instead
1588            properties.insert(sanitized_name.clone(), param_schema);
1589            if param.required.unwrap_or(false) {
1590                required.push(sanitized_name);
1591            }
1592        }
1593
1594        // Process cookie parameters (rare, but supported)
1595        for param in &cookie_params {
1596            let (param_schema, mut annotations) = Self::convert_parameter_schema(
1597                param,
1598                ParameterIn::Cookie,
1599                spec,
1600                skip_parameter_descriptions,
1601                parameter_examples_in_description,
1602            )?;
1603
1604            // Sanitize parameter name after prefixing and add original name annotation if needed
1605            let prefixed_name = format!("cookie_{}", param.name);
1606            let sanitized_name = sanitize_property_name(&prefixed_name);
1607            if sanitized_name != prefixed_name {
1608                annotations = annotations.with_original_name(param.name.clone());
1609            }
1610
1611            // Extract explode setting from annotations
1612            let explode = annotations
1613                .annotations
1614                .iter()
1615                .find_map(|a| {
1616                    if let Annotation::Explode(e) = a {
1617                        Some(*e)
1618                    } else {
1619                        None
1620                    }
1621                })
1622                .unwrap_or(true);
1623
1624            // Store parameter mapping
1625            parameter_mappings.insert(
1626                sanitized_name.clone(),
1627                crate::tool::ParameterMapping {
1628                    sanitized_name: sanitized_name.clone(),
1629                    original_name: param.name.clone(),
1630                    location: "cookie".to_string(),
1631                    explode,
1632                },
1633            );
1634
1635            // No longer apply annotations to schema - use parameter_mappings instead
1636            properties.insert(sanitized_name.clone(), param_schema);
1637            if param.required.unwrap_or(false) {
1638                required.push(sanitized_name);
1639            }
1640        }
1641
1642        // Add request body parameter if defined in the OpenAPI spec
1643        if let Some(request_body) = request_body
1644            && let Some((body_schema, _annotations, is_required)) =
1645                Self::convert_request_body_to_json_schema(request_body, spec)?
1646        {
1647            // Store parameter mapping for request_body
1648            parameter_mappings.insert(
1649                "request_body".to_string(),
1650                crate::tool::ParameterMapping {
1651                    sanitized_name: "request_body".to_string(),
1652                    original_name: "request_body".to_string(),
1653                    location: "body".to_string(),
1654                    explode: false,
1655                },
1656            );
1657
1658            // No longer apply annotations to schema - use parameter_mappings instead
1659            properties.insert("request_body".to_string(), body_schema);
1660            if is_required {
1661                required.push("request_body".to_string());
1662            }
1663        }
1664
1665        // Add special parameters for request configuration
1666        if !query_params.is_empty() || !header_params.is_empty() || !cookie_params.is_empty() {
1667            // Add optional timeout parameter
1668            properties.insert(
1669                "timeout_seconds".to_string(),
1670                json!({
1671                    "type": "integer",
1672                    "description": "Request timeout in seconds",
1673                    "minimum": 1,
1674                    "maximum": 300,
1675                    "default": 30
1676                }),
1677            );
1678        }
1679
1680        let schema = json!({
1681            "type": "object",
1682            "properties": properties,
1683            "required": required,
1684            "additionalProperties": false
1685        });
1686
1687        Ok((schema, parameter_mappings))
1688    }
1689
1690    /// Convert `OpenAPI` parameter schema to JSON Schema for MCP tools
1691    fn convert_parameter_schema(
1692        param: &Parameter,
1693        location: ParameterIn,
1694        spec: &Spec,
1695        skip_parameter_descriptions: bool,
1696        parameter_examples_in_description: bool,
1697    ) -> Result<(Value, Annotations), Error> {
1698        // Convert the parameter schema using the unified converter
1699        let base_schema = if let Some(schema_ref) = &param.schema {
1700            match schema_ref {
1701                ObjectOrReference::Object(obj_schema) => {
1702                    let mut visited = HashSet::new();
1703                    Self::convert_schema_to_json_schema(
1704                        &Schema::Object(Box::new(ObjectOrReference::Object(obj_schema.clone()))),
1705                        spec,
1706                        &mut visited,
1707                    )?
1708                }
1709                ObjectOrReference::Ref {
1710                    ref_path,
1711                    summary,
1712                    description,
1713                } => {
1714                    // Resolve the reference with metadata extraction
1715                    let mut visited = HashSet::new();
1716                    match Self::resolve_reference_with_metadata(
1717                        ref_path,
1718                        summary.clone(),
1719                        description.clone(),
1720                        spec,
1721                        &mut visited,
1722                    ) {
1723                        Ok((resolved_schema, ref_metadata)) => {
1724                            let mut schema_json = Self::convert_schema_to_json_schema(
1725                                &Schema::Object(Box::new(ObjectOrReference::Object(
1726                                    resolved_schema,
1727                                ))),
1728                                spec,
1729                                &mut visited,
1730                            )?;
1731
1732                            // Enhance schema with reference metadata if available
1733                            if let Value::Object(ref mut schema_obj) = schema_json {
1734                                // Reference metadata takes precedence over schema descriptions (OpenAPI 3.1 semantics)
1735                                if let Some(ref_desc) = ref_metadata.best_description() {
1736                                    schema_obj.insert("description".to_string(), json!(ref_desc));
1737                                }
1738                                // Fallback: if no reference metadata but schema lacks description, keep existing logic
1739                                // (This case is now handled by the reference metadata being None)
1740                            }
1741
1742                            schema_json
1743                        }
1744                        Err(_) => {
1745                            // Fallback to string for unresolvable references
1746                            json!({"type": "string"})
1747                        }
1748                    }
1749                }
1750            }
1751        } else {
1752            // Default to string if no schema
1753            json!({"type": "string"})
1754        };
1755
1756        // Merge the base schema properties with parameter metadata
1757        let mut result = match base_schema {
1758            Value::Object(obj) => obj,
1759            _ => {
1760                // This should never happen as our converter always returns objects
1761                return Err(Error::ToolGeneration(format!(
1762                    "Internal error: schema converter returned non-object for parameter '{}'",
1763                    param.name
1764                )));
1765            }
1766        };
1767
1768        // Collect examples from all sources, chained so neither the singular `example`
1769        // nor the plural `examples` is dropped at the parameter or schema level.
1770        let mut collected_examples: Vec<Value> = Vec::new();
1771
1772        // Parameter-level singular `example`.
1773        if let Some(example) = &param.example {
1774            collected_examples.push(example.clone());
1775        }
1776        // Parameter-level `examples` map.
1777        for example_ref in param.examples.values() {
1778            if let ObjectOrReference::Object(example_obj) = example_ref
1779                && let Some(value) = &example_obj.value
1780            {
1781                collected_examples.push(value.clone());
1782            }
1783            // References in the examples map are not resolved here.
1784        }
1785        // Schema-level singular `example` (added during base schema conversion).
1786        if let Some(example) = result.get("example") {
1787            collected_examples.push(example.clone());
1788        }
1789        // Schema-level plural `examples` (added during base schema conversion).
1790        if let Some(Value::Array(examples)) = result.get("examples") {
1791            collected_examples.extend(examples.iter().cloned());
1792        }
1793        // Array parameters: lift element-level `examples` to the parameter level, wrapping each
1794        // as a single-element array. A valid value for an array parameter is itself an array, and
1795        // MCP clients (and the inspector) read examples at the parameter level, not nested under
1796        // `items`. Clear them from `items` afterwards so the tokens are not duplicated.
1797        let lifted_item_examples: Option<Vec<Value>> = (result.get("type")
1798            == Some(&json!("array")))
1799        .then(|| {
1800            result
1801                .get("items")
1802                .and_then(|items| items.get("examples"))
1803                .and_then(Value::as_array)
1804                .cloned()
1805        })
1806        .flatten();
1807        if let Some(item_examples) = lifted_item_examples {
1808            for item_example in &item_examples {
1809                collected_examples.push(json!([item_example]));
1810            }
1811            if let Some(Value::Object(items)) = result.get_mut("items") {
1812                items.remove("examples");
1813            }
1814        }
1815        // De-duplicate while preserving first-seen order.
1816        let mut deduped: Vec<Value> = Vec::with_capacity(collected_examples.len());
1817        for example in collected_examples {
1818            if !deduped.contains(&example) {
1819                deduped.push(example);
1820            }
1821        }
1822        let collected_examples = deduped;
1823
1824        // Examples are emitted to exactly one channel to avoid duplicating their tokens.
1825        // Default: the structured `examples` field (the JSON Schema standard form). When
1826        // `parameter_examples_in_description` is set, they go into the parameter description
1827        // instead, for clients that do not read structured `examples` (e.g. OpenAI strict
1828        // mode). Ad-hoc example fields from the base schema conversion are cleared first so
1829        // examples cannot leak into both channels.
1830        result.remove("example");
1831        result.remove("examples");
1832
1833        let base_description = param
1834            .description
1835            .as_ref()
1836            .map(|d| d.to_string())
1837            .or_else(|| {
1838                result
1839                    .get("description")
1840                    .and_then(|d| d.as_str())
1841                    .map(|d| d.to_string())
1842            })
1843            .unwrap_or_else(|| format!("{} parameter", param.name));
1844
1845        let description = if parameter_examples_in_description {
1846            match Self::format_examples_for_description(&collected_examples) {
1847                Some(examples_str) => format!("{base_description}. {examples_str}"),
1848                None => base_description,
1849            }
1850        } else {
1851            base_description
1852        };
1853
1854        if !skip_parameter_descriptions {
1855            result.insert("description".to_string(), json!(description));
1856        }
1857
1858        if !parameter_examples_in_description && !collected_examples.is_empty() {
1859            result.insert("examples".to_string(), json!(collected_examples));
1860        }
1861
1862        // Create annotations instead of adding them to the JSON
1863        let mut annotations = Annotations::new()
1864            .with_location(Location::Parameter(location))
1865            .with_required(param.required.unwrap_or(false));
1866
1867        // Add explode annotation if present
1868        if let Some(explode) = param.explode {
1869            annotations = annotations.with_explode(explode);
1870        } else {
1871            // Default explode behavior based on OpenAPI spec:
1872            // - form style defaults to true
1873            // - other styles default to false
1874            let default_explode = match &param.style {
1875                Some(ParameterStyle::Form) | None => true, // form is default style
1876                _ => false,
1877            };
1878            annotations = annotations.with_explode(default_explode);
1879        }
1880
1881        Ok((Value::Object(result), annotations))
1882    }
1883
1884    /// Format examples for inclusion in parameter descriptions
1885    fn format_examples_for_description(examples: &[Value]) -> Option<String> {
1886        if examples.is_empty() {
1887            return None;
1888        }
1889
1890        if examples.len() == 1 {
1891            let example_str =
1892                serde_json::to_string(&examples[0]).unwrap_or_else(|_| "null".to_string());
1893            Some(format!("Example: `{example_str}`"))
1894        } else {
1895            let mut result = String::from("Examples:\n");
1896            for ex in examples {
1897                let json_str = serde_json::to_string(ex).unwrap_or_else(|_| "null".to_string());
1898                result.push_str(&format!("- `{json_str}`\n"));
1899            }
1900            // Remove trailing newline
1901            result.pop();
1902            Some(result)
1903        }
1904    }
1905
1906    /// Converts prefixItems (tuple-like arrays) to JSON Schema draft-07 compatible format.
1907    ///
1908    /// This handles OpenAPI 3.1 prefixItems which define specific schemas for each array position,
1909    /// converting them to draft-07 format that MCP tools can understand.
1910    ///
1911    /// Conversion strategy:
1912    /// - If items is `false`, set minItems=maxItems=prefix_items.len() for exact length
1913    /// - If all prefixItems have same type, use that type for items
1914    /// - If mixed types, use oneOf with all unique types from prefixItems
1915    /// - Add descriptive comment about tuple nature
1916    fn convert_prefix_items_to_draft07(
1917        prefix_items: &[ObjectOrReference<ObjectSchema>],
1918        items: &Option<Box<Schema>>,
1919        result: &mut serde_json::Map<String, Value>,
1920        spec: &Spec,
1921    ) -> Result<(), Error> {
1922        let prefix_count = prefix_items.len();
1923
1924        // Extract types from prefixItems
1925        let mut item_types = Vec::new();
1926        for prefix_item in prefix_items {
1927            match prefix_item {
1928                ObjectOrReference::Object(obj_schema) => {
1929                    if let Some(schema_type) = &obj_schema.schema_type {
1930                        match schema_type {
1931                            SchemaTypeSet::Single(SchemaType::String) => item_types.push("string"),
1932                            SchemaTypeSet::Single(SchemaType::Integer) => {
1933                                item_types.push("integer")
1934                            }
1935                            SchemaTypeSet::Single(SchemaType::Number) => item_types.push("number"),
1936                            SchemaTypeSet::Single(SchemaType::Boolean) => {
1937                                item_types.push("boolean")
1938                            }
1939                            SchemaTypeSet::Single(SchemaType::Array) => item_types.push("array"),
1940                            SchemaTypeSet::Single(SchemaType::Object) => item_types.push("object"),
1941                            _ => item_types.push("string"), // fallback
1942                        }
1943                    } else {
1944                        item_types.push("string"); // fallback
1945                    }
1946                }
1947                ObjectOrReference::Ref { ref_path, .. } => {
1948                    // Try to resolve the reference
1949                    let mut visited = HashSet::new();
1950                    match Self::resolve_reference(ref_path, spec, &mut visited) {
1951                        Ok(resolved_schema) => {
1952                            // Extract the type immediately and store it as a string
1953                            if let Some(schema_type_set) = &resolved_schema.schema_type {
1954                                match schema_type_set {
1955                                    SchemaTypeSet::Single(SchemaType::String) => {
1956                                        item_types.push("string")
1957                                    }
1958                                    SchemaTypeSet::Single(SchemaType::Integer) => {
1959                                        item_types.push("integer")
1960                                    }
1961                                    SchemaTypeSet::Single(SchemaType::Number) => {
1962                                        item_types.push("number")
1963                                    }
1964                                    SchemaTypeSet::Single(SchemaType::Boolean) => {
1965                                        item_types.push("boolean")
1966                                    }
1967                                    SchemaTypeSet::Single(SchemaType::Array) => {
1968                                        item_types.push("array")
1969                                    }
1970                                    SchemaTypeSet::Single(SchemaType::Object) => {
1971                                        item_types.push("object")
1972                                    }
1973                                    _ => item_types.push("string"), // fallback
1974                                }
1975                            } else {
1976                                item_types.push("string"); // fallback
1977                            }
1978                        }
1979                        Err(_) => {
1980                            // Fallback to string for unresolvable references
1981                            item_types.push("string");
1982                        }
1983                    }
1984                }
1985            }
1986        }
1987
1988        // Check if items is false (no additional items allowed)
1989        let items_is_false =
1990            matches!(items.as_ref().map(|i| i.as_ref()), Some(Schema::Boolean(b)) if !b.0);
1991
1992        if items_is_false {
1993            // Exact array length required
1994            result.insert("minItems".to_string(), json!(prefix_count));
1995            result.insert("maxItems".to_string(), json!(prefix_count));
1996        }
1997
1998        // Determine items schema based on prefixItems types
1999        let unique_types: std::collections::BTreeSet<_> = item_types.into_iter().collect();
2000
2001        if unique_types.len() == 1 {
2002            // All items have same type
2003            let item_type = unique_types.into_iter().next().unwrap();
2004            result.insert("items".to_string(), json!({"type": item_type}));
2005        } else if unique_types.len() > 1 {
2006            // Mixed types, use oneOf (sorted for consistent ordering)
2007            let one_of: Vec<Value> = unique_types
2008                .into_iter()
2009                .map(|t| json!({"type": t}))
2010                .collect();
2011            result.insert("items".to_string(), json!({"oneOf": one_of}));
2012        }
2013
2014        Ok(())
2015    }
2016
2017    /// Converts the new oas3 Schema enum (which can be Boolean or Object) to draft-07 format.
2018    ///
2019    /// The oas3 crate now supports:
2020    /// - Schema::Object(`ObjectOrReference<ObjectSchema>`) - regular object schemas
2021    /// - Schema::Boolean(BooleanSchema) - true/false schemas for validation control
2022    ///
2023    /// For MCP compatibility (draft-07), we convert:
2024    /// - Boolean true -> allow any items (no items constraint)
2025    /// - Boolean false -> not handled here (should be handled by caller with array constraints)
2026    ///
2027    /// Convert request body from OpenAPI to JSON Schema for MCP tools
2028    fn convert_request_body_to_json_schema(
2029        request_body_ref: &ObjectOrReference<RequestBody>,
2030        spec: &Spec,
2031    ) -> Result<Option<(Value, Annotations, bool)>, Error> {
2032        match request_body_ref {
2033            ObjectOrReference::Object(request_body) => {
2034                // Check for multipart/form-data first
2035                if let Some(media_type) = request_body.content.get("multipart/form-data") {
2036                    return Self::convert_multipart_request_body(request_body, media_type, spec);
2037                }
2038
2039                // Extract schema from request body content
2040                // Prioritize application/json content type
2041                let schema_info = request_body
2042                    .content
2043                    .get(mime::APPLICATION_JSON.as_ref())
2044                    .or_else(|| request_body.content.get("application/json"))
2045                    .or_else(|| {
2046                        // Fall back to first available content type
2047                        request_body.content.values().next()
2048                    });
2049
2050                if let Some(media_type) = schema_info {
2051                    if let Some(schema_ref) = &media_type.schema {
2052                        // Convert ObjectOrReference<ObjectSchema> to Schema
2053                        let schema = Schema::Object(Box::new(schema_ref.clone()));
2054
2055                        // Use the unified converter
2056                        let mut visited = HashSet::new();
2057                        let converted_schema =
2058                            Self::convert_schema_to_json_schema(&schema, spec, &mut visited)?;
2059
2060                        // Ensure we have an object schema
2061                        let mut schema_obj = match converted_schema {
2062                            Value::Object(obj) => obj,
2063                            _ => {
2064                                // If not an object, wrap it in an object
2065                                let mut obj = serde_json::Map::new();
2066                                obj.insert("type".to_string(), json!("object"));
2067                                obj.insert("additionalProperties".to_string(), json!(true));
2068                                obj
2069                            }
2070                        };
2071
2072                        // Add description following OpenAPI 3.1 precedence (schema description > request body description)
2073                        if !schema_obj.contains_key("description") {
2074                            let description = request_body
2075                                .description
2076                                .clone()
2077                                .unwrap_or_else(|| "Request body data".to_string());
2078                            schema_obj.insert("description".to_string(), json!(description));
2079                        }
2080
2081                        // Create annotations instead of adding them to the JSON
2082                        let annotations = Annotations::new()
2083                            .with_location(Location::Body)
2084                            .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
2085
2086                        let required = request_body.required.unwrap_or(false);
2087                        Ok(Some((Value::Object(schema_obj), annotations, required)))
2088                    } else {
2089                        Ok(None)
2090                    }
2091                } else {
2092                    Ok(None)
2093                }
2094            }
2095            ObjectOrReference::Ref {
2096                ref_path: _,
2097                summary,
2098                description,
2099            } => {
2100                // Use reference metadata to enhance request body description
2101                let ref_metadata = ReferenceMetadata::new(summary.clone(), description.clone());
2102                let enhanced_description = ref_metadata
2103                    .best_description()
2104                    .map(|desc| desc.to_string())
2105                    .unwrap_or_else(|| "Request body data".to_string());
2106
2107                let mut result = serde_json::Map::new();
2108                result.insert("type".to_string(), json!("object"));
2109                result.insert("additionalProperties".to_string(), json!(true));
2110                result.insert("description".to_string(), json!(enhanced_description));
2111
2112                // Create annotations instead of adding them to the JSON
2113                let annotations = Annotations::new()
2114                    .with_location(Location::Body)
2115                    .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
2116
2117                Ok(Some((Value::Object(result), annotations, false)))
2118            }
2119        }
2120    }
2121
2122    /// Convert multipart/form-data request body to JSON Schema.
2123    ///
2124    /// This function handles multipart/form-data content types by:
2125    /// 1. Iterating over all properties in the schema
2126    /// 2. Detecting file fields (format: binary or byte)
2127    /// 3. Transforming file fields to structured file object schemas
2128    /// 4. Keeping non-file fields as-is
2129    /// 5. Adding file_fields annotation for HTTP client processing
2130    fn convert_multipart_request_body(
2131        request_body: &RequestBody,
2132        media_type: &oas3::spec::MediaType,
2133        spec: &Spec,
2134    ) -> Result<Option<(Value, Annotations, bool)>, Error> {
2135        let Some(schema_ref) = &media_type.schema else {
2136            return Ok(None);
2137        };
2138
2139        // Get the properties from the schema
2140        let obj_schema = match schema_ref {
2141            ObjectOrReference::Object(obj) => obj.clone(),
2142            ObjectOrReference::Ref { ref_path, .. } => {
2143                // Resolve the reference
2144                let mut visited = HashSet::new();
2145                Self::resolve_reference(ref_path, spec, &mut visited)?
2146            }
2147        };
2148
2149        // Build properties with file field transformation
2150        let mut props_map = serde_json::Map::new();
2151        let mut file_fields = Vec::new();
2152
2153        for (prop_name, prop_schema_or_ref) in &obj_schema.properties {
2154            let sanitized_name = sanitize_property_name(prop_name);
2155
2156            let prop_schema = if Self::is_file_field_property(prop_schema_or_ref) {
2157                // Track this as a file field
2158                file_fields.push(sanitized_name.clone());
2159
2160                // Get the description from the original schema
2161                let description = match prop_schema_or_ref {
2162                    ObjectOrReference::Object(obj) => obj.description.as_deref(),
2163                    ObjectOrReference::Ref { .. } => None,
2164                };
2165
2166                // Transform to file object schema
2167                Self::convert_file_field_to_schema(description)
2168            } else {
2169                // Convert non-file field using standard conversion
2170                let schema = Schema::Object(Box::new(prop_schema_or_ref.clone()));
2171                let mut visited = HashSet::new();
2172                Self::convert_schema_to_json_schema(&schema, spec, &mut visited)?
2173            };
2174
2175            props_map.insert(sanitized_name, prop_schema);
2176        }
2177
2178        // Build the result schema
2179        let mut schema_obj = serde_json::Map::new();
2180        schema_obj.insert("type".to_string(), json!("object"));
2181
2182        if !props_map.is_empty() {
2183            schema_obj.insert("properties".to_string(), Value::Object(props_map));
2184        }
2185
2186        // Add required fields
2187        if !obj_schema.required.is_empty() {
2188            // Sanitize required field names
2189            let sanitized_required: Vec<String> = obj_schema
2190                .required
2191                .iter()
2192                .map(|name| sanitize_property_name(name))
2193                .collect();
2194            schema_obj.insert("required".to_string(), json!(sanitized_required));
2195        }
2196
2197        // Add description
2198        let description = obj_schema
2199            .description
2200            .clone()
2201            .or_else(|| request_body.description.clone())
2202            .unwrap_or_else(|| "Request body data".to_string());
2203        schema_obj.insert("description".to_string(), json!(description));
2204
2205        // Create annotations with multipart/form-data content type and file fields
2206        let mut annotations = Annotations::new()
2207            .with_location(Location::Body)
2208            .with_content_type("multipart/form-data".to_string());
2209
2210        if !file_fields.is_empty() {
2211            annotations = annotations.with_file_fields(file_fields);
2212        }
2213
2214        let required = request_body.required.unwrap_or(false);
2215        Ok(Some((Value::Object(schema_obj), annotations, required)))
2216    }
2217
2218    /// Extract parameter values from MCP tool call arguments
2219    ///
2220    /// # Errors
2221    ///
2222    /// Returns an error if the arguments are invalid or missing required parameters
2223    pub fn extract_parameters(
2224        tool_metadata: &ToolMetadata,
2225        arguments: &Value,
2226    ) -> Result<ExtractedParameters, ToolCallValidationError> {
2227        let args = arguments.as_object().ok_or_else(|| {
2228            ToolCallValidationError::RequestConstructionError {
2229                reason: "Arguments must be an object".to_string(),
2230            }
2231        })?;
2232
2233        trace!(
2234            tool_name = %tool_metadata.name,
2235            raw_arguments = ?arguments,
2236            "Starting parameter extraction"
2237        );
2238
2239        let mut path_params = HashMap::new();
2240        let mut query_params = HashMap::new();
2241        let mut header_params = HashMap::new();
2242        let mut cookie_params = HashMap::new();
2243        let mut body_params = HashMap::new();
2244        let mut config = RequestConfig::default();
2245
2246        // Extract timeout if provided
2247        if let Some(timeout) = args.get("timeout_seconds").and_then(Value::as_u64) {
2248            config.timeout_seconds = u32::try_from(timeout).unwrap_or(u32::MAX);
2249        }
2250
2251        // Process each argument
2252        for (key, value) in args {
2253            if key == "timeout_seconds" {
2254                continue; // Already processed
2255            }
2256
2257            // Handle special request_body parameter
2258            if key == "request_body" {
2259                body_params.insert("request_body".to_string(), value.clone());
2260                continue;
2261            }
2262
2263            // Get parameter mapping from tool metadata
2264            let mapping = tool_metadata.parameter_mappings.get(key);
2265
2266            if let Some(mapping) = mapping {
2267                // Use server-side parameter mapping
2268                match mapping.location.as_str() {
2269                    "path" => {
2270                        path_params.insert(mapping.original_name.clone(), value.clone());
2271                    }
2272                    "query" => {
2273                        query_params.insert(
2274                            mapping.original_name.clone(),
2275                            QueryParameter::new(value.clone(), mapping.explode),
2276                        );
2277                    }
2278                    "header" => {
2279                        header_params.insert(mapping.original_name.clone(), value.clone());
2280                    }
2281                    "cookie" => {
2282                        cookie_params.insert(mapping.original_name.clone(), value.clone());
2283                    }
2284                    "body" => {
2285                        body_params.insert(mapping.original_name.clone(), value.clone());
2286                    }
2287                    _ => {
2288                        return Err(ToolCallValidationError::RequestConstructionError {
2289                            reason: format!("Unknown parameter location for parameter: {key}"),
2290                        });
2291                    }
2292                }
2293            } else {
2294                // Fallback to schema annotations for backward compatibility
2295                let location = Self::get_parameter_location(tool_metadata, key).map_err(|e| {
2296                    ToolCallValidationError::RequestConstructionError {
2297                        reason: e.to_string(),
2298                    }
2299                })?;
2300
2301                let original_name = Self::get_original_parameter_name(tool_metadata, key);
2302
2303                match location.as_str() {
2304                    "path" => {
2305                        path_params
2306                            .insert(original_name.unwrap_or_else(|| key.clone()), value.clone());
2307                    }
2308                    "query" => {
2309                        let param_name = original_name.unwrap_or_else(|| key.clone());
2310                        let explode = Self::get_parameter_explode(tool_metadata, key);
2311                        query_params
2312                            .insert(param_name, QueryParameter::new(value.clone(), explode));
2313                    }
2314                    "header" => {
2315                        let header_name = if let Some(orig) = original_name {
2316                            orig
2317                        } else if key.starts_with("header_") {
2318                            key.strip_prefix("header_").unwrap_or(key).to_string()
2319                        } else {
2320                            key.clone()
2321                        };
2322                        header_params.insert(header_name, value.clone());
2323                    }
2324                    "cookie" => {
2325                        let cookie_name = if let Some(orig) = original_name {
2326                            orig
2327                        } else if key.starts_with("cookie_") {
2328                            key.strip_prefix("cookie_").unwrap_or(key).to_string()
2329                        } else {
2330                            key.clone()
2331                        };
2332                        cookie_params.insert(cookie_name, value.clone());
2333                    }
2334                    "body" => {
2335                        let body_name = if key.starts_with("body_") {
2336                            key.strip_prefix("body_").unwrap_or(key).to_string()
2337                        } else {
2338                            key.clone()
2339                        };
2340                        body_params.insert(body_name, value.clone());
2341                    }
2342                    _ => {
2343                        return Err(ToolCallValidationError::RequestConstructionError {
2344                            reason: format!("Unknown parameter location for parameter: {key}"),
2345                        });
2346                    }
2347                }
2348            }
2349        }
2350
2351        let extracted = ExtractedParameters {
2352            path: path_params,
2353            query: query_params,
2354            headers: header_params,
2355            cookies: cookie_params,
2356            body: body_params,
2357            config,
2358        };
2359
2360        trace!(
2361            tool_name = %tool_metadata.name,
2362            extracted_parameters = ?extracted,
2363            "Parameter extraction completed"
2364        );
2365
2366        // Validate parameters against tool metadata using the original arguments
2367        Self::validate_parameters(tool_metadata, arguments)?;
2368
2369        Ok(extracted)
2370    }
2371
2372    /// Get the original parameter name from x-original-name annotation if it exists
2373    fn get_original_parameter_name(
2374        tool_metadata: &ToolMetadata,
2375        param_name: &str,
2376    ) -> Option<String> {
2377        tool_metadata
2378            .parameters
2379            .get("properties")
2380            .and_then(|p| p.as_object())
2381            .and_then(|props| props.get(param_name))
2382            .and_then(|schema| schema.get(X_ORIGINAL_NAME))
2383            .and_then(|v| v.as_str())
2384            .map(|s| s.to_string())
2385    }
2386
2387    /// Get parameter explode setting from tool metadata
2388    fn get_parameter_explode(tool_metadata: &ToolMetadata, param_name: &str) -> bool {
2389        tool_metadata
2390            .parameters
2391            .get("properties")
2392            .and_then(|p| p.as_object())
2393            .and_then(|props| props.get(param_name))
2394            .and_then(|schema| schema.get(X_PARAMETER_EXPLODE))
2395            .and_then(|v| v.as_bool())
2396            .unwrap_or(true) // Default to true (OpenAPI default for form style)
2397    }
2398
2399    /// Get parameter location from tool metadata
2400    fn get_parameter_location(
2401        tool_metadata: &ToolMetadata,
2402        param_name: &str,
2403    ) -> Result<String, Error> {
2404        let properties = tool_metadata
2405            .parameters
2406            .get("properties")
2407            .and_then(|p| p.as_object())
2408            .ok_or_else(|| Error::ToolGeneration("Invalid tool parameters schema".to_string()))?;
2409
2410        if let Some(param_schema) = properties.get(param_name)
2411            && let Some(location) = param_schema
2412                .get(X_PARAMETER_LOCATION)
2413                .and_then(|v| v.as_str())
2414        {
2415            return Ok(location.to_string());
2416        }
2417
2418        // Fallback: infer from parameter name prefix
2419        if param_name.starts_with("header_") {
2420            Ok("header".to_string())
2421        } else if param_name.starts_with("cookie_") {
2422            Ok("cookie".to_string())
2423        } else if param_name.starts_with("body_") {
2424            Ok("body".to_string())
2425        } else {
2426            // Default to query for unknown parameters
2427            Ok("query".to_string())
2428        }
2429    }
2430
2431    /// Validate parameters against tool metadata
2432    fn validate_parameters(
2433        tool_metadata: &ToolMetadata,
2434        arguments: &Value,
2435    ) -> Result<(), ToolCallValidationError> {
2436        let schema = &tool_metadata.parameters;
2437
2438        // Get required parameters from schema
2439        let required_params = schema
2440            .get("required")
2441            .and_then(|r| r.as_array())
2442            .map(|arr| {
2443                arr.iter()
2444                    .filter_map(|v| v.as_str())
2445                    .collect::<std::collections::HashSet<_>>()
2446            })
2447            .unwrap_or_default();
2448
2449        let properties = schema
2450            .get("properties")
2451            .and_then(|p| p.as_object())
2452            .ok_or_else(|| ToolCallValidationError::RequestConstructionError {
2453                reason: "Tool schema missing properties".to_string(),
2454            })?;
2455
2456        let args = arguments.as_object().ok_or_else(|| {
2457            ToolCallValidationError::RequestConstructionError {
2458                reason: "Arguments must be an object".to_string(),
2459            }
2460        })?;
2461
2462        // Collect ALL validation errors before returning
2463        let mut all_errors = Vec::new();
2464
2465        // Check for unknown parameters
2466        all_errors.extend(Self::check_unknown_parameters(args, properties));
2467
2468        // Check all required parameters are provided in the arguments
2469        all_errors.extend(Self::check_missing_required(
2470            args,
2471            properties,
2472            &required_params,
2473        ));
2474
2475        // Validate parameter values against their schemas
2476        all_errors.extend(Self::validate_parameter_values(
2477            args,
2478            properties,
2479            &required_params,
2480        ));
2481
2482        // Return all errors if any were found
2483        if !all_errors.is_empty() {
2484            return Err(ToolCallValidationError::InvalidParameters {
2485                violations: all_errors,
2486            });
2487        }
2488
2489        Ok(())
2490    }
2491
2492    /// Check for unknown parameters in the provided arguments
2493    fn check_unknown_parameters(
2494        args: &serde_json::Map<String, Value>,
2495        properties: &serde_json::Map<String, Value>,
2496    ) -> Vec<ValidationError> {
2497        let mut errors = Vec::new();
2498
2499        // Get list of valid parameter names
2500        let valid_params: Vec<String> = properties.keys().map(|s| s.to_string()).collect();
2501
2502        // Check each provided argument
2503        for (arg_name, _) in args.iter() {
2504            if !properties.contains_key(arg_name) {
2505                // Create InvalidParameter error with suggestions
2506                errors.push(ValidationError::invalid_parameter(
2507                    arg_name.clone(),
2508                    &valid_params,
2509                ));
2510            }
2511        }
2512
2513        errors
2514    }
2515
2516    /// Check for missing required parameters
2517    fn check_missing_required(
2518        args: &serde_json::Map<String, Value>,
2519        properties: &serde_json::Map<String, Value>,
2520        required_params: &HashSet<&str>,
2521    ) -> Vec<ValidationError> {
2522        let mut errors = Vec::new();
2523
2524        for required_param in required_params {
2525            if !args.contains_key(*required_param) {
2526                // Get the parameter schema to extract description and type
2527                let param_schema = properties.get(*required_param);
2528
2529                let description = param_schema
2530                    .and_then(|schema| schema.get("description"))
2531                    .and_then(|d| d.as_str())
2532                    .map(|s| s.to_string());
2533
2534                let expected_type = param_schema
2535                    .and_then(Self::get_expected_type)
2536                    .unwrap_or_else(|| "unknown".to_string());
2537
2538                errors.push(ValidationError::MissingRequiredParameter {
2539                    parameter: (*required_param).to_string(),
2540                    description,
2541                    expected_type,
2542                });
2543            }
2544        }
2545
2546        errors
2547    }
2548
2549    /// Validate parameter values against their schemas
2550    fn validate_parameter_values(
2551        args: &serde_json::Map<String, Value>,
2552        properties: &serde_json::Map<String, Value>,
2553        required_params: &std::collections::HashSet<&str>,
2554    ) -> Vec<ValidationError> {
2555        let mut errors = Vec::new();
2556
2557        for (param_name, param_value) in args {
2558            if let Some(param_schema) = properties.get(param_name) {
2559                // Check if this is a null value to provide better error messages
2560                let is_null_value = param_value.is_null();
2561                let is_required = required_params.contains(param_name.as_str());
2562
2563                // Create a schema that wraps the parameter schema
2564                let schema = json!({
2565                    "type": "object",
2566                    "properties": {
2567                        param_name: param_schema
2568                    }
2569                });
2570
2571                // Compile the schema
2572                let compiled = match jsonschema::validator_for(&schema) {
2573                    Ok(compiled) => compiled,
2574                    Err(e) => {
2575                        errors.push(ValidationError::ConstraintViolation {
2576                            parameter: param_name.clone(),
2577                            message: format!(
2578                                "Failed to compile schema for parameter '{param_name}': {e}"
2579                            ),
2580                            field_path: None,
2581                            actual_value: None,
2582                            expected_type: None,
2583                            constraints: vec![],
2584                        });
2585                        continue;
2586                    }
2587                };
2588
2589                // Create an object with just this parameter to validate
2590                let instance = json!({ param_name: param_value });
2591
2592                // Validate and collect all errors for this parameter
2593                let validation_errors: Vec<_> =
2594                    compiled.validate(&instance).err().into_iter().collect();
2595
2596                for validation_error in validation_errors {
2597                    // Extract error details
2598                    let error_message = validation_error.to_string();
2599                    let instance_path_str = validation_error.instance_path().to_string();
2600                    let field_path = if instance_path_str.is_empty() || instance_path_str == "/" {
2601                        Some(param_name.clone())
2602                    } else {
2603                        Some(instance_path_str.trim_start_matches('/').to_string())
2604                    };
2605
2606                    // Extract constraints from the schema
2607                    let constraints = Self::extract_constraints_from_schema(param_schema);
2608
2609                    // Determine expected type
2610                    let expected_type = Self::get_expected_type(param_schema);
2611
2612                    // Generate context-aware error message for null values
2613                    // Check if this is a null value error (either top-level null or nested null in message)
2614                    // This is important because some LLMs might confuse "not required" with "nullable"
2615                    let maybe_type_error = match &validation_error.kind() {
2616                        ValidationErrorKind::Type { kind } => Some(kind),
2617                        _ => None,
2618                    };
2619                    let is_type_error = maybe_type_error.is_some();
2620                    let is_null_error = is_null_value
2621                        || (is_type_error && validation_error.instance().as_null().is_some());
2622                    let message = if is_null_error && let Some(type_error) = maybe_type_error {
2623                        // Extract the field name from field_path if available
2624                        let field_name = field_path.as_ref().unwrap_or(param_name);
2625
2626                        // Determine the expected type from the error message if not available from schema
2627                        let final_expected_type =
2628                            expected_type.clone().unwrap_or_else(|| match type_error {
2629                                TypeKind::Single(json_type) => json_type.to_string(),
2630                                TypeKind::Multiple(json_type_set) => json_type_set
2631                                    .iter()
2632                                    .map(|t| t.to_string())
2633                                    .collect::<Vec<_>>()
2634                                    .join(", "),
2635                            });
2636
2637                        // Check if this field is required by looking at the constraints
2638                        // Extract the actual field name from field_path (e.g., "request_body/name" -> "name")
2639                        let actual_field_name = field_path
2640                            .as_ref()
2641                            .and_then(|path| path.split('/').next_back())
2642                            .unwrap_or(param_name);
2643
2644                        // For nested fields (field_path contains '/'), only check the constraint
2645                        // For top-level fields, use the is_required parameter
2646                        let is_nested_field = field_path.as_ref().is_some_and(|p| p.contains('/'));
2647
2648                        let field_is_required = if is_nested_field {
2649                            constraints.iter().any(|c| {
2650                                if let ValidationConstraint::Required { properties } = c {
2651                                    properties.contains(&actual_field_name.to_string())
2652                                } else {
2653                                    false
2654                                }
2655                            })
2656                        } else {
2657                            is_required
2658                        };
2659
2660                        if field_is_required {
2661                            format!(
2662                                "Parameter '{field_name}' is required and must not be null (expected: {final_expected_type})"
2663                            )
2664                        } else {
2665                            format!(
2666                                "Parameter '{field_name}' is optional but must not be null (expected: {final_expected_type})"
2667                            )
2668                        }
2669                    } else {
2670                        error_message
2671                    };
2672
2673                    errors.push(ValidationError::ConstraintViolation {
2674                        parameter: param_name.clone(),
2675                        message,
2676                        field_path,
2677                        actual_value: Some(Box::new(param_value.clone())),
2678                        expected_type,
2679                        constraints,
2680                    });
2681                }
2682            }
2683        }
2684
2685        errors
2686    }
2687
2688    /// Extract validation constraints from a schema
2689    fn extract_constraints_from_schema(schema: &Value) -> Vec<ValidationConstraint> {
2690        let mut constraints = Vec::new();
2691
2692        // Minimum value constraint
2693        if let Some(min_value) = schema.get("minimum").and_then(|v| v.as_f64()) {
2694            let exclusive = schema
2695                .get("exclusiveMinimum")
2696                .and_then(|v| v.as_bool())
2697                .unwrap_or(false);
2698            constraints.push(ValidationConstraint::Minimum {
2699                value: min_value,
2700                exclusive,
2701            });
2702        }
2703
2704        // Maximum value constraint
2705        if let Some(max_value) = schema.get("maximum").and_then(|v| v.as_f64()) {
2706            let exclusive = schema
2707                .get("exclusiveMaximum")
2708                .and_then(|v| v.as_bool())
2709                .unwrap_or(false);
2710            constraints.push(ValidationConstraint::Maximum {
2711                value: max_value,
2712                exclusive,
2713            });
2714        }
2715
2716        // Minimum length constraint
2717        if let Some(min_len) = schema
2718            .get("minLength")
2719            .and_then(|v| v.as_u64())
2720            .map(|v| v as usize)
2721        {
2722            constraints.push(ValidationConstraint::MinLength { value: min_len });
2723        }
2724
2725        // Maximum length constraint
2726        if let Some(max_len) = schema
2727            .get("maxLength")
2728            .and_then(|v| v.as_u64())
2729            .map(|v| v as usize)
2730        {
2731            constraints.push(ValidationConstraint::MaxLength { value: max_len });
2732        }
2733
2734        // Pattern constraint
2735        if let Some(pattern) = schema
2736            .get("pattern")
2737            .and_then(|v| v.as_str())
2738            .map(|s| s.to_string())
2739        {
2740            constraints.push(ValidationConstraint::Pattern { pattern });
2741        }
2742
2743        // Enum values constraint
2744        if let Some(enum_values) = schema.get("enum").and_then(|v| v.as_array()).cloned() {
2745            constraints.push(ValidationConstraint::EnumValues {
2746                values: enum_values,
2747            });
2748        }
2749
2750        // Format constraint
2751        if let Some(format) = schema
2752            .get("format")
2753            .and_then(|v| v.as_str())
2754            .map(|s| s.to_string())
2755        {
2756            constraints.push(ValidationConstraint::Format { format });
2757        }
2758
2759        // Multiple of constraint
2760        if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
2761            constraints.push(ValidationConstraint::MultipleOf { value: multiple_of });
2762        }
2763
2764        // Minimum items constraint
2765        if let Some(min_items) = schema
2766            .get("minItems")
2767            .and_then(|v| v.as_u64())
2768            .map(|v| v as usize)
2769        {
2770            constraints.push(ValidationConstraint::MinItems { value: min_items });
2771        }
2772
2773        // Maximum items constraint
2774        if let Some(max_items) = schema
2775            .get("maxItems")
2776            .and_then(|v| v.as_u64())
2777            .map(|v| v as usize)
2778        {
2779            constraints.push(ValidationConstraint::MaxItems { value: max_items });
2780        }
2781
2782        // Unique items constraint
2783        if let Some(true) = schema.get("uniqueItems").and_then(|v| v.as_bool()) {
2784            constraints.push(ValidationConstraint::UniqueItems);
2785        }
2786
2787        // Minimum properties constraint
2788        if let Some(min_props) = schema
2789            .get("minProperties")
2790            .and_then(|v| v.as_u64())
2791            .map(|v| v as usize)
2792        {
2793            constraints.push(ValidationConstraint::MinProperties { value: min_props });
2794        }
2795
2796        // Maximum properties constraint
2797        if let Some(max_props) = schema
2798            .get("maxProperties")
2799            .and_then(|v| v.as_u64())
2800            .map(|v| v as usize)
2801        {
2802            constraints.push(ValidationConstraint::MaxProperties { value: max_props });
2803        }
2804
2805        // Constant value constraint
2806        if let Some(const_value) = schema.get("const").cloned() {
2807            constraints.push(ValidationConstraint::ConstValue { value: const_value });
2808        }
2809
2810        // Required properties constraint
2811        if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
2812            let properties: Vec<String> = required
2813                .iter()
2814                .filter_map(|v| v.as_str().map(|s| s.to_string()))
2815                .collect();
2816            if !properties.is_empty() {
2817                constraints.push(ValidationConstraint::Required { properties });
2818            }
2819        }
2820
2821        constraints
2822    }
2823
2824    /// Get the expected type from a schema
2825    fn get_expected_type(schema: &Value) -> Option<String> {
2826        if let Some(type_value) = schema.get("type") {
2827            if let Some(type_str) = type_value.as_str() {
2828                return Some(type_str.to_string());
2829            } else if let Some(type_array) = type_value.as_array() {
2830                // Handle multiple types (e.g., ["string", "null"])
2831                let types: Vec<String> = type_array
2832                    .iter()
2833                    .filter_map(|v| v.as_str())
2834                    .map(|s| s.to_string())
2835                    .collect();
2836                if !types.is_empty() {
2837                    return Some(types.join(" | "));
2838                }
2839            }
2840        }
2841        None
2842    }
2843
2844    /// Wrap an output schema to include both success and error responses
2845    ///
2846    /// This function creates a unified response schema that can represent both successful
2847    /// responses and error responses. It uses `json!()` macro instead of `schema_for!()`
2848    /// for several important reasons:
2849    ///
2850    /// 1. **Dynamic Schema Construction**: The success schema is dynamically converted from
2851    ///    OpenAPI specifications at runtime, not from a static Rust type. The `schema_for!()`
2852    ///    macro requires a compile-time type, but we're working with schemas that are only
2853    ///    known when parsing the OpenAPI spec.
2854    ///
2855    /// 2. **Composite Schema Building**: The function builds a complex wrapper schema that:
2856    ///    - Contains a dynamically-converted OpenAPI schema for success responses
2857    ///    - Includes a statically-typed error schema (which does use `schema_for!()`)
2858    ///    - Adds metadata fields like HTTP status codes and descriptions
2859    ///    - Uses JSON Schema's `oneOf` to allow either success or error responses
2860    ///
2861    /// 3. **Runtime Flexibility**: OpenAPI schemas can have arbitrary complexity and types
2862    ///    that don't map directly to Rust types. Using `json!()` allows us to construct
2863    ///    the exact JSON Schema structure needed without being constrained by Rust's type system.
2864    ///
2865    /// The error schema component does use `schema_for!(ErrorResponse)` (via `create_error_response_schema()`)
2866    /// because `ErrorResponse` is a known Rust type, but the overall wrapper must be built dynamically.
2867    fn wrap_output_schema(
2868        body_schema: &ObjectOrReference<ObjectSchema>,
2869        spec: &Spec,
2870    ) -> Result<Value, Error> {
2871        // Convert the body schema to JSON
2872        let mut visited = HashSet::new();
2873        let body_schema_json = match body_schema {
2874            ObjectOrReference::Object(obj_schema) => {
2875                Self::convert_object_schema_to_json_schema(obj_schema, spec, &mut visited)?
2876            }
2877            ObjectOrReference::Ref { ref_path, .. } => {
2878                let resolved = Self::resolve_reference(ref_path, spec, &mut visited)?;
2879                let result =
2880                    Self::convert_object_schema_to_json_schema(&resolved, spec, &mut visited)?;
2881                // Remove after conversion to allow schema reuse (see convert_schema_to_json_schema)
2882                visited.remove(ref_path);
2883                result
2884            }
2885        };
2886
2887        let error_schema = create_error_response_schema();
2888
2889        Ok(json!({
2890            "type": "object",
2891            "description": "Unified response structure with success and error variants",
2892            "required": ["status", "body"],
2893            "additionalProperties": false,
2894            "properties": {
2895                "status": {
2896                    "type": "integer",
2897                    "description": "HTTP status code",
2898                    "minimum": 100,
2899                    "maximum": 599
2900                },
2901                "body": {
2902                    "description": "Response body - either success data or error information",
2903                    "oneOf": [
2904                        body_schema_json,
2905                        error_schema
2906                    ]
2907                }
2908            }
2909        }))
2910    }
2911
2912    /// Check if a schema represents a file field based on its format.
2913    ///
2914    /// Returns `true` if the schema has `format: binary` or `format: byte`,
2915    /// which indicates a file upload field in multipart/form-data requests.
2916    ///
2917    /// # Arguments
2918    /// * `schema` - The OpenAPI Schema to check
2919    ///
2920    /// # Returns
2921    /// `true` if the schema represents a file field, `false` otherwise
2922    #[must_use]
2923    pub fn is_file_field(schema: &Schema) -> bool {
2924        match schema {
2925            Schema::Object(obj_or_ref) => match obj_or_ref.as_ref() {
2926                ObjectOrReference::Object(obj_schema) => {
2927                    Self::is_file_field_object_schema(obj_schema)
2928                }
2929                ObjectOrReference::Ref { .. } => {
2930                    // References need to be resolved first; return false for unresolved refs
2931                    false
2932                }
2933            },
2934            Schema::Boolean(_) => false,
2935        }
2936    }
2937
2938    /// Check if an ObjectSchema represents a file field based on its format.
2939    ///
2940    /// Returns `true` if the schema has `format: binary` or `format: byte`,
2941    /// which indicates a file upload field in multipart/form-data requests.
2942    fn is_file_field_object_schema(obj_schema: &ObjectSchema) -> bool {
2943        if let Some(format) = &obj_schema.format {
2944            format == "binary" || format == "byte"
2945        } else {
2946            false
2947        }
2948    }
2949
2950    /// Check if an ObjectOrReference<ObjectSchema> represents a file field.
2951    ///
2952    /// This is a convenience method for checking file fields when iterating
2953    /// over properties in a multipart/form-data schema.
2954    fn is_file_field_property(prop_schema: &ObjectOrReference<ObjectSchema>) -> bool {
2955        match prop_schema {
2956            ObjectOrReference::Object(obj_schema) => Self::is_file_field_object_schema(obj_schema),
2957            ObjectOrReference::Ref { .. } => {
2958                // References need to be resolved first; return false for unresolved refs
2959                false
2960            }
2961        }
2962    }
2963
2964    /// Convert a file field to the structured file object schema.
2965    ///
2966    /// Transforms a file field (format: binary or byte) into a structured
2967    /// object schema with `content` (required) and `filename` (optional) properties.
2968    /// The content field expects a data URI format (e.g., `data:image/png;base64,...`).
2969    ///
2970    /// # Arguments
2971    /// * `original_description` - The original description from the OpenAPI schema
2972    ///
2973    /// # Returns
2974    /// A JSON Schema value representing the file object structure
2975    fn convert_file_field_to_schema(original_description: Option<&str>) -> Value {
2976        let description = original_description.unwrap_or("File upload");
2977        json!({
2978            "type": "object",
2979            "description": description,
2980            "properties": {
2981                "content": {
2982                    "type": "string",
2983                    "description": "File content as data URI (e.g., data:image/png;base64,...)"
2984                },
2985                "filename": {
2986                    "type": "string",
2987                    "description": "Optional filename for the upload"
2988                }
2989            },
2990            "required": ["content"]
2991        })
2992    }
2993}
2994
2995/// Create the error schema structure that all tool errors conform to
2996fn create_error_response_schema() -> Value {
2997    let root_schema = schema_for!(ErrorResponse);
2998    let schema_json = serde_json::to_value(root_schema).expect("Valid error schema");
2999
3000    // Extract definitions/defs for inlining
3001    let definitions = schema_json
3002        .get("$defs")
3003        .or_else(|| schema_json.get("definitions"))
3004        .cloned()
3005        .unwrap_or_else(|| json!({}));
3006
3007    // Clone the schema and remove metadata
3008    let mut result = schema_json.clone();
3009    if let Some(obj) = result.as_object_mut() {
3010        obj.remove("$schema");
3011        obj.remove("$defs");
3012        obj.remove("definitions");
3013        obj.remove("title");
3014    }
3015
3016    // Inline all references
3017    inline_refs(&mut result, &definitions);
3018
3019    result
3020}
3021
3022/// Recursively inline all $ref references in a JSON Schema
3023fn inline_refs(schema: &mut Value, definitions: &Value) {
3024    match schema {
3025        Value::Object(obj) => {
3026            // Check if this object has a $ref
3027            if let Some(ref_value) = obj.get("$ref").cloned()
3028                && let Some(ref_str) = ref_value.as_str()
3029            {
3030                // Extract the definition name from the ref
3031                let def_name = ref_str
3032                    .strip_prefix("#/$defs/")
3033                    .or_else(|| ref_str.strip_prefix("#/definitions/"));
3034
3035                if let Some(name) = def_name
3036                    && let Some(definition) = definitions.get(name)
3037                {
3038                    // Replace the entire object with the definition
3039                    *schema = definition.clone();
3040                    // Continue to inline any refs in the definition
3041                    inline_refs(schema, definitions);
3042                    return;
3043                }
3044            }
3045
3046            // Recursively process all values in the object
3047            for (_, value) in obj.iter_mut() {
3048                inline_refs(value, definitions);
3049            }
3050        }
3051        Value::Array(arr) => {
3052            // Recursively process all items in the array
3053            for item in arr.iter_mut() {
3054                inline_refs(item, definitions);
3055            }
3056        }
3057        _ => {} // Other types don't contain refs
3058    }
3059}
3060
3061/// Query parameter with explode information
3062#[derive(Debug, Clone)]
3063pub struct QueryParameter {
3064    pub value: Value,
3065    pub explode: bool,
3066}
3067
3068impl QueryParameter {
3069    pub fn new(value: Value, explode: bool) -> Self {
3070        Self { value, explode }
3071    }
3072}
3073
3074/// Extracted parameters from MCP tool call
3075#[derive(Debug, Clone)]
3076pub struct ExtractedParameters {
3077    pub path: HashMap<String, Value>,
3078    pub query: HashMap<String, QueryParameter>,
3079    pub headers: HashMap<String, Value>,
3080    pub cookies: HashMap<String, Value>,
3081    pub body: HashMap<String, Value>,
3082    pub config: RequestConfig,
3083}
3084
3085/// Request configuration options
3086#[derive(Debug, Clone)]
3087pub struct RequestConfig {
3088    pub timeout_seconds: u32,
3089    pub content_type: String,
3090}
3091
3092impl Default for RequestConfig {
3093    fn default() -> Self {
3094        Self {
3095            timeout_seconds: 30,
3096            content_type: mime::APPLICATION_JSON.to_string(),
3097        }
3098    }
3099}
3100
3101#[cfg(test)]
3102mod tests {
3103    use super::*;
3104
3105    use insta::assert_json_snapshot;
3106    use oas3::spec::{
3107        BooleanSchema, Components, MediaType, ObjectOrReference, ObjectSchema, Operation,
3108        Parameter, ParameterIn, RequestBody, Schema, SchemaType, SchemaTypeSet, Spec,
3109    };
3110    use rmcp::model::Tool;
3111    use serde_json::{Value, json};
3112    use std::collections::BTreeMap;
3113
3114    #[test]
3115    fn converter_preserves_schema_level_examples_plural() {
3116        let spec = create_test_spec();
3117        let schema: ObjectSchema = serde_json::from_value(json!({
3118            "type": "string",
3119            "examples": ["a", "a.b", "a.b.c"],
3120        }))
3121        .expect("valid object schema");
3122        let mut visited = std::collections::HashSet::new();
3123        let result =
3124            ToolGenerator::convert_object_schema_to_json_schema(&schema, &spec, &mut visited)
3125                .expect("conversion succeeds");
3126        assert_eq!(result["type"], json!("string"));
3127        assert_eq!(
3128            result["examples"],
3129            json!(["a", "a.b", "a.b.c"]),
3130            "schema-level plural `examples` must be preserved: {result}"
3131        );
3132    }
3133
3134    fn parameter_with_singular_and_named_map_examples() -> Parameter {
3135        serde_json::from_value(json!({
3136            "name": "q",
3137            "in": "query",
3138            "schema": { "type": "string" },
3139            "example": "alpha",
3140            "examples": {
3141                "beta": { "value": "beta" },
3142                "gamma": { "value": "gamma" },
3143            },
3144        }))
3145        .expect("valid parameter")
3146    }
3147
3148    #[test]
3149    fn parameter_examples_default_to_structured_field() {
3150        let spec = create_test_spec();
3151        let param = parameter_with_singular_and_named_map_examples();
3152        // Default: examples chained from all sources into the structured `examples` field,
3153        // not duplicated into the description.
3154        let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3155            &param,
3156            ParameterIn::Query,
3157            &spec,
3158            false,
3159            false,
3160        )
3161        .expect("conversion succeeds");
3162        let values: Vec<String> = result["examples"]
3163            .as_array()
3164            .expect("structured `examples` present")
3165            .iter()
3166            .filter_map(|value| value.as_str().map(ToString::to_string))
3167            .collect();
3168        assert!(
3169            values.iter().any(|v| v == "alpha")
3170                && values.iter().any(|v| v == "beta")
3171                && values.iter().any(|v| v == "gamma"),
3172            "all sources chained into structured `examples`: {result}"
3173        );
3174        let description = result["description"].as_str().unwrap_or_default();
3175        assert!(
3176            !description.contains("alpha") && !description.contains("beta"),
3177            "examples must not be duplicated into the description by default: {description}"
3178        );
3179    }
3180
3181    #[test]
3182    fn parameter_examples_in_description_when_flag_set() {
3183        let spec = create_test_spec();
3184        let param = parameter_with_singular_and_named_map_examples();
3185        // Flag on: examples folded into the description, omitted from the structured field.
3186        let (result, _annotations) =
3187            ToolGenerator::convert_parameter_schema(&param, ParameterIn::Query, &spec, false, true)
3188                .expect("conversion succeeds");
3189        let description = result["description"].as_str().unwrap_or_default();
3190        assert!(
3191            description.contains("alpha")
3192                && description.contains("beta")
3193                && description.contains("gamma"),
3194            "examples folded into description: {description}"
3195        );
3196        assert!(
3197            result.get("examples").is_none(),
3198            "structured `examples` omitted when folding into the description: {result}"
3199        );
3200    }
3201
3202    #[test]
3203    fn array_parameter_lifts_item_examples_to_parameter_level() {
3204        let spec = create_test_spec();
3205        // An array parameter whose element schema carries `examples` (the natural place for
3206        // per-element examples). MCP clients and the inspector read examples at the parameter
3207        // level, not nested under `items`, so they must be surfaced there.
3208        let param: Parameter = serde_json::from_value(json!({
3209            "name": "include",
3210            "in": "query",
3211            "schema": {
3212                "type": "array",
3213                "items": { "type": "string", "examples": ["camera", "mesh.primitives"] },
3214            },
3215        }))
3216        .expect("valid parameter");
3217        let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3218            &param,
3219            ParameterIn::Query,
3220            &spec,
3221            false,
3222            false,
3223        )
3224        .expect("conversion succeeds");
3225        // Each element example is lifted to the parameter level wrapped as a single-element
3226        // array, since a valid value for an array parameter is itself an array.
3227        assert_eq!(
3228            result["examples"],
3229            json!([["camera"], ["mesh.primitives"]]),
3230            "array element examples must be lifted to parameter-level examples: {result}"
3231        );
3232    }
3233
3234    #[test]
3235    fn lifting_array_item_examples_clears_them_from_items() {
3236        let spec = create_test_spec();
3237        let param: Parameter = serde_json::from_value(json!({
3238            "name": "include",
3239            "in": "query",
3240            "schema": {
3241                "type": "array",
3242                "items": { "type": "string", "examples": ["camera", "mesh.primitives"] },
3243            },
3244        }))
3245        .expect("valid parameter");
3246        let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3247            &param,
3248            ParameterIn::Query,
3249            &spec,
3250            false,
3251            false,
3252        )
3253        .expect("conversion succeeds");
3254        // Once lifted to the parameter level, the examples must not also remain under `items`:
3255        // a single channel, so their tokens are not duplicated.
3256        assert!(
3257            result["items"].get("examples").is_none(),
3258            "item-level examples must be cleared once lifted to the parameter level: {result}"
3259        );
3260    }
3261
3262    #[test]
3263    fn array_parameter_examples_lift_snapshot() {
3264        let spec = create_test_spec();
3265        // Mirrors a JSON:API `include` parameter: an array of relationship-path strings whose
3266        // element schema carries representative `examples` and a grammar description. The snapshot
3267        // renders the converted MCP input schema so the lifted, parameter-level shape is reviewable.
3268        let param: Parameter = serde_json::from_value(json!({
3269            "name": "include",
3270            "in": "query",
3271            "description": "Relationship paths to include.",
3272            "schema": {
3273                "type": "array",
3274                "items": {
3275                    "type": "string",
3276                    "description": "A relationship path: a dot-separated chain of relationship names.",
3277                    "examples": ["camera", "mesh.primitives.material", "mesh.primitives.indices"],
3278                },
3279            },
3280        }))
3281        .expect("valid parameter");
3282        let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3283            &param,
3284            ParameterIn::Query,
3285            &spec,
3286            false,
3287            false,
3288        )
3289        .expect("conversion succeeds");
3290        assert_json_snapshot!("array_parameter_examples_lift", result);
3291    }
3292
3293    /// Create a minimal test OpenAPI spec for testing purposes
3294    fn create_test_spec() -> Spec {
3295        Spec {
3296            openapi: "3.0.0".to_string(),
3297            info: oas3::spec::Info {
3298                title: "Test API".to_string(),
3299                version: "1.0.0".to_string(),
3300                summary: None,
3301                description: Some("Test API for unit tests".to_string()),
3302                terms_of_service: None,
3303                contact: None,
3304                license: None,
3305                extensions: Default::default(),
3306            },
3307            components: Some(Components {
3308                schemas: BTreeMap::new(),
3309                responses: BTreeMap::new(),
3310                parameters: BTreeMap::new(),
3311                examples: BTreeMap::new(),
3312                request_bodies: BTreeMap::new(),
3313                headers: BTreeMap::new(),
3314                security_schemes: BTreeMap::new(),
3315                links: BTreeMap::new(),
3316                callbacks: BTreeMap::new(),
3317                path_items: BTreeMap::new(),
3318                extensions: Default::default(),
3319            }),
3320            servers: vec![],
3321            paths: None,
3322            external_docs: None,
3323            tags: vec![],
3324            security: vec![],
3325            webhooks: BTreeMap::new(),
3326            extensions: Default::default(),
3327        }
3328    }
3329
3330    fn validate_tool_against_mcp_schema(metadata: &ToolMetadata) {
3331        let schema_content = std::fs::read_to_string("schema/2025-06-18/schema.json")
3332            .expect("Failed to read MCP schema file");
3333        let full_schema: Value =
3334            serde_json::from_str(&schema_content).expect("Failed to parse MCP schema JSON");
3335
3336        // Create a schema that references the Tool definition from the full schema
3337        let tool_schema = json!({
3338            "$schema": "http://json-schema.org/draft-07/schema#",
3339            "definitions": full_schema.get("definitions"),
3340            "$ref": "#/definitions/Tool"
3341        });
3342
3343        let validator =
3344            jsonschema::validator_for(&tool_schema).expect("Failed to compile MCP Tool schema");
3345
3346        // Convert ToolMetadata to MCP Tool format using the From trait
3347        let tool = Tool::from(metadata);
3348
3349        // Serialize the Tool to JSON for validation
3350        let mcp_tool_json = serde_json::to_value(&tool).expect("Failed to serialize Tool to JSON");
3351
3352        // Validate the generated tool against MCP schema
3353        let errors: Vec<String> = validator
3354            .iter_errors(&mcp_tool_json)
3355            .map(|e| e.to_string())
3356            .collect();
3357
3358        if !errors.is_empty() {
3359            panic!("Generated tool failed MCP schema validation: {errors:?}");
3360        }
3361    }
3362
3363    #[test]
3364    fn test_error_schema_structure() {
3365        let error_schema = create_error_response_schema();
3366
3367        // Should not contain $schema or definitions at top level
3368        assert!(error_schema.get("$schema").is_none());
3369        assert!(error_schema.get("definitions").is_none());
3370
3371        // Verify the structure using snapshot
3372        assert_json_snapshot!(error_schema);
3373    }
3374
3375    #[test]
3376    fn test_petstore_get_pet_by_id() {
3377        use oas3::spec::Response;
3378
3379        let mut operation = Operation {
3380            operation_id: Some("getPetById".to_string()),
3381            summary: Some("Find pet by ID".to_string()),
3382            description: Some("Returns a single pet".to_string()),
3383            tags: vec![],
3384            external_docs: None,
3385            parameters: vec![],
3386            request_body: None,
3387            responses: Default::default(),
3388            callbacks: Default::default(),
3389            deprecated: Some(false),
3390            security: vec![],
3391            servers: vec![],
3392            extensions: Default::default(),
3393        };
3394
3395        // Create a path parameter
3396        let param = Parameter {
3397            name: "petId".to_string(),
3398            location: ParameterIn::Path,
3399            description: Some("ID of pet to return".to_string()),
3400            required: Some(true),
3401            deprecated: Some(false),
3402            allow_empty_value: Some(false),
3403            style: None,
3404            explode: None,
3405            allow_reserved: Some(false),
3406            schema: Some(ObjectOrReference::Object(ObjectSchema {
3407                schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3408                minimum: Some(serde_json::Number::from(1_i64)),
3409                format: Some("int64".to_string()),
3410                ..Default::default()
3411            })),
3412            example: None,
3413            examples: Default::default(),
3414            content: None,
3415            extensions: Default::default(),
3416        };
3417
3418        operation.parameters.push(ObjectOrReference::Object(param));
3419
3420        // Add a 200 response with Pet schema
3421        let mut responses = BTreeMap::new();
3422        let mut content = BTreeMap::new();
3423        content.insert(
3424            "application/json".to_string(),
3425            MediaType {
3426                extensions: Default::default(),
3427                schema: Some(ObjectOrReference::Object(ObjectSchema {
3428                    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3429                    properties: {
3430                        let mut props = BTreeMap::new();
3431                        props.insert(
3432                            "id".to_string(),
3433                            ObjectOrReference::Object(ObjectSchema {
3434                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3435                                format: Some("int64".to_string()),
3436                                ..Default::default()
3437                            }),
3438                        );
3439                        props.insert(
3440                            "name".to_string(),
3441                            ObjectOrReference::Object(ObjectSchema {
3442                                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3443                                ..Default::default()
3444                            }),
3445                        );
3446                        props.insert(
3447                            "status".to_string(),
3448                            ObjectOrReference::Object(ObjectSchema {
3449                                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3450                                ..Default::default()
3451                            }),
3452                        );
3453                        props
3454                    },
3455                    required: vec!["id".to_string(), "name".to_string()],
3456                    ..Default::default()
3457                })),
3458                examples: None,
3459                encoding: Default::default(),
3460            },
3461        );
3462
3463        responses.insert(
3464            "200".to_string(),
3465            ObjectOrReference::Object(Response {
3466                description: Some("successful operation".to_string()),
3467                headers: Default::default(),
3468                content,
3469                links: Default::default(),
3470                extensions: Default::default(),
3471            }),
3472        );
3473        operation.responses = Some(responses);
3474
3475        let spec = create_test_spec();
3476        let metadata = ToolGenerator::generate_tool_metadata(
3477            &operation,
3478            "get".to_string(),
3479            "/pet/{petId}".to_string(),
3480            &spec,
3481            false,
3482            false,
3483            false,
3484        )
3485        .unwrap();
3486
3487        assert_eq!(metadata.name, "getPetById");
3488        assert_eq!(metadata.method, "get");
3489        assert_eq!(metadata.path, "/pet/{petId}");
3490        assert!(
3491            metadata
3492                .description
3493                .clone()
3494                .unwrap()
3495                .contains("Find pet by ID")
3496        );
3497
3498        // Check output_schema is included and correct
3499        assert!(metadata.output_schema.is_some());
3500        let output_schema = metadata.output_schema.as_ref().unwrap();
3501
3502        // Use snapshot testing for the output schema
3503        insta::assert_json_snapshot!("test_petstore_get_pet_by_id_output_schema", output_schema);
3504
3505        // Validate against MCP Tool schema
3506        validate_tool_against_mcp_schema(&metadata);
3507    }
3508
3509    #[test]
3510    fn test_convert_prefix_items_to_draft07_mixed_types() {
3511        // Test prefixItems with mixed types and items:false
3512
3513        let prefix_items = vec![
3514            ObjectOrReference::Object(ObjectSchema {
3515                schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3516                format: Some("int32".to_string()),
3517                ..Default::default()
3518            }),
3519            ObjectOrReference::Object(ObjectSchema {
3520                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3521                ..Default::default()
3522            }),
3523        ];
3524
3525        // items: false (no additional items allowed)
3526        let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
3527
3528        let mut result = serde_json::Map::new();
3529        let spec = create_test_spec();
3530        ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
3531            .unwrap();
3532
3533        // Use JSON snapshot for the schema
3534        insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_mixed_types", result);
3535    }
3536
3537    #[test]
3538    fn test_convert_prefix_items_to_draft07_uniform_types() {
3539        // Test prefixItems with uniform types
3540        let prefix_items = vec![
3541            ObjectOrReference::Object(ObjectSchema {
3542                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3543                ..Default::default()
3544            }),
3545            ObjectOrReference::Object(ObjectSchema {
3546                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3547                ..Default::default()
3548            }),
3549        ];
3550
3551        // items: false
3552        let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
3553
3554        let mut result = serde_json::Map::new();
3555        let spec = create_test_spec();
3556        ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
3557            .unwrap();
3558
3559        // Use JSON snapshot for the schema
3560        insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_uniform_types", result);
3561    }
3562
3563    #[test]
3564    fn test_array_with_prefix_items_integration() {
3565        // Integration test: parameter with prefixItems and items:false
3566        let param = Parameter {
3567            name: "coordinates".to_string(),
3568            location: ParameterIn::Query,
3569            description: Some("X,Y coordinates as tuple".to_string()),
3570            required: Some(true),
3571            deprecated: Some(false),
3572            allow_empty_value: Some(false),
3573            style: None,
3574            explode: None,
3575            allow_reserved: Some(false),
3576            schema: Some(ObjectOrReference::Object(ObjectSchema {
3577                schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3578                prefix_items: vec![
3579                    ObjectOrReference::Object(ObjectSchema {
3580                        schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
3581                        format: Some("double".to_string()),
3582                        ..Default::default()
3583                    }),
3584                    ObjectOrReference::Object(ObjectSchema {
3585                        schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
3586                        format: Some("double".to_string()),
3587                        ..Default::default()
3588                    }),
3589                ],
3590                items: Some(Box::new(Schema::Boolean(BooleanSchema(false)))),
3591                ..Default::default()
3592            })),
3593            example: None,
3594            examples: Default::default(),
3595            content: None,
3596            extensions: Default::default(),
3597        };
3598
3599        let spec = create_test_spec();
3600        let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3601            &param,
3602            ParameterIn::Query,
3603            &spec,
3604            false,
3605            false,
3606        )
3607        .unwrap();
3608
3609        // Use JSON snapshot for the schema
3610        insta::assert_json_snapshot!("test_array_with_prefix_items_integration", result);
3611    }
3612
3613    #[test]
3614    fn test_skip_tool_description() {
3615        let operation = Operation {
3616            operation_id: Some("getPetById".to_string()),
3617            summary: Some("Find pet by ID".to_string()),
3618            description: Some("Returns a single pet".to_string()),
3619            tags: vec![],
3620            external_docs: None,
3621            parameters: vec![],
3622            request_body: None,
3623            responses: Default::default(),
3624            callbacks: Default::default(),
3625            deprecated: Some(false),
3626            security: vec![],
3627            servers: vec![],
3628            extensions: Default::default(),
3629        };
3630
3631        let spec = create_test_spec();
3632        let metadata = ToolGenerator::generate_tool_metadata(
3633            &operation,
3634            "get".to_string(),
3635            "/pet/{petId}".to_string(),
3636            &spec,
3637            true,
3638            false,
3639            false,
3640        )
3641        .unwrap();
3642
3643        assert_eq!(metadata.name, "getPetById");
3644        assert_eq!(metadata.method, "get");
3645        assert_eq!(metadata.path, "/pet/{petId}");
3646        assert!(metadata.description.is_none());
3647
3648        // Use snapshot testing for the output schema
3649        insta::assert_json_snapshot!("test_skip_tool_description", metadata);
3650
3651        // Validate against MCP Tool schema
3652        validate_tool_against_mcp_schema(&metadata);
3653    }
3654
3655    #[test]
3656    fn test_keep_tool_description() {
3657        let description = Some("Returns a single pet".to_string());
3658        let operation = Operation {
3659            operation_id: Some("getPetById".to_string()),
3660            summary: Some("Find pet by ID".to_string()),
3661            description: description.clone(),
3662            tags: vec![],
3663            external_docs: None,
3664            parameters: vec![],
3665            request_body: None,
3666            responses: Default::default(),
3667            callbacks: Default::default(),
3668            deprecated: Some(false),
3669            security: vec![],
3670            servers: vec![],
3671            extensions: Default::default(),
3672        };
3673
3674        let spec = create_test_spec();
3675        let metadata = ToolGenerator::generate_tool_metadata(
3676            &operation,
3677            "get".to_string(),
3678            "/pet/{petId}".to_string(),
3679            &spec,
3680            false,
3681            false,
3682            false,
3683        )
3684        .unwrap();
3685
3686        assert_eq!(metadata.name, "getPetById");
3687        assert_eq!(metadata.method, "get");
3688        assert_eq!(metadata.path, "/pet/{petId}");
3689        assert!(metadata.description.is_some());
3690
3691        // Use snapshot testing for the output schema
3692        insta::assert_json_snapshot!("test_keep_tool_description", metadata);
3693
3694        // Validate against MCP Tool schema
3695        validate_tool_against_mcp_schema(&metadata);
3696    }
3697
3698    #[test]
3699    fn test_skip_parameter_descriptions() {
3700        let param = Parameter {
3701            name: "status".to_string(),
3702            location: ParameterIn::Query,
3703            description: Some("Filter by status".to_string()),
3704            required: Some(false),
3705            deprecated: Some(false),
3706            allow_empty_value: Some(false),
3707            style: None,
3708            explode: None,
3709            allow_reserved: Some(false),
3710            schema: Some(ObjectOrReference::Object(ObjectSchema {
3711                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3712                enum_values: vec![json!("available"), json!("pending"), json!("sold")],
3713                ..Default::default()
3714            })),
3715            example: Some(json!("available")),
3716            examples: Default::default(),
3717            content: None,
3718            extensions: Default::default(),
3719        };
3720
3721        let spec = create_test_spec();
3722        let (schema, _) =
3723            ToolGenerator::convert_parameter_schema(&param, ParameterIn::Query, &spec, true, false)
3724                .unwrap();
3725
3726        // When skip_parameter_descriptions is true, description should not be present
3727        assert!(schema.get("description").is_none());
3728
3729        // Other properties should still be present; the example surfaces in the structured
3730        // `examples` field by default (not the deprecated singular `example`).
3731        assert_eq!(schema.get("type").unwrap(), "string");
3732        assert!(schema.get("example").is_none());
3733        assert_eq!(schema.get("examples").unwrap(), &json!(["available"]));
3734
3735        insta::assert_json_snapshot!("test_skip_parameter_descriptions", schema);
3736    }
3737
3738    #[test]
3739    fn test_keep_parameter_descriptions() {
3740        let param = Parameter {
3741            name: "status".to_string(),
3742            location: ParameterIn::Query,
3743            description: Some("Filter by status".to_string()),
3744            required: Some(false),
3745            deprecated: Some(false),
3746            allow_empty_value: Some(false),
3747            style: None,
3748            explode: None,
3749            allow_reserved: Some(false),
3750            schema: Some(ObjectOrReference::Object(ObjectSchema {
3751                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3752                enum_values: vec![json!("available"), json!("pending"), json!("sold")],
3753                ..Default::default()
3754            })),
3755            example: Some(json!("available")),
3756            examples: Default::default(),
3757            content: None,
3758            extensions: Default::default(),
3759        };
3760
3761        let spec = create_test_spec();
3762        let (schema, _) = ToolGenerator::convert_parameter_schema(
3763            &param,
3764            ParameterIn::Query,
3765            &spec,
3766            false,
3767            false,
3768        )
3769        .unwrap();
3770
3771        // When skip_parameter_descriptions is false, description should be present — but by
3772        // default it carries no examples (those go to the structured `examples` field).
3773        assert!(schema.get("description").is_some());
3774        let description = schema.get("description").unwrap().as_str().unwrap();
3775        assert!(description.contains("Filter by status"));
3776        assert!(!description.contains("Example:"));
3777
3778        // Other properties should also be present; the example is structured.
3779        assert_eq!(schema.get("type").unwrap(), "string");
3780        assert!(schema.get("example").is_none());
3781        assert_eq!(schema.get("examples").unwrap(), &json!(["available"]));
3782
3783        insta::assert_json_snapshot!("test_keep_parameter_descriptions", schema);
3784    }
3785
3786    #[test]
3787    fn test_array_with_regular_items_schema() {
3788        // Test regular array with object schema items (not boolean)
3789        let param = Parameter {
3790            name: "tags".to_string(),
3791            location: ParameterIn::Query,
3792            description: Some("List of tags".to_string()),
3793            required: Some(false),
3794            deprecated: Some(false),
3795            allow_empty_value: Some(false),
3796            style: None,
3797            explode: None,
3798            allow_reserved: Some(false),
3799            schema: Some(ObjectOrReference::Object(ObjectSchema {
3800                schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3801                items: Some(Box::new(Schema::Object(Box::new(
3802                    ObjectOrReference::Object(ObjectSchema {
3803                        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3804                        min_length: Some(1),
3805                        max_length: Some(50),
3806                        ..Default::default()
3807                    }),
3808                )))),
3809                ..Default::default()
3810            })),
3811            example: None,
3812            examples: Default::default(),
3813            content: None,
3814            extensions: Default::default(),
3815        };
3816
3817        let spec = create_test_spec();
3818        let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3819            &param,
3820            ParameterIn::Query,
3821            &spec,
3822            false,
3823            false,
3824        )
3825        .unwrap();
3826
3827        // Use JSON snapshot for the schema
3828        insta::assert_json_snapshot!("test_array_with_regular_items_schema", result);
3829    }
3830
3831    #[test]
3832    fn test_request_body_object_schema() {
3833        // Test with object request body
3834        let operation = Operation {
3835            operation_id: Some("createPet".to_string()),
3836            summary: Some("Create a new pet".to_string()),
3837            description: Some("Creates a new pet in the store".to_string()),
3838            tags: vec![],
3839            external_docs: None,
3840            parameters: vec![],
3841            request_body: Some(ObjectOrReference::Object(RequestBody {
3842                description: Some("Pet object that needs to be added to the store".to_string()),
3843                content: {
3844                    let mut content = BTreeMap::new();
3845                    content.insert(
3846                        "application/json".to_string(),
3847                        MediaType {
3848                            extensions: Default::default(),
3849                            schema: Some(ObjectOrReference::Object(ObjectSchema {
3850                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3851                                ..Default::default()
3852                            })),
3853                            examples: None,
3854                            encoding: Default::default(),
3855                        },
3856                    );
3857                    content
3858                },
3859                required: Some(true),
3860            })),
3861            responses: Default::default(),
3862            callbacks: Default::default(),
3863            deprecated: Some(false),
3864            security: vec![],
3865            servers: vec![],
3866            extensions: Default::default(),
3867        };
3868
3869        let spec = create_test_spec();
3870        let metadata = ToolGenerator::generate_tool_metadata(
3871            &operation,
3872            "post".to_string(),
3873            "/pets".to_string(),
3874            &spec,
3875            false,
3876            false,
3877            false,
3878        )
3879        .unwrap();
3880
3881        // Check that request_body is in properties
3882        let properties = metadata
3883            .parameters
3884            .get("properties")
3885            .unwrap()
3886            .as_object()
3887            .unwrap();
3888        assert!(properties.contains_key("request_body"));
3889
3890        // Check that request_body is required
3891        let required = metadata
3892            .parameters
3893            .get("required")
3894            .unwrap()
3895            .as_array()
3896            .unwrap();
3897        assert!(required.contains(&json!("request_body")));
3898
3899        // Check request body schema using snapshot
3900        let request_body_schema = properties.get("request_body").unwrap();
3901        insta::assert_json_snapshot!("test_request_body_object_schema", request_body_schema);
3902
3903        // Validate against MCP Tool schema
3904        validate_tool_against_mcp_schema(&metadata);
3905    }
3906
3907    #[test]
3908    fn test_request_body_array_schema() {
3909        // Test with array request body
3910        let operation = Operation {
3911            operation_id: Some("createPets".to_string()),
3912            summary: Some("Create multiple pets".to_string()),
3913            description: None,
3914            tags: vec![],
3915            external_docs: None,
3916            parameters: vec![],
3917            request_body: Some(ObjectOrReference::Object(RequestBody {
3918                description: Some("Array of pet objects".to_string()),
3919                content: {
3920                    let mut content = BTreeMap::new();
3921                    content.insert(
3922                        "application/json".to_string(),
3923                        MediaType {
3924                            extensions: Default::default(),
3925                            schema: Some(ObjectOrReference::Object(ObjectSchema {
3926                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3927                                items: Some(Box::new(Schema::Object(Box::new(
3928                                    ObjectOrReference::Object(ObjectSchema {
3929                                        schema_type: Some(SchemaTypeSet::Single(
3930                                            SchemaType::Object,
3931                                        )),
3932                                        ..Default::default()
3933                                    }),
3934                                )))),
3935                                ..Default::default()
3936                            })),
3937                            examples: None,
3938                            encoding: Default::default(),
3939                        },
3940                    );
3941                    content
3942                },
3943                required: Some(false),
3944            })),
3945            responses: Default::default(),
3946            callbacks: Default::default(),
3947            deprecated: Some(false),
3948            security: vec![],
3949            servers: vec![],
3950            extensions: Default::default(),
3951        };
3952
3953        let spec = create_test_spec();
3954        let metadata = ToolGenerator::generate_tool_metadata(
3955            &operation,
3956            "post".to_string(),
3957            "/pets/batch".to_string(),
3958            &spec,
3959            false,
3960            false,
3961            false,
3962        )
3963        .unwrap();
3964
3965        // Check that request_body is in properties
3966        let properties = metadata
3967            .parameters
3968            .get("properties")
3969            .unwrap()
3970            .as_object()
3971            .unwrap();
3972        assert!(properties.contains_key("request_body"));
3973
3974        // Check that request_body is NOT required (required: false)
3975        let required = metadata
3976            .parameters
3977            .get("required")
3978            .unwrap()
3979            .as_array()
3980            .unwrap();
3981        assert!(!required.contains(&json!("request_body")));
3982
3983        // Check request body schema using snapshot
3984        let request_body_schema = properties.get("request_body").unwrap();
3985        insta::assert_json_snapshot!("test_request_body_array_schema", request_body_schema);
3986
3987        // Validate against MCP Tool schema
3988        validate_tool_against_mcp_schema(&metadata);
3989    }
3990
3991    #[test]
3992    fn test_request_body_string_schema() {
3993        // Test with string request body
3994        let operation = Operation {
3995            operation_id: Some("updatePetName".to_string()),
3996            summary: Some("Update pet name".to_string()),
3997            description: None,
3998            tags: vec![],
3999            external_docs: None,
4000            parameters: vec![],
4001            request_body: Some(ObjectOrReference::Object(RequestBody {
4002                description: None,
4003                content: {
4004                    let mut content = BTreeMap::new();
4005                    content.insert(
4006                        "text/plain".to_string(),
4007                        MediaType {
4008                            extensions: Default::default(),
4009                            schema: Some(ObjectOrReference::Object(ObjectSchema {
4010                                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4011                                min_length: Some(1),
4012                                max_length: Some(100),
4013                                ..Default::default()
4014                            })),
4015                            examples: None,
4016                            encoding: Default::default(),
4017                        },
4018                    );
4019                    content
4020                },
4021                required: Some(true),
4022            })),
4023            responses: Default::default(),
4024            callbacks: Default::default(),
4025            deprecated: Some(false),
4026            security: vec![],
4027            servers: vec![],
4028            extensions: Default::default(),
4029        };
4030
4031        let spec = create_test_spec();
4032        let metadata = ToolGenerator::generate_tool_metadata(
4033            &operation,
4034            "put".to_string(),
4035            "/pets/{petId}/name".to_string(),
4036            &spec,
4037            false,
4038            false,
4039            false,
4040        )
4041        .unwrap();
4042
4043        // Check request body schema
4044        let properties = metadata
4045            .parameters
4046            .get("properties")
4047            .unwrap()
4048            .as_object()
4049            .unwrap();
4050        let request_body_schema = properties.get("request_body").unwrap();
4051        insta::assert_json_snapshot!("test_request_body_string_schema", request_body_schema);
4052
4053        // Validate against MCP Tool schema
4054        validate_tool_against_mcp_schema(&metadata);
4055    }
4056
4057    #[test]
4058    fn test_request_body_ref_schema() {
4059        // Test with reference request body
4060        let operation = Operation {
4061            operation_id: Some("updatePet".to_string()),
4062            summary: Some("Update existing pet".to_string()),
4063            description: None,
4064            tags: vec![],
4065            external_docs: None,
4066            parameters: vec![],
4067            request_body: Some(ObjectOrReference::Ref {
4068                ref_path: "#/components/requestBodies/PetBody".to_string(),
4069                summary: None,
4070                description: None,
4071            }),
4072            responses: Default::default(),
4073            callbacks: Default::default(),
4074            deprecated: Some(false),
4075            security: vec![],
4076            servers: vec![],
4077            extensions: Default::default(),
4078        };
4079
4080        let spec = create_test_spec();
4081        let metadata = ToolGenerator::generate_tool_metadata(
4082            &operation,
4083            "put".to_string(),
4084            "/pets/{petId}".to_string(),
4085            &spec,
4086            false,
4087            false,
4088            false,
4089        )
4090        .unwrap();
4091
4092        // Check that request_body uses generic object schema for refs
4093        let properties = metadata
4094            .parameters
4095            .get("properties")
4096            .unwrap()
4097            .as_object()
4098            .unwrap();
4099        let request_body_schema = properties.get("request_body").unwrap();
4100        insta::assert_json_snapshot!("test_request_body_ref_schema", request_body_schema);
4101
4102        // Validate against MCP Tool schema
4103        validate_tool_against_mcp_schema(&metadata);
4104    }
4105
4106    #[test]
4107    fn test_no_request_body_for_get() {
4108        // Test that GET operations don't get request body by default
4109        let operation = Operation {
4110            operation_id: Some("listPets".to_string()),
4111            summary: Some("List all pets".to_string()),
4112            description: None,
4113            tags: vec![],
4114            external_docs: None,
4115            parameters: vec![],
4116            request_body: None,
4117            responses: Default::default(),
4118            callbacks: Default::default(),
4119            deprecated: Some(false),
4120            security: vec![],
4121            servers: vec![],
4122            extensions: Default::default(),
4123        };
4124
4125        let spec = create_test_spec();
4126        let metadata = ToolGenerator::generate_tool_metadata(
4127            &operation,
4128            "get".to_string(),
4129            "/pets".to_string(),
4130            &spec,
4131            false,
4132            false,
4133            false,
4134        )
4135        .unwrap();
4136
4137        // Check that request_body is NOT in properties
4138        let properties = metadata
4139            .parameters
4140            .get("properties")
4141            .unwrap()
4142            .as_object()
4143            .unwrap();
4144        assert!(!properties.contains_key("request_body"));
4145
4146        // Validate against MCP Tool schema
4147        validate_tool_against_mcp_schema(&metadata);
4148    }
4149
4150    #[test]
4151    fn test_request_body_simple_object_with_properties() {
4152        // Test with simple object schema with a few properties
4153        let operation = Operation {
4154            operation_id: Some("updatePetStatus".to_string()),
4155            summary: Some("Update pet status".to_string()),
4156            description: None,
4157            tags: vec![],
4158            external_docs: None,
4159            parameters: vec![],
4160            request_body: Some(ObjectOrReference::Object(RequestBody {
4161                description: Some("Pet status update".to_string()),
4162                content: {
4163                    let mut content = BTreeMap::new();
4164                    content.insert(
4165                        "application/json".to_string(),
4166                        MediaType {
4167                            extensions: Default::default(),
4168                            schema: Some(ObjectOrReference::Object(ObjectSchema {
4169                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4170                                properties: {
4171                                    let mut props = BTreeMap::new();
4172                                    props.insert(
4173                                        "status".to_string(),
4174                                        ObjectOrReference::Object(ObjectSchema {
4175                                            schema_type: Some(SchemaTypeSet::Single(
4176                                                SchemaType::String,
4177                                            )),
4178                                            ..Default::default()
4179                                        }),
4180                                    );
4181                                    props.insert(
4182                                        "reason".to_string(),
4183                                        ObjectOrReference::Object(ObjectSchema {
4184                                            schema_type: Some(SchemaTypeSet::Single(
4185                                                SchemaType::String,
4186                                            )),
4187                                            ..Default::default()
4188                                        }),
4189                                    );
4190                                    props
4191                                },
4192                                required: vec!["status".to_string()],
4193                                ..Default::default()
4194                            })),
4195                            examples: None,
4196                            encoding: Default::default(),
4197                        },
4198                    );
4199                    content
4200                },
4201                required: Some(false),
4202            })),
4203            responses: Default::default(),
4204            callbacks: Default::default(),
4205            deprecated: Some(false),
4206            security: vec![],
4207            servers: vec![],
4208            extensions: Default::default(),
4209        };
4210
4211        let spec = create_test_spec();
4212        let metadata = ToolGenerator::generate_tool_metadata(
4213            &operation,
4214            "patch".to_string(),
4215            "/pets/{petId}/status".to_string(),
4216            &spec,
4217            false,
4218            false,
4219            false,
4220        )
4221        .unwrap();
4222
4223        // Check request body schema - should have actual properties
4224        let properties = metadata
4225            .parameters
4226            .get("properties")
4227            .unwrap()
4228            .as_object()
4229            .unwrap();
4230        let request_body_schema = properties.get("request_body").unwrap();
4231        insta::assert_json_snapshot!(
4232            "test_request_body_simple_object_with_properties",
4233            request_body_schema
4234        );
4235
4236        // Should not be in top-level required since request body itself is optional
4237        let required = metadata
4238            .parameters
4239            .get("required")
4240            .unwrap()
4241            .as_array()
4242            .unwrap();
4243        assert!(!required.contains(&json!("request_body")));
4244
4245        // Validate against MCP Tool schema
4246        validate_tool_against_mcp_schema(&metadata);
4247    }
4248
4249    #[test]
4250    fn test_request_body_with_nested_properties() {
4251        // Test with complex nested object schema
4252        let operation = Operation {
4253            operation_id: Some("createUser".to_string()),
4254            summary: Some("Create a new user".to_string()),
4255            description: None,
4256            tags: vec![],
4257            external_docs: None,
4258            parameters: vec![],
4259            request_body: Some(ObjectOrReference::Object(RequestBody {
4260                description: Some("User creation data".to_string()),
4261                content: {
4262                    let mut content = BTreeMap::new();
4263                    content.insert(
4264                        "application/json".to_string(),
4265                        MediaType {
4266                            extensions: Default::default(),
4267                            schema: Some(ObjectOrReference::Object(ObjectSchema {
4268                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4269                                properties: {
4270                                    let mut props = BTreeMap::new();
4271                                    props.insert(
4272                                        "name".to_string(),
4273                                        ObjectOrReference::Object(ObjectSchema {
4274                                            schema_type: Some(SchemaTypeSet::Single(
4275                                                SchemaType::String,
4276                                            )),
4277                                            ..Default::default()
4278                                        }),
4279                                    );
4280                                    props.insert(
4281                                        "age".to_string(),
4282                                        ObjectOrReference::Object(ObjectSchema {
4283                                            schema_type: Some(SchemaTypeSet::Single(
4284                                                SchemaType::Integer,
4285                                            )),
4286                                            minimum: Some(serde_json::Number::from(0)),
4287                                            maximum: Some(serde_json::Number::from(150)),
4288                                            ..Default::default()
4289                                        }),
4290                                    );
4291                                    props
4292                                },
4293                                required: vec!["name".to_string()],
4294                                ..Default::default()
4295                            })),
4296                            examples: None,
4297                            encoding: Default::default(),
4298                        },
4299                    );
4300                    content
4301                },
4302                required: Some(true),
4303            })),
4304            responses: Default::default(),
4305            callbacks: Default::default(),
4306            deprecated: Some(false),
4307            security: vec![],
4308            servers: vec![],
4309            extensions: Default::default(),
4310        };
4311
4312        let spec = create_test_spec();
4313        let metadata = ToolGenerator::generate_tool_metadata(
4314            &operation,
4315            "post".to_string(),
4316            "/users".to_string(),
4317            &spec,
4318            false,
4319            false,
4320            false,
4321        )
4322        .unwrap();
4323
4324        // Check request body schema
4325        let properties = metadata
4326            .parameters
4327            .get("properties")
4328            .unwrap()
4329            .as_object()
4330            .unwrap();
4331        let request_body_schema = properties.get("request_body").unwrap();
4332        insta::assert_json_snapshot!(
4333            "test_request_body_with_nested_properties",
4334            request_body_schema
4335        );
4336
4337        // Validate against MCP Tool schema
4338        validate_tool_against_mcp_schema(&metadata);
4339    }
4340
4341    #[test]
4342    fn test_operation_without_responses_has_no_output_schema() {
4343        let operation = Operation {
4344            operation_id: Some("testOperation".to_string()),
4345            summary: Some("Test operation".to_string()),
4346            description: None,
4347            tags: vec![],
4348            external_docs: None,
4349            parameters: vec![],
4350            request_body: None,
4351            responses: None,
4352            callbacks: Default::default(),
4353            deprecated: Some(false),
4354            security: vec![],
4355            servers: vec![],
4356            extensions: Default::default(),
4357        };
4358
4359        let spec = create_test_spec();
4360        let metadata = ToolGenerator::generate_tool_metadata(
4361            &operation,
4362            "get".to_string(),
4363            "/test".to_string(),
4364            &spec,
4365            false,
4366            false,
4367            false,
4368        )
4369        .unwrap();
4370
4371        // When no responses are defined, output_schema should be None
4372        assert!(metadata.output_schema.is_none());
4373
4374        // Validate against MCP Tool schema
4375        validate_tool_against_mcp_schema(&metadata);
4376    }
4377
4378    #[test]
4379    fn test_extract_output_schema_with_200_response() {
4380        use oas3::spec::Response;
4381
4382        // Create a 200 response with schema
4383        let mut responses = BTreeMap::new();
4384        let mut content = BTreeMap::new();
4385        content.insert(
4386            "application/json".to_string(),
4387            MediaType {
4388                extensions: Default::default(),
4389                schema: Some(ObjectOrReference::Object(ObjectSchema {
4390                    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4391                    properties: {
4392                        let mut props = BTreeMap::new();
4393                        props.insert(
4394                            "id".to_string(),
4395                            ObjectOrReference::Object(ObjectSchema {
4396                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4397                                ..Default::default()
4398                            }),
4399                        );
4400                        props.insert(
4401                            "name".to_string(),
4402                            ObjectOrReference::Object(ObjectSchema {
4403                                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4404                                ..Default::default()
4405                            }),
4406                        );
4407                        props
4408                    },
4409                    required: vec!["id".to_string(), "name".to_string()],
4410                    ..Default::default()
4411                })),
4412                examples: None,
4413                encoding: Default::default(),
4414            },
4415        );
4416
4417        responses.insert(
4418            "200".to_string(),
4419            ObjectOrReference::Object(Response {
4420                description: Some("Successful response".to_string()),
4421                headers: Default::default(),
4422                content,
4423                links: Default::default(),
4424                extensions: Default::default(),
4425            }),
4426        );
4427
4428        let spec = create_test_spec();
4429        let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4430
4431        // Result is already a JSON Value
4432        insta::assert_json_snapshot!(result);
4433    }
4434
4435    #[test]
4436    fn test_extract_output_schema_with_201_response() {
4437        use oas3::spec::Response;
4438
4439        // Create only a 201 response (no 200)
4440        let mut responses = BTreeMap::new();
4441        let mut content = BTreeMap::new();
4442        content.insert(
4443            "application/json".to_string(),
4444            MediaType {
4445                extensions: Default::default(),
4446                schema: Some(ObjectOrReference::Object(ObjectSchema {
4447                    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4448                    properties: {
4449                        let mut props = BTreeMap::new();
4450                        props.insert(
4451                            "created".to_string(),
4452                            ObjectOrReference::Object(ObjectSchema {
4453                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Boolean)),
4454                                ..Default::default()
4455                            }),
4456                        );
4457                        props
4458                    },
4459                    ..Default::default()
4460                })),
4461                examples: None,
4462                encoding: Default::default(),
4463            },
4464        );
4465
4466        responses.insert(
4467            "201".to_string(),
4468            ObjectOrReference::Object(Response {
4469                description: Some("Created".to_string()),
4470                headers: Default::default(),
4471                content,
4472                links: Default::default(),
4473                extensions: Default::default(),
4474            }),
4475        );
4476
4477        let spec = create_test_spec();
4478        let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4479
4480        // Result is already a JSON Value
4481        insta::assert_json_snapshot!(result);
4482    }
4483
4484    #[test]
4485    fn test_extract_output_schema_with_2xx_response() {
4486        use oas3::spec::Response;
4487
4488        // Create only a 2XX response
4489        let mut responses = BTreeMap::new();
4490        let mut content = BTreeMap::new();
4491        content.insert(
4492            "application/json".to_string(),
4493            MediaType {
4494                extensions: Default::default(),
4495                schema: Some(ObjectOrReference::Object(ObjectSchema {
4496                    schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
4497                    items: Some(Box::new(Schema::Object(Box::new(
4498                        ObjectOrReference::Object(ObjectSchema {
4499                            schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4500                            ..Default::default()
4501                        }),
4502                    )))),
4503                    ..Default::default()
4504                })),
4505                examples: None,
4506                encoding: Default::default(),
4507            },
4508        );
4509
4510        responses.insert(
4511            "2XX".to_string(),
4512            ObjectOrReference::Object(Response {
4513                description: Some("Success".to_string()),
4514                headers: Default::default(),
4515                content,
4516                links: Default::default(),
4517                extensions: Default::default(),
4518            }),
4519        );
4520
4521        let spec = create_test_spec();
4522        let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4523
4524        // Result is already a JSON Value
4525        insta::assert_json_snapshot!(result);
4526    }
4527
4528    #[test]
4529    fn test_extract_output_schema_no_responses() {
4530        let spec = create_test_spec();
4531        let result = ToolGenerator::extract_output_schema(&None, &spec).unwrap();
4532
4533        // Result is already a JSON Value
4534        insta::assert_json_snapshot!(result);
4535    }
4536
4537    #[test]
4538    fn test_extract_output_schema_only_error_responses() {
4539        use oas3::spec::Response;
4540
4541        // Create only error responses
4542        let mut responses = BTreeMap::new();
4543        responses.insert(
4544            "404".to_string(),
4545            ObjectOrReference::Object(Response {
4546                description: Some("Not found".to_string()),
4547                headers: Default::default(),
4548                content: Default::default(),
4549                links: Default::default(),
4550                extensions: Default::default(),
4551            }),
4552        );
4553        responses.insert(
4554            "500".to_string(),
4555            ObjectOrReference::Object(Response {
4556                description: Some("Server error".to_string()),
4557                headers: Default::default(),
4558                content: Default::default(),
4559                links: Default::default(),
4560                extensions: Default::default(),
4561            }),
4562        );
4563
4564        let spec = create_test_spec();
4565        let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4566
4567        // Result is already a JSON Value
4568        insta::assert_json_snapshot!(result);
4569    }
4570
4571    #[test]
4572    fn test_extract_output_schema_with_ref() {
4573        use oas3::spec::Response;
4574
4575        // Create a spec with schema reference
4576        let mut spec = create_test_spec();
4577        let mut schemas = BTreeMap::new();
4578        schemas.insert(
4579            "Pet".to_string(),
4580            ObjectOrReference::Object(ObjectSchema {
4581                schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4582                properties: {
4583                    let mut props = BTreeMap::new();
4584                    props.insert(
4585                        "name".to_string(),
4586                        ObjectOrReference::Object(ObjectSchema {
4587                            schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4588                            ..Default::default()
4589                        }),
4590                    );
4591                    props
4592                },
4593                ..Default::default()
4594            }),
4595        );
4596        spec.components.as_mut().unwrap().schemas = schemas;
4597
4598        // Create response with $ref
4599        let mut responses = BTreeMap::new();
4600        let mut content = BTreeMap::new();
4601        content.insert(
4602            "application/json".to_string(),
4603            MediaType {
4604                extensions: Default::default(),
4605                schema: Some(ObjectOrReference::Ref {
4606                    ref_path: "#/components/schemas/Pet".to_string(),
4607                    summary: None,
4608                    description: None,
4609                }),
4610                examples: None,
4611                encoding: Default::default(),
4612            },
4613        );
4614
4615        responses.insert(
4616            "200".to_string(),
4617            ObjectOrReference::Object(Response {
4618                description: Some("Success".to_string()),
4619                headers: Default::default(),
4620                content,
4621                links: Default::default(),
4622                extensions: Default::default(),
4623            }),
4624        );
4625
4626        let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4627
4628        // Result is already a JSON Value
4629        insta::assert_json_snapshot!(result);
4630    }
4631
4632    #[test]
4633    fn test_generate_tool_metadata_includes_output_schema() {
4634        use oas3::spec::Response;
4635
4636        let mut operation = Operation {
4637            operation_id: Some("getPet".to_string()),
4638            summary: Some("Get a pet".to_string()),
4639            description: None,
4640            tags: vec![],
4641            external_docs: None,
4642            parameters: vec![],
4643            request_body: None,
4644            responses: Default::default(),
4645            callbacks: Default::default(),
4646            deprecated: Some(false),
4647            security: vec![],
4648            servers: vec![],
4649            extensions: Default::default(),
4650        };
4651
4652        // Add a response
4653        let mut responses = BTreeMap::new();
4654        let mut content = BTreeMap::new();
4655        content.insert(
4656            "application/json".to_string(),
4657            MediaType {
4658                extensions: Default::default(),
4659                schema: Some(ObjectOrReference::Object(ObjectSchema {
4660                    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4661                    properties: {
4662                        let mut props = BTreeMap::new();
4663                        props.insert(
4664                            "id".to_string(),
4665                            ObjectOrReference::Object(ObjectSchema {
4666                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4667                                ..Default::default()
4668                            }),
4669                        );
4670                        props
4671                    },
4672                    ..Default::default()
4673                })),
4674                examples: None,
4675                encoding: Default::default(),
4676            },
4677        );
4678
4679        responses.insert(
4680            "200".to_string(),
4681            ObjectOrReference::Object(Response {
4682                description: Some("Success".to_string()),
4683                headers: Default::default(),
4684                content,
4685                links: Default::default(),
4686                extensions: Default::default(),
4687            }),
4688        );
4689        operation.responses = Some(responses);
4690
4691        let spec = create_test_spec();
4692        let metadata = ToolGenerator::generate_tool_metadata(
4693            &operation,
4694            "get".to_string(),
4695            "/pets/{id}".to_string(),
4696            &spec,
4697            false,
4698            false,
4699            false,
4700        )
4701        .unwrap();
4702
4703        // Check that output_schema is included
4704        assert!(metadata.output_schema.is_some());
4705        let output_schema = metadata.output_schema.as_ref().unwrap();
4706
4707        // Use JSON snapshot for the output schema
4708        insta::assert_json_snapshot!(
4709            "test_generate_tool_metadata_includes_output_schema",
4710            output_schema
4711        );
4712
4713        // Validate against MCP Tool schema (this also validates output_schema if present)
4714        validate_tool_against_mcp_schema(&metadata);
4715    }
4716
4717    #[test]
4718    fn test_sanitize_property_name() {
4719        // Test spaces are replaced with underscores
4720        assert_eq!(sanitize_property_name("user name"), "user_name");
4721        assert_eq!(
4722            sanitize_property_name("first name last name"),
4723            "first_name_last_name"
4724        );
4725
4726        // Test special characters are replaced
4727        assert_eq!(sanitize_property_name("user(admin)"), "user_admin");
4728        assert_eq!(sanitize_property_name("user[admin]"), "user_admin");
4729        assert_eq!(sanitize_property_name("price($)"), "price");
4730        assert_eq!(sanitize_property_name("email@address"), "email_address");
4731        assert_eq!(sanitize_property_name("item#1"), "item_1");
4732        assert_eq!(sanitize_property_name("a/b/c"), "a_b_c");
4733
4734        // Test valid characters are preserved
4735        assert_eq!(sanitize_property_name("user_name"), "user_name");
4736        assert_eq!(sanitize_property_name("userName123"), "userName123");
4737        assert_eq!(sanitize_property_name("user.name"), "user.name");
4738        assert_eq!(sanitize_property_name("user-name"), "user-name");
4739
4740        // Test numeric starting names
4741        assert_eq!(sanitize_property_name("123name"), "param_123name");
4742        assert_eq!(sanitize_property_name("1st_place"), "param_1st_place");
4743
4744        // Test empty string
4745        assert_eq!(sanitize_property_name(""), "param_");
4746
4747        // Test length limit (64 characters)
4748        let long_name = "a".repeat(100);
4749        assert_eq!(sanitize_property_name(&long_name).len(), 64);
4750
4751        // Test all special characters become underscores
4752        // Note: After collapsing and trimming, this becomes empty and gets "param_" prefix
4753        assert_eq!(sanitize_property_name("!@#$%^&*()"), "param_");
4754    }
4755
4756    #[test]
4757    fn test_sanitize_property_name_trailing_underscores() {
4758        // Basic trailing underscore removal
4759        assert_eq!(sanitize_property_name("page[size]"), "page_size");
4760        assert_eq!(sanitize_property_name("user[id]"), "user_id");
4761        assert_eq!(sanitize_property_name("field[]"), "field");
4762
4763        // Multiple trailing underscores
4764        assert_eq!(sanitize_property_name("field___"), "field");
4765        assert_eq!(sanitize_property_name("test[[["), "test");
4766    }
4767
4768    #[test]
4769    fn test_sanitize_property_name_consecutive_underscores() {
4770        // Consecutive underscores in the middle
4771        assert_eq!(sanitize_property_name("user__name"), "user_name");
4772        assert_eq!(sanitize_property_name("first___last"), "first_last");
4773        assert_eq!(sanitize_property_name("a____b____c"), "a_b_c");
4774
4775        // Mix of special characters creating consecutive underscores
4776        assert_eq!(sanitize_property_name("user[[name]]"), "user_name");
4777        assert_eq!(sanitize_property_name("field@#$value"), "field_value");
4778    }
4779
4780    #[test]
4781    fn test_sanitize_property_name_edge_cases() {
4782        // Leading underscores (preserved)
4783        assert_eq!(sanitize_property_name("_private"), "_private");
4784        assert_eq!(sanitize_property_name("__dunder"), "_dunder");
4785
4786        // Only special characters
4787        assert_eq!(sanitize_property_name("[[["), "param_");
4788        assert_eq!(sanitize_property_name("@@@"), "param_");
4789
4790        // Empty after sanitization
4791        assert_eq!(sanitize_property_name(""), "param_");
4792
4793        // Mix of leading and trailing
4794        assert_eq!(sanitize_property_name("_field[size]"), "_field_size");
4795        assert_eq!(sanitize_property_name("__test__"), "_test");
4796    }
4797
4798    #[test]
4799    fn test_sanitize_property_name_complex_cases() {
4800        // Real-world examples
4801        assert_eq!(sanitize_property_name("page[size]"), "page_size");
4802        assert_eq!(sanitize_property_name("filter[status]"), "filter_status");
4803        assert_eq!(
4804            sanitize_property_name("sort[-created_at]"),
4805            "sort_-created_at"
4806        );
4807        assert_eq!(
4808            sanitize_property_name("include[author.posts]"),
4809            "include_author.posts"
4810        );
4811
4812        // Very long names with special characters
4813        let long_name = "very_long_field_name_with_special[characters]_that_needs_truncation_____";
4814        let expected = "very_long_field_name_with_special_characters_that_needs_truncat";
4815        assert_eq!(sanitize_property_name(long_name), expected);
4816    }
4817
4818    #[test]
4819    fn test_property_sanitization_with_annotations() {
4820        let spec = create_test_spec();
4821        let mut visited = HashSet::new();
4822
4823        // Create an object schema with properties that need sanitization
4824        let obj_schema = ObjectSchema {
4825            schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4826            properties: {
4827                let mut props = BTreeMap::new();
4828                // Property with space
4829                props.insert(
4830                    "user name".to_string(),
4831                    ObjectOrReference::Object(ObjectSchema {
4832                        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4833                        ..Default::default()
4834                    }),
4835                );
4836                // Property with special characters
4837                props.insert(
4838                    "price($)".to_string(),
4839                    ObjectOrReference::Object(ObjectSchema {
4840                        schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
4841                        ..Default::default()
4842                    }),
4843                );
4844                // Valid property name
4845                props.insert(
4846                    "validName".to_string(),
4847                    ObjectOrReference::Object(ObjectSchema {
4848                        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4849                        ..Default::default()
4850                    }),
4851                );
4852                props
4853            },
4854            ..Default::default()
4855        };
4856
4857        let result =
4858            ToolGenerator::convert_object_schema_to_json_schema(&obj_schema, &spec, &mut visited)
4859                .unwrap();
4860
4861        // Use JSON snapshot for the schema
4862        insta::assert_json_snapshot!("test_property_sanitization_with_annotations", result);
4863    }
4864
4865    #[test]
4866    fn test_parameter_sanitization_and_extraction() {
4867        let spec = create_test_spec();
4868
4869        // Create an operation with parameters that need sanitization
4870        let operation = Operation {
4871            operation_id: Some("testOp".to_string()),
4872            parameters: vec![
4873                // Path parameter with special characters
4874                ObjectOrReference::Object(Parameter {
4875                    name: "user(id)".to_string(),
4876                    location: ParameterIn::Path,
4877                    description: Some("User ID".to_string()),
4878                    required: Some(true),
4879                    deprecated: Some(false),
4880                    allow_empty_value: Some(false),
4881                    style: None,
4882                    explode: None,
4883                    allow_reserved: Some(false),
4884                    schema: Some(ObjectOrReference::Object(ObjectSchema {
4885                        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4886                        ..Default::default()
4887                    })),
4888                    example: None,
4889                    examples: Default::default(),
4890                    content: None,
4891                    extensions: Default::default(),
4892                }),
4893                // Query parameter with spaces
4894                ObjectOrReference::Object(Parameter {
4895                    name: "page size".to_string(),
4896                    location: ParameterIn::Query,
4897                    description: Some("Page size".to_string()),
4898                    required: Some(false),
4899                    deprecated: Some(false),
4900                    allow_empty_value: Some(false),
4901                    style: None,
4902                    explode: None,
4903                    allow_reserved: Some(false),
4904                    schema: Some(ObjectOrReference::Object(ObjectSchema {
4905                        schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4906                        ..Default::default()
4907                    })),
4908                    example: None,
4909                    examples: Default::default(),
4910                    content: None,
4911                    extensions: Default::default(),
4912                }),
4913                // Header parameter with special characters
4914                ObjectOrReference::Object(Parameter {
4915                    name: "auth-token!".to_string(),
4916                    location: ParameterIn::Header,
4917                    description: Some("Auth token".to_string()),
4918                    required: Some(false),
4919                    deprecated: Some(false),
4920                    allow_empty_value: Some(false),
4921                    style: None,
4922                    explode: None,
4923                    allow_reserved: Some(false),
4924                    schema: Some(ObjectOrReference::Object(ObjectSchema {
4925                        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4926                        ..Default::default()
4927                    })),
4928                    example: None,
4929                    examples: Default::default(),
4930                    content: None,
4931                    extensions: Default::default(),
4932                }),
4933            ],
4934            ..Default::default()
4935        };
4936
4937        let tool_metadata = ToolGenerator::generate_tool_metadata(
4938            &operation,
4939            "get".to_string(),
4940            "/users/{user(id)}".to_string(),
4941            &spec,
4942            false,
4943            false,
4944            false,
4945        )
4946        .unwrap();
4947
4948        // Check sanitized parameter names in schema
4949        let properties = tool_metadata
4950            .parameters
4951            .get("properties")
4952            .unwrap()
4953            .as_object()
4954            .unwrap();
4955
4956        assert!(properties.contains_key("user_id"));
4957        assert!(properties.contains_key("page_size"));
4958        assert!(properties.contains_key("header_auth-token"));
4959
4960        // Check that required array contains the sanitized name
4961        let required = tool_metadata
4962            .parameters
4963            .get("required")
4964            .unwrap()
4965            .as_array()
4966            .unwrap();
4967        assert!(required.contains(&json!("user_id")));
4968
4969        // Test parameter extraction with original names
4970        let arguments = json!({
4971            "user_id": "123",
4972            "page_size": 10,
4973            "header_auth-token": "secret"
4974        });
4975
4976        let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
4977
4978        // Path parameter should use original name
4979        assert_eq!(extracted.path.get("user(id)"), Some(&json!("123")));
4980
4981        // Query parameter should use original name
4982        assert_eq!(
4983            extracted.query.get("page size").map(|q| &q.value),
4984            Some(&json!(10))
4985        );
4986
4987        // Header parameter should use original name (without prefix)
4988        assert_eq!(extracted.headers.get("auth-token!"), Some(&json!("secret")));
4989    }
4990
4991    #[test]
4992    fn test_check_unknown_parameters() {
4993        // Test with unknown parameter that has a suggestion
4994        let mut properties = serde_json::Map::new();
4995        properties.insert("page_size".to_string(), json!({"type": "integer"}));
4996        properties.insert("user_id".to_string(), json!({"type": "string"}));
4997
4998        let mut args = serde_json::Map::new();
4999        args.insert("page_sixe".to_string(), json!(10)); // typo
5000
5001        let result = ToolGenerator::check_unknown_parameters(&args, &properties);
5002        assert!(!result.is_empty());
5003        assert_eq!(result.len(), 1);
5004
5005        match &result[0] {
5006            ValidationError::InvalidParameter {
5007                parameter,
5008                suggestions,
5009                valid_parameters,
5010            } => {
5011                assert_eq!(parameter, "page_sixe");
5012                assert_eq!(suggestions, &vec!["page_size".to_string()]);
5013                assert_eq!(
5014                    valid_parameters,
5015                    &vec!["page_size".to_string(), "user_id".to_string()]
5016                );
5017            }
5018            _ => panic!("Expected InvalidParameter variant"),
5019        }
5020    }
5021
5022    #[test]
5023    fn test_check_unknown_parameters_no_suggestions() {
5024        // Test with unknown parameter that has no suggestions
5025        let mut properties = serde_json::Map::new();
5026        properties.insert("limit".to_string(), json!({"type": "integer"}));
5027        properties.insert("offset".to_string(), json!({"type": "integer"}));
5028
5029        let mut args = serde_json::Map::new();
5030        args.insert("xyz123".to_string(), json!("value"));
5031
5032        let result = ToolGenerator::check_unknown_parameters(&args, &properties);
5033        assert!(!result.is_empty());
5034        assert_eq!(result.len(), 1);
5035
5036        match &result[0] {
5037            ValidationError::InvalidParameter {
5038                parameter,
5039                suggestions,
5040                valid_parameters,
5041            } => {
5042                assert_eq!(parameter, "xyz123");
5043                assert!(suggestions.is_empty());
5044                assert!(valid_parameters.contains(&"limit".to_string()));
5045                assert!(valid_parameters.contains(&"offset".to_string()));
5046            }
5047            _ => panic!("Expected InvalidParameter variant"),
5048        }
5049    }
5050
5051    #[test]
5052    fn test_check_unknown_parameters_multiple_suggestions() {
5053        // Test with unknown parameter that has multiple suggestions
5054        let mut properties = serde_json::Map::new();
5055        properties.insert("user_id".to_string(), json!({"type": "string"}));
5056        properties.insert("user_iid".to_string(), json!({"type": "string"}));
5057        properties.insert("user_name".to_string(), json!({"type": "string"}));
5058
5059        let mut args = serde_json::Map::new();
5060        args.insert("usr_id".to_string(), json!("123"));
5061
5062        let result = ToolGenerator::check_unknown_parameters(&args, &properties);
5063        assert!(!result.is_empty());
5064        assert_eq!(result.len(), 1);
5065
5066        match &result[0] {
5067            ValidationError::InvalidParameter {
5068                parameter,
5069                suggestions,
5070                valid_parameters,
5071            } => {
5072                assert_eq!(parameter, "usr_id");
5073                assert!(!suggestions.is_empty());
5074                assert!(suggestions.contains(&"user_id".to_string()));
5075                assert_eq!(valid_parameters.len(), 3);
5076            }
5077            _ => panic!("Expected InvalidParameter variant"),
5078        }
5079    }
5080
5081    #[test]
5082    fn test_check_unknown_parameters_valid() {
5083        // Test with all valid parameters
5084        let mut properties = serde_json::Map::new();
5085        properties.insert("name".to_string(), json!({"type": "string"}));
5086        properties.insert("email".to_string(), json!({"type": "string"}));
5087
5088        let mut args = serde_json::Map::new();
5089        args.insert("name".to_string(), json!("John"));
5090        args.insert("email".to_string(), json!("john@example.com"));
5091
5092        let result = ToolGenerator::check_unknown_parameters(&args, &properties);
5093        assert!(result.is_empty());
5094    }
5095
5096    #[test]
5097    fn test_check_unknown_parameters_empty() {
5098        // Test with no parameters defined
5099        let properties = serde_json::Map::new();
5100
5101        let mut args = serde_json::Map::new();
5102        args.insert("any_param".to_string(), json!("value"));
5103
5104        let result = ToolGenerator::check_unknown_parameters(&args, &properties);
5105        assert!(!result.is_empty());
5106        assert_eq!(result.len(), 1);
5107
5108        match &result[0] {
5109            ValidationError::InvalidParameter {
5110                parameter,
5111                suggestions,
5112                valid_parameters,
5113            } => {
5114                assert_eq!(parameter, "any_param");
5115                assert!(suggestions.is_empty());
5116                assert!(valid_parameters.is_empty());
5117            }
5118            _ => panic!("Expected InvalidParameter variant"),
5119        }
5120    }
5121
5122    #[test]
5123    fn test_check_unknown_parameters_gltf_pagination() {
5124        // Test the GLTF Live pagination scenario
5125        let mut properties = serde_json::Map::new();
5126        properties.insert(
5127            "page_number".to_string(),
5128            json!({
5129                "type": "integer",
5130                "x-original-name": "page[number]"
5131            }),
5132        );
5133        properties.insert(
5134            "page_size".to_string(),
5135            json!({
5136                "type": "integer",
5137                "x-original-name": "page[size]"
5138            }),
5139        );
5140
5141        // User passes page/per_page (common pagination params)
5142        let mut args = serde_json::Map::new();
5143        args.insert("page".to_string(), json!(1));
5144        args.insert("per_page".to_string(), json!(10));
5145
5146        let result = ToolGenerator::check_unknown_parameters(&args, &properties);
5147        assert_eq!(result.len(), 2, "Should have 2 unknown parameters");
5148
5149        // Check that both parameters are flagged as invalid
5150        let page_error = result
5151            .iter()
5152            .find(|e| {
5153                if let ValidationError::InvalidParameter { parameter, .. } = e {
5154                    parameter == "page"
5155                } else {
5156                    false
5157                }
5158            })
5159            .expect("Should have error for 'page'");
5160
5161        let per_page_error = result
5162            .iter()
5163            .find(|e| {
5164                if let ValidationError::InvalidParameter { parameter, .. } = e {
5165                    parameter == "per_page"
5166                } else {
5167                    false
5168                }
5169            })
5170            .expect("Should have error for 'per_page'");
5171
5172        // Verify suggestions are provided for 'page'
5173        match page_error {
5174            ValidationError::InvalidParameter {
5175                suggestions,
5176                valid_parameters,
5177                ..
5178            } => {
5179                assert!(
5180                    suggestions.contains(&"page_number".to_string()),
5181                    "Should suggest 'page_number' for 'page'"
5182                );
5183                assert_eq!(valid_parameters.len(), 2);
5184                assert!(valid_parameters.contains(&"page_number".to_string()));
5185                assert!(valid_parameters.contains(&"page_size".to_string()));
5186            }
5187            _ => panic!("Expected InvalidParameter"),
5188        }
5189
5190        // Verify error for 'per_page' (may not have suggestions due to low similarity)
5191        match per_page_error {
5192            ValidationError::InvalidParameter {
5193                parameter,
5194                suggestions,
5195                valid_parameters,
5196                ..
5197            } => {
5198                assert_eq!(parameter, "per_page");
5199                assert_eq!(valid_parameters.len(), 2);
5200                // per_page might not get suggestions if the similarity algorithm
5201                // doesn't find it similar enough to page_size
5202                if !suggestions.is_empty() {
5203                    assert!(suggestions.contains(&"page_size".to_string()));
5204                }
5205            }
5206            _ => panic!("Expected InvalidParameter"),
5207        }
5208    }
5209
5210    #[test]
5211    fn test_validate_parameters_with_invalid_params() {
5212        // Create a tool metadata with sanitized parameter names
5213        let tool_metadata = ToolMetadata {
5214            name: "listItems".to_string(),
5215            title: None,
5216            description: Some("List items".to_string()),
5217            parameters: json!({
5218                "type": "object",
5219                "properties": {
5220                    "page_number": {
5221                        "type": "integer",
5222                        "x-original-name": "page[number]"
5223                    },
5224                    "page_size": {
5225                        "type": "integer",
5226                        "x-original-name": "page[size]"
5227                    }
5228                },
5229                "required": []
5230            }),
5231            output_schema: None,
5232            method: "GET".to_string(),
5233            path: "/items".to_string(),
5234            security: None,
5235            parameter_mappings: std::collections::HashMap::new(),
5236        };
5237
5238        // Pass incorrect parameter names
5239        let arguments = json!({
5240            "page": 1,
5241            "per_page": 10
5242        });
5243
5244        let result = ToolGenerator::validate_parameters(&tool_metadata, &arguments);
5245        assert!(
5246            result.is_err(),
5247            "Should fail validation with unknown parameters"
5248        );
5249
5250        let error = result.unwrap_err();
5251        match error {
5252            ToolCallValidationError::InvalidParameters { violations } => {
5253                assert_eq!(violations.len(), 2, "Should have 2 validation errors");
5254
5255                // Check that both parameters are in the error
5256                let has_page_error = violations.iter().any(|v| {
5257                    if let ValidationError::InvalidParameter { parameter, .. } = v {
5258                        parameter == "page"
5259                    } else {
5260                        false
5261                    }
5262                });
5263
5264                let has_per_page_error = violations.iter().any(|v| {
5265                    if let ValidationError::InvalidParameter { parameter, .. } = v {
5266                        parameter == "per_page"
5267                    } else {
5268                        false
5269                    }
5270                });
5271
5272                assert!(has_page_error, "Should have error for 'page' parameter");
5273                assert!(
5274                    has_per_page_error,
5275                    "Should have error for 'per_page' parameter"
5276                );
5277            }
5278            _ => panic!("Expected InvalidParameters"),
5279        }
5280    }
5281
5282    #[test]
5283    fn test_cookie_parameter_sanitization() {
5284        let spec = create_test_spec();
5285
5286        let operation = Operation {
5287            operation_id: Some("testCookie".to_string()),
5288            parameters: vec![ObjectOrReference::Object(Parameter {
5289                name: "session[id]".to_string(),
5290                location: ParameterIn::Cookie,
5291                description: Some("Session ID".to_string()),
5292                required: Some(false),
5293                deprecated: Some(false),
5294                allow_empty_value: Some(false),
5295                style: None,
5296                explode: None,
5297                allow_reserved: Some(false),
5298                schema: Some(ObjectOrReference::Object(ObjectSchema {
5299                    schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5300                    ..Default::default()
5301                })),
5302                example: None,
5303                examples: Default::default(),
5304                content: None,
5305                extensions: Default::default(),
5306            })],
5307            ..Default::default()
5308        };
5309
5310        let tool_metadata = ToolGenerator::generate_tool_metadata(
5311            &operation,
5312            "get".to_string(),
5313            "/data".to_string(),
5314            &spec,
5315            false,
5316            false,
5317            false,
5318        )
5319        .unwrap();
5320
5321        let properties = tool_metadata
5322            .parameters
5323            .get("properties")
5324            .unwrap()
5325            .as_object()
5326            .unwrap();
5327
5328        // Check sanitized cookie parameter name
5329        assert!(properties.contains_key("cookie_session_id"));
5330
5331        // Test extraction
5332        let arguments = json!({
5333            "cookie_session_id": "abc123"
5334        });
5335
5336        let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
5337
5338        // Cookie should use original name
5339        assert_eq!(extracted.cookies.get("session[id]"), Some(&json!("abc123")));
5340    }
5341
5342    #[test]
5343    fn test_parameter_description_with_examples() {
5344        let spec = create_test_spec();
5345
5346        // Test parameter with single example
5347        let param_with_example = Parameter {
5348            name: "status".to_string(),
5349            location: ParameterIn::Query,
5350            description: Some("Filter by status".to_string()),
5351            required: Some(false),
5352            deprecated: Some(false),
5353            allow_empty_value: Some(false),
5354            style: None,
5355            explode: None,
5356            allow_reserved: Some(false),
5357            schema: Some(ObjectOrReference::Object(ObjectSchema {
5358                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5359                ..Default::default()
5360            })),
5361            example: Some(json!("active")),
5362            examples: Default::default(),
5363            content: None,
5364            extensions: Default::default(),
5365        };
5366
5367        let (schema, _) = ToolGenerator::convert_parameter_schema(
5368            &param_with_example,
5369            ParameterIn::Query,
5370            &spec,
5371            false,
5372            true,
5373        )
5374        .unwrap();
5375        let description = schema.get("description").unwrap().as_str().unwrap();
5376        assert_eq!(description, "Filter by status. Example: `\"active\"`");
5377
5378        // Test parameter with multiple examples
5379        let mut examples_map = std::collections::BTreeMap::new();
5380        examples_map.insert(
5381            "example1".to_string(),
5382            ObjectOrReference::Object(oas3::spec::Example {
5383                value: Some(json!("pending")),
5384                ..Default::default()
5385            }),
5386        );
5387        examples_map.insert(
5388            "example2".to_string(),
5389            ObjectOrReference::Object(oas3::spec::Example {
5390                value: Some(json!("completed")),
5391                ..Default::default()
5392            }),
5393        );
5394
5395        let param_with_examples = Parameter {
5396            name: "status".to_string(),
5397            location: ParameterIn::Query,
5398            description: Some("Filter by status".to_string()),
5399            required: Some(false),
5400            deprecated: Some(false),
5401            allow_empty_value: Some(false),
5402            style: None,
5403            explode: None,
5404            allow_reserved: Some(false),
5405            schema: Some(ObjectOrReference::Object(ObjectSchema {
5406                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5407                ..Default::default()
5408            })),
5409            example: None,
5410            examples: examples_map,
5411            content: None,
5412            extensions: Default::default(),
5413        };
5414
5415        let (schema, _) = ToolGenerator::convert_parameter_schema(
5416            &param_with_examples,
5417            ParameterIn::Query,
5418            &spec,
5419            false,
5420            true,
5421        )
5422        .unwrap();
5423        let description = schema.get("description").unwrap().as_str().unwrap();
5424        assert!(description.starts_with("Filter by status. Examples:\n"));
5425        assert!(description.contains("`\"pending\"`"));
5426        assert!(description.contains("`\"completed\"`"));
5427
5428        // Test parameter with no description but with example
5429        let param_no_desc = Parameter {
5430            name: "limit".to_string(),
5431            location: ParameterIn::Query,
5432            description: None,
5433            required: Some(false),
5434            deprecated: Some(false),
5435            allow_empty_value: Some(false),
5436            style: None,
5437            explode: None,
5438            allow_reserved: Some(false),
5439            schema: Some(ObjectOrReference::Object(ObjectSchema {
5440                schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
5441                ..Default::default()
5442            })),
5443            example: Some(json!(100)),
5444            examples: Default::default(),
5445            content: None,
5446            extensions: Default::default(),
5447        };
5448
5449        let (schema, _) = ToolGenerator::convert_parameter_schema(
5450            &param_no_desc,
5451            ParameterIn::Query,
5452            &spec,
5453            false,
5454            true,
5455        )
5456        .unwrap();
5457        let description = schema.get("description").unwrap().as_str().unwrap();
5458        assert_eq!(description, "limit parameter. Example: `100`");
5459    }
5460
5461    #[test]
5462    fn test_format_examples_for_description() {
5463        // Test single string example
5464        let examples = vec![json!("active")];
5465        let result = ToolGenerator::format_examples_for_description(&examples);
5466        assert_eq!(result, Some("Example: `\"active\"`".to_string()));
5467
5468        // Test single number example
5469        let examples = vec![json!(42)];
5470        let result = ToolGenerator::format_examples_for_description(&examples);
5471        assert_eq!(result, Some("Example: `42`".to_string()));
5472
5473        // Test single boolean example
5474        let examples = vec![json!(true)];
5475        let result = ToolGenerator::format_examples_for_description(&examples);
5476        assert_eq!(result, Some("Example: `true`".to_string()));
5477
5478        // Test multiple examples
5479        let examples = vec![json!("active"), json!("pending"), json!("completed")];
5480        let result = ToolGenerator::format_examples_for_description(&examples);
5481        assert_eq!(
5482            result,
5483            Some("Examples:\n- `\"active\"`\n- `\"pending\"`\n- `\"completed\"`".to_string())
5484        );
5485
5486        // Test array example
5487        let examples = vec![json!(["a", "b", "c"])];
5488        let result = ToolGenerator::format_examples_for_description(&examples);
5489        assert_eq!(result, Some("Example: `[\"a\",\"b\",\"c\"]`".to_string()));
5490
5491        // Test object example
5492        let examples = vec![json!({"key": "value"})];
5493        let result = ToolGenerator::format_examples_for_description(&examples);
5494        assert_eq!(result, Some("Example: `{\"key\":\"value\"}`".to_string()));
5495
5496        // Test empty examples
5497        let examples = vec![];
5498        let result = ToolGenerator::format_examples_for_description(&examples);
5499        assert_eq!(result, None);
5500
5501        // Test null example
5502        let examples = vec![json!(null)];
5503        let result = ToolGenerator::format_examples_for_description(&examples);
5504        assert_eq!(result, Some("Example: `null`".to_string()));
5505
5506        // Test mixed type examples
5507        let examples = vec![json!("text"), json!(123), json!(true)];
5508        let result = ToolGenerator::format_examples_for_description(&examples);
5509        assert_eq!(
5510            result,
5511            Some("Examples:\n- `\"text\"`\n- `123`\n- `true`".to_string())
5512        );
5513
5514        // Test long array (should be truncated)
5515        let examples = vec![json!(["a", "b", "c", "d", "e", "f"])];
5516        let result = ToolGenerator::format_examples_for_description(&examples);
5517        assert_eq!(
5518            result,
5519            Some("Example: `[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\"]`".to_string())
5520        );
5521
5522        // Test short array (should show full content)
5523        let examples = vec![json!([1, 2])];
5524        let result = ToolGenerator::format_examples_for_description(&examples);
5525        assert_eq!(result, Some("Example: `[1,2]`".to_string()));
5526
5527        // Test nested object
5528        let examples = vec![json!({"user": {"name": "John", "age": 30}})];
5529        let result = ToolGenerator::format_examples_for_description(&examples);
5530        assert_eq!(
5531            result,
5532            Some("Example: `{\"user\":{\"name\":\"John\",\"age\":30}}`".to_string())
5533        );
5534
5535        // Test more than 3 examples (should only show first 3)
5536        let examples = vec![json!("a"), json!("b"), json!("c"), json!("d"), json!("e")];
5537        let result = ToolGenerator::format_examples_for_description(&examples);
5538        assert_eq!(
5539            result,
5540            Some("Examples:\n- `\"a\"`\n- `\"b\"`\n- `\"c\"`\n- `\"d\"`\n- `\"e\"`".to_string())
5541        );
5542
5543        // Test float number
5544        let examples = vec![json!(3.5)];
5545        let result = ToolGenerator::format_examples_for_description(&examples);
5546        assert_eq!(result, Some("Example: `3.5`".to_string()));
5547
5548        // Test negative number
5549        let examples = vec![json!(-42)];
5550        let result = ToolGenerator::format_examples_for_description(&examples);
5551        assert_eq!(result, Some("Example: `-42`".to_string()));
5552
5553        // Test false boolean
5554        let examples = vec![json!(false)];
5555        let result = ToolGenerator::format_examples_for_description(&examples);
5556        assert_eq!(result, Some("Example: `false`".to_string()));
5557
5558        // Test string with special characters
5559        let examples = vec![json!("hello \"world\"")];
5560        let result = ToolGenerator::format_examples_for_description(&examples);
5561        // The format function just wraps strings in quotes, it doesn't escape them
5562        assert_eq!(result, Some(r#"Example: `"hello \"world\""`"#.to_string()));
5563
5564        // Test empty string
5565        let examples = vec![json!("")];
5566        let result = ToolGenerator::format_examples_for_description(&examples);
5567        assert_eq!(result, Some("Example: `\"\"`".to_string()));
5568
5569        // Test empty array
5570        let examples = vec![json!([])];
5571        let result = ToolGenerator::format_examples_for_description(&examples);
5572        assert_eq!(result, Some("Example: `[]`".to_string()));
5573
5574        // Test empty object
5575        let examples = vec![json!({})];
5576        let result = ToolGenerator::format_examples_for_description(&examples);
5577        assert_eq!(result, Some("Example: `{}`".to_string()));
5578    }
5579
5580    #[test]
5581    fn test_reference_metadata_functionality() {
5582        // Test ReferenceMetadata creation and methods
5583        let metadata = ReferenceMetadata::new(
5584            Some("User Reference".to_string()),
5585            Some("A reference to user data with additional context".to_string()),
5586        );
5587
5588        assert!(!metadata.is_empty());
5589        assert_eq!(metadata.summary(), Some("User Reference"));
5590        assert_eq!(
5591            metadata.best_description(),
5592            Some("A reference to user data with additional context")
5593        );
5594
5595        // Test metadata with only summary
5596        let summary_only = ReferenceMetadata::new(Some("Pet Summary".to_string()), None);
5597        assert_eq!(summary_only.best_description(), Some("Pet Summary"));
5598
5599        // Test empty metadata
5600        let empty_metadata = ReferenceMetadata::new(None, None);
5601        assert!(empty_metadata.is_empty());
5602        assert_eq!(empty_metadata.best_description(), None);
5603
5604        // Test merge_with_description
5605        let metadata = ReferenceMetadata::new(
5606            Some("Reference Summary".to_string()),
5607            Some("Reference Description".to_string()),
5608        );
5609
5610        // Test with no existing description
5611        let result = metadata.merge_with_description(None, false);
5612        assert_eq!(result, Some("Reference Description".to_string()));
5613
5614        // Test with existing description and no prepend - reference description takes precedence
5615        let result = metadata.merge_with_description(Some("Existing desc"), false);
5616        assert_eq!(result, Some("Reference Description".to_string()));
5617
5618        // Test with existing description and prepend summary - reference description still takes precedence
5619        let result = metadata.merge_with_description(Some("Existing desc"), true);
5620        assert_eq!(result, Some("Reference Description".to_string()));
5621
5622        // Test enhance_parameter_description - reference description takes precedence with proper formatting
5623        let result = metadata.enhance_parameter_description("userId", Some("User ID parameter"));
5624        assert_eq!(result, Some("userId: Reference Description".to_string()));
5625
5626        let result = metadata.enhance_parameter_description("userId", None);
5627        assert_eq!(result, Some("userId: Reference Description".to_string()));
5628
5629        // Test precedence: summary-only metadata should use summary when no description
5630        let summary_only = ReferenceMetadata::new(Some("API Token".to_string()), None);
5631
5632        let result = summary_only.merge_with_description(Some("Generic token"), false);
5633        assert_eq!(result, Some("API Token".to_string()));
5634
5635        let result = summary_only.merge_with_description(Some("Different desc"), true);
5636        assert_eq!(result, Some("API Token".to_string())); // Summary takes precedence via best_description()
5637
5638        let result = summary_only.enhance_parameter_description("token", Some("Token field"));
5639        assert_eq!(result, Some("token: API Token".to_string()));
5640
5641        // Test fallback behavior: no reference metadata should use schema description
5642        let empty_meta = ReferenceMetadata::new(None, None);
5643
5644        let result = empty_meta.merge_with_description(Some("Schema description"), false);
5645        assert_eq!(result, Some("Schema description".to_string()));
5646
5647        let result = empty_meta.enhance_parameter_description("param", Some("Schema param"));
5648        assert_eq!(result, Some("Schema param".to_string()));
5649
5650        let result = empty_meta.enhance_parameter_description("param", None);
5651        assert_eq!(result, Some("param parameter".to_string()));
5652    }
5653
5654    #[test]
5655    fn test_parameter_schema_with_reference_metadata() {
5656        let mut spec = create_test_spec();
5657
5658        // Add a Pet schema to resolve the reference
5659        spec.components.as_mut().unwrap().schemas.insert(
5660            "Pet".to_string(),
5661            ObjectOrReference::Object(ObjectSchema {
5662                description: None, // No description so reference metadata should be used as fallback
5663                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5664                ..Default::default()
5665            }),
5666        );
5667
5668        // Create a parameter with a reference that has metadata
5669        let param_with_ref = Parameter {
5670            name: "user".to_string(),
5671            location: ParameterIn::Query,
5672            description: None,
5673            required: Some(true),
5674            deprecated: Some(false),
5675            allow_empty_value: Some(false),
5676            style: None,
5677            explode: None,
5678            allow_reserved: Some(false),
5679            schema: Some(ObjectOrReference::Ref {
5680                ref_path: "#/components/schemas/Pet".to_string(),
5681                summary: Some("Pet Reference".to_string()),
5682                description: Some("A reference to pet schema with additional context".to_string()),
5683            }),
5684            example: None,
5685            examples: BTreeMap::new(),
5686            content: None,
5687            extensions: Default::default(),
5688        };
5689
5690        // Convert the parameter schema
5691        let result = ToolGenerator::convert_parameter_schema(
5692            &param_with_ref,
5693            ParameterIn::Query,
5694            &spec,
5695            false,
5696            false,
5697        );
5698
5699        assert!(result.is_ok());
5700        let (schema, _annotations) = result.unwrap();
5701
5702        // Check that the schema includes the reference description as fallback
5703        let description = schema.get("description").and_then(|v| v.as_str());
5704        assert!(description.is_some());
5705        // The description should be the reference metadata since resolved schema may not have one
5706        assert!(
5707            description.unwrap().contains("Pet Reference")
5708                || description
5709                    .unwrap()
5710                    .contains("A reference to pet schema with additional context")
5711        );
5712    }
5713
5714    #[test]
5715    fn test_request_body_with_reference_metadata() {
5716        let spec = create_test_spec();
5717
5718        // Create request body reference with metadata
5719        let request_body_ref = ObjectOrReference::Ref {
5720            ref_path: "#/components/requestBodies/PetBody".to_string(),
5721            summary: Some("Pet Request Body".to_string()),
5722            description: Some(
5723                "Request body containing pet information for API operations".to_string(),
5724            ),
5725        };
5726
5727        let result = ToolGenerator::convert_request_body_to_json_schema(&request_body_ref, &spec);
5728
5729        assert!(result.is_ok());
5730        let schema_result = result.unwrap();
5731        assert!(schema_result.is_some());
5732
5733        let (schema, _annotations, _required) = schema_result.unwrap();
5734        let description = schema.get("description").and_then(|v| v.as_str());
5735
5736        assert!(description.is_some());
5737        // Should use the reference description
5738        assert_eq!(
5739            description.unwrap(),
5740            "Request body containing pet information for API operations"
5741        );
5742    }
5743
5744    #[test]
5745    fn test_response_schema_with_reference_metadata() {
5746        let spec = create_test_spec();
5747
5748        // Create responses with a reference that has metadata
5749        let mut responses = BTreeMap::new();
5750        responses.insert(
5751            "200".to_string(),
5752            ObjectOrReference::Ref {
5753                ref_path: "#/components/responses/PetResponse".to_string(),
5754                summary: Some("Successful Pet Response".to_string()),
5755                description: Some(
5756                    "Response containing pet data on successful operation".to_string(),
5757                ),
5758            },
5759        );
5760        let responses_option = Some(responses);
5761
5762        let result = ToolGenerator::extract_output_schema(&responses_option, &spec);
5763
5764        assert!(result.is_ok());
5765        let schema = result.unwrap();
5766        assert!(schema.is_some());
5767
5768        let schema_value = schema.unwrap();
5769        let body_desc = schema_value
5770            .get("properties")
5771            .and_then(|props| props.get("body"))
5772            .and_then(|body| body.get("description"))
5773            .and_then(|desc| desc.as_str());
5774
5775        assert!(body_desc.is_some());
5776        // Should contain the reference description
5777        assert_eq!(
5778            body_desc.unwrap(),
5779            "Response containing pet data on successful operation"
5780        );
5781    }
5782
5783    #[test]
5784    fn test_self_referencing_schema_does_not_overflow() {
5785        // Create a spec with a self-referencing schema (like a tree node)
5786        // This should be handled gracefully, not cause a stack overflow
5787        let mut spec = create_test_spec();
5788
5789        // Create a "Node" schema that references itself via children
5790        let node_schema = ObjectSchema {
5791            schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5792            properties: {
5793                let mut props = BTreeMap::new();
5794                props.insert(
5795                    "name".to_string(),
5796                    ObjectOrReference::Object(ObjectSchema {
5797                        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5798                        ..Default::default()
5799                    }),
5800                );
5801                // Self-reference: children is an array of Node
5802                props.insert(
5803                    "children".to_string(),
5804                    ObjectOrReference::Object(ObjectSchema {
5805                        schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
5806                        items: Some(Box::new(Schema::Object(Box::new(ObjectOrReference::Ref {
5807                            ref_path: "#/components/schemas/Node".to_string(),
5808                            summary: None,
5809                            description: None,
5810                        })))),
5811                        ..Default::default()
5812                    }),
5813                );
5814                props
5815            },
5816            ..Default::default()
5817        };
5818
5819        // Add the schema to components
5820        if let Some(ref mut components) = spec.components {
5821            components
5822                .schemas
5823                .insert("Node".to_string(), ObjectOrReference::Object(node_schema));
5824        }
5825
5826        // Now try to convert a reference to this self-referencing schema
5827        let mut visited = HashSet::new();
5828        let result = ToolGenerator::convert_schema_to_json_schema(
5829            &Schema::Object(Box::new(ObjectOrReference::Ref {
5830                ref_path: "#/components/schemas/Node".to_string(),
5831                summary: None,
5832                description: None,
5833            })),
5834            &spec,
5835            &mut visited,
5836        );
5837
5838        // Should return an error about circular reference, not overflow the stack
5839        assert!(
5840            result.is_err(),
5841            "Expected circular reference error, got: {result:?}"
5842        );
5843        let error = result.unwrap_err();
5844        assert!(
5845            error.to_string().contains("Circular reference"),
5846            "Expected circular reference error message, got: {error}"
5847        );
5848    }
5849
5850    #[test]
5851    fn test_one_of_diamond_through_alias_is_not_circular() {
5852        // Two oneOf branches reach the same Target, one directly and one through an
5853        // alias ref (components.schemas.AliasA = $ref Target). This is a DAG diamond,
5854        // not a cycle; the previous remove-top-ref-only cleanup leaked the nested hop
5855        // into `visited` and false-tripped the circular guard, aborting tool generation
5856        // for the whole server.
5857        let mut spec = create_test_spec();
5858        if let Some(ref mut components) = spec.components {
5859            components.schemas.insert(
5860                "Target".to_string(),
5861                ObjectOrReference::Object(ObjectSchema {
5862                    schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5863                    ..Default::default()
5864                }),
5865            );
5866            components.schemas.insert(
5867                "AliasA".to_string(),
5868                ObjectOrReference::Ref {
5869                    ref_path: "#/components/schemas/Target".to_string(),
5870                    summary: None,
5871                    description: None,
5872                },
5873            );
5874        }
5875        let outer = ObjectSchema {
5876            one_of: vec![
5877                ObjectOrReference::Ref {
5878                    ref_path: "#/components/schemas/AliasA".to_string(),
5879                    summary: None,
5880                    description: None,
5881                },
5882                ObjectOrReference::Ref {
5883                    ref_path: "#/components/schemas/Target".to_string(),
5884                    summary: None,
5885                    description: None,
5886                },
5887            ],
5888            ..Default::default()
5889        };
5890        let mut visited = HashSet::new();
5891        let result =
5892            ToolGenerator::convert_object_schema_to_json_schema(&outer, &spec, &mut visited)
5893                .expect("a DAG diamond through an alias chain is not a cycle");
5894        let branches = result["oneOf"].as_array().expect("oneOf array");
5895        assert_eq!(branches.len(), 2);
5896        assert!(branches.iter().all(|b| b["type"] == json!("string")));
5897    }
5898
5899    #[test]
5900    fn test_properties_diamond_through_alias_is_not_circular() {
5901        // Same DAG-diamond leak through the `properties` conversion path: property `a`
5902        // refs AliasA (-> Target), property `b` refs Target directly.
5903        let mut spec = create_test_spec();
5904        if let Some(ref mut components) = spec.components {
5905            components.schemas.insert(
5906                "Target".to_string(),
5907                ObjectOrReference::Object(ObjectSchema {
5908                    schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5909                    ..Default::default()
5910                }),
5911            );
5912            components.schemas.insert(
5913                "AliasA".to_string(),
5914                ObjectOrReference::Ref {
5915                    ref_path: "#/components/schemas/Target".to_string(),
5916                    summary: None,
5917                    description: None,
5918                },
5919            );
5920        }
5921        let outer = ObjectSchema {
5922            schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5923            properties: BTreeMap::from([
5924                (
5925                    "a".to_string(),
5926                    ObjectOrReference::Ref {
5927                        ref_path: "#/components/schemas/AliasA".to_string(),
5928                        summary: None,
5929                        description: None,
5930                    },
5931                ),
5932                (
5933                    "b".to_string(),
5934                    ObjectOrReference::Ref {
5935                        ref_path: "#/components/schemas/Target".to_string(),
5936                        summary: None,
5937                        description: None,
5938                    },
5939                ),
5940            ]),
5941            ..Default::default()
5942        };
5943        let mut visited = HashSet::new();
5944        let result =
5945            ToolGenerator::convert_object_schema_to_json_schema(&outer, &spec, &mut visited)
5946                .expect("sibling properties sharing a target via an alias are not a cycle");
5947        assert_eq!(result["properties"]["a"]["type"], json!("string"));
5948        assert_eq!(result["properties"]["b"]["type"], json!("string"));
5949    }
5950
5951    // ==================== allOf / anyOf composition ====================
5952
5953    #[test]
5954    fn test_all_of_branches_are_merged_not_emptied() {
5955        // An internally-tagged enum (`#[serde(tag = "type")]` in Rust/utoipa terms) is
5956        // emitted as `oneOf` of `allOf: [<variant $ref>, <type discriminator>]`.
5957        // Ignoring `allOf` collapses each branch to `{}`; empty subschemas match
5958        // everything, making the strict `oneOf` unsatisfiable and the parameter
5959        // uncallable. The members must be merged instead.
5960        let mut spec = create_test_spec();
5961
5962        let application = ObjectSchema {
5963            schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5964            required: vec!["title".to_string()],
5965            properties: {
5966                let mut props = BTreeMap::new();
5967                props.insert(
5968                    "title".to_string(),
5969                    ObjectOrReference::Object(ObjectSchema {
5970                        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5971                        ..Default::default()
5972                    }),
5973                );
5974                props
5975            },
5976            ..Default::default()
5977        };
5978
5979        // { type: object, required: [type], properties: { type: { enum: ["Application"] } } }
5980        let discriminator = ObjectSchema {
5981            schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5982            required: vec!["type".to_string()],
5983            properties: {
5984                let mut props = BTreeMap::new();
5985                props.insert(
5986                    "type".to_string(),
5987                    ObjectOrReference::Object(ObjectSchema {
5988                        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5989                        enum_values: vec![json!("Application")],
5990                        ..Default::default()
5991                    }),
5992                );
5993                props
5994            },
5995            ..Default::default()
5996        };
5997
5998        let section = ObjectSchema {
5999            one_of: vec![ObjectOrReference::Object(ObjectSchema {
6000                all_of: vec![
6001                    ObjectOrReference::Ref {
6002                        ref_path: "#/components/schemas/Application".to_string(),
6003                        summary: None,
6004                        description: None,
6005                    },
6006                    ObjectOrReference::Object(discriminator),
6007                ],
6008                ..Default::default()
6009            })],
6010            ..Default::default()
6011        };
6012
6013        if let Some(ref mut components) = spec.components {
6014            components.schemas.insert(
6015                "Application".to_string(),
6016                ObjectOrReference::Object(application),
6017            );
6018        }
6019
6020        let mut visited = HashSet::new();
6021        let result =
6022            ToolGenerator::convert_object_schema_to_json_schema(&section, &spec, &mut visited)
6023                .expect("conversion should succeed");
6024
6025        let branch = &result["oneOf"][0];
6026        // The branch must not have collapsed to an empty schema.
6027        assert!(
6028            branch.as_object().is_some_and(|object| !object.is_empty()),
6029            "allOf branch collapsed to empty schema: {result}"
6030        );
6031        // It carries both the discriminator const and the variant's own fields…
6032        assert_eq!(
6033            branch["properties"]["type"]["enum"][0],
6034            json!("Application")
6035        );
6036        assert!(
6037            branch["properties"].get("title").is_some(),
6038            "merged branch is missing the variant's fields: {branch}"
6039        );
6040        // …and the union of required fields.
6041        let required = branch["required"].as_array().expect("required array");
6042        assert!(required.iter().any(|value| value == "type"));
6043        assert!(required.iter().any(|value| value == "title"));
6044    }
6045
6046    #[test]
6047    fn test_any_of_is_surfaced() {
6048        let spec = create_test_spec();
6049        let schema = ObjectSchema {
6050            any_of: vec![
6051                ObjectOrReference::Object(ObjectSchema {
6052                    schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
6053                    ..Default::default()
6054                }),
6055                ObjectOrReference::Object(ObjectSchema {
6056                    schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
6057                    ..Default::default()
6058                }),
6059            ],
6060            ..Default::default()
6061        };
6062
6063        let mut visited = HashSet::new();
6064        let result =
6065            ToolGenerator::convert_object_schema_to_json_schema(&schema, &spec, &mut visited)
6066                .expect("conversion should succeed");
6067        let any_of = result["anyOf"].as_array().expect("anyOf array");
6068        assert_eq!(any_of.len(), 2);
6069        assert_eq!(any_of[0]["type"], json!("string"));
6070        assert_eq!(any_of[1]["type"], json!("integer"));
6071    }
6072
6073    // ==================== Multipart Form Data Tests ====================
6074
6075    #[test]
6076    fn test_multipart_form_data_with_single_file() {
6077        // Test that a single binary file field in multipart/form-data is transformed
6078        // to the structured file object schema with content and filename properties
6079        let request_body = ObjectOrReference::Object(RequestBody {
6080            description: Some("File upload request".to_string()),
6081            content: {
6082                let mut content = BTreeMap::new();
6083                content.insert(
6084                    "multipart/form-data".to_string(),
6085                    MediaType {
6086                        extensions: Default::default(),
6087                        schema: Some(ObjectOrReference::Object(ObjectSchema {
6088                            schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
6089                            properties: {
6090                                let mut props = BTreeMap::new();
6091                                props.insert(
6092                                    "file".to_string(),
6093                                    ObjectOrReference::Object(ObjectSchema {
6094                                        schema_type: Some(SchemaTypeSet::Single(
6095                                            SchemaType::String,
6096                                        )),
6097                                        format: Some("binary".to_string()),
6098                                        description: Some("The file to upload".to_string()),
6099                                        ..Default::default()
6100                                    }),
6101                                );
6102                                props
6103                            },
6104                            required: vec!["file".to_string()],
6105                            ..Default::default()
6106                        })),
6107                        examples: None,
6108                        encoding: Default::default(),
6109                    },
6110                );
6111                content
6112            },
6113            required: Some(true),
6114        });
6115
6116        let spec = create_test_spec();
6117        let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
6118            .unwrap()
6119            .unwrap();
6120
6121        let (schema, annotations, is_required) = result;
6122
6123        // Verify the schema structure
6124        let schema_obj = schema.as_object().unwrap();
6125        assert_eq!(schema_obj.get("type").unwrap(), "object");
6126
6127        // Verify the file field is transformed to the file object schema
6128        let file_schema = schema_obj.get("properties").unwrap().get("file").unwrap();
6129
6130        // Check that it has the expected structure for file fields
6131        assert_eq!(file_schema.get("type").unwrap(), "object");
6132        assert!(
6133            file_schema
6134                .get("properties")
6135                .unwrap()
6136                .get("content")
6137                .is_some()
6138        );
6139        assert!(
6140            file_schema
6141                .get("properties")
6142                .unwrap()
6143                .get("filename")
6144                .is_some()
6145        );
6146        assert!(
6147            file_schema
6148                .get("required")
6149                .unwrap()
6150                .as_array()
6151                .unwrap()
6152                .contains(&json!("content"))
6153        );
6154
6155        // Check the annotations
6156        let annotations_value = serde_json::to_value(&annotations).unwrap();
6157        let annotations_obj = annotations_value.as_object().unwrap();
6158
6159        // Check x-content-type annotation
6160        assert_eq!(
6161            annotations_obj.get("x-content-type").unwrap(),
6162            "multipart/form-data"
6163        );
6164
6165        // Check x-file-fields annotation
6166        let x_file_fields = annotations_obj
6167            .get("x-file-fields")
6168            .unwrap()
6169            .as_array()
6170            .unwrap();
6171        assert_eq!(x_file_fields.len(), 1);
6172        assert!(x_file_fields.contains(&json!("file")));
6173
6174        // Check required flag
6175        assert!(is_required);
6176
6177        // Validate using snapshot
6178        insta::assert_json_snapshot!("test_multipart_form_data_with_single_file", schema);
6179    }
6180
6181    #[test]
6182    fn test_multipart_form_data_with_multiple_files() {
6183        // Test that multiple binary file fields are all transformed correctly
6184        let request_body = ObjectOrReference::Object(RequestBody {
6185            description: Some("Multiple file upload request".to_string()),
6186            content: {
6187                let mut content = BTreeMap::new();
6188                content.insert(
6189                    "multipart/form-data".to_string(),
6190                    MediaType {
6191                        extensions: Default::default(),
6192                        schema: Some(ObjectOrReference::Object(ObjectSchema {
6193                            schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
6194                            properties: {
6195                                let mut props = BTreeMap::new();
6196                                props.insert(
6197                                    "avatar".to_string(),
6198                                    ObjectOrReference::Object(ObjectSchema {
6199                                        schema_type: Some(SchemaTypeSet::Single(
6200                                            SchemaType::String,
6201                                        )),
6202                                        format: Some("binary".to_string()),
6203                                        description: Some("Profile avatar image".to_string()),
6204                                        ..Default::default()
6205                                    }),
6206                                );
6207                                props.insert(
6208                                    "document".to_string(),
6209                                    ObjectOrReference::Object(ObjectSchema {
6210                                        schema_type: Some(SchemaTypeSet::Single(
6211                                            SchemaType::String,
6212                                        )),
6213                                        format: Some("binary".to_string()),
6214                                        description: Some("Supporting document".to_string()),
6215                                        ..Default::default()
6216                                    }),
6217                                );
6218                                props.insert(
6219                                    "resume".to_string(),
6220                                    ObjectOrReference::Object(ObjectSchema {
6221                                        schema_type: Some(SchemaTypeSet::Single(
6222                                            SchemaType::String,
6223                                        )),
6224                                        format: Some("binary".to_string()),
6225                                        description: Some("Resume file".to_string()),
6226                                        ..Default::default()
6227                                    }),
6228                                );
6229                                props
6230                            },
6231                            required: vec!["avatar".to_string(), "resume".to_string()],
6232                            ..Default::default()
6233                        })),
6234                        examples: None,
6235                        encoding: Default::default(),
6236                    },
6237                );
6238                content
6239            },
6240            required: Some(true),
6241        });
6242
6243        let spec = create_test_spec();
6244        let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
6245            .unwrap()
6246            .unwrap();
6247
6248        let (schema, annotations, _is_required) = result;
6249
6250        // Verify all file fields are transformed
6251        let body_properties = schema.get("properties").unwrap();
6252        for field_name in ["avatar", "document", "resume"] {
6253            let field_schema = body_properties.get(field_name).unwrap();
6254            assert_eq!(
6255                field_schema.get("type").unwrap(),
6256                "object",
6257                "Field {field_name} should be transformed to object type"
6258            );
6259            assert!(
6260                field_schema
6261                    .get("properties")
6262                    .unwrap()
6263                    .get("content")
6264                    .is_some(),
6265                "Field {field_name} should have content property"
6266            );
6267        }
6268
6269        // Check the annotations contain all file fields
6270        let annotations_value = serde_json::to_value(&annotations).unwrap();
6271        let annotations_obj = annotations_value.as_object().unwrap();
6272
6273        let x_file_fields = annotations_obj
6274            .get("x-file-fields")
6275            .unwrap()
6276            .as_array()
6277            .unwrap();
6278        assert_eq!(x_file_fields.len(), 3);
6279        assert!(x_file_fields.contains(&json!("avatar")));
6280        assert!(x_file_fields.contains(&json!("document")));
6281        assert!(x_file_fields.contains(&json!("resume")));
6282
6283        // Validate using snapshot
6284        insta::assert_json_snapshot!("test_multipart_form_data_with_multiple_files", schema);
6285    }
6286
6287    #[test]
6288    fn test_multipart_form_data_mixed_fields() {
6289        // Test that binary fields are transformed but regular fields remain unchanged
6290        let request_body = ObjectOrReference::Object(RequestBody {
6291            description: Some("Profile creation with file upload".to_string()),
6292            content: {
6293                let mut content = BTreeMap::new();
6294                content.insert(
6295                    "multipart/form-data".to_string(),
6296                    MediaType {
6297                        extensions: Default::default(),
6298                        schema: Some(ObjectOrReference::Object(ObjectSchema {
6299                            schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
6300                            properties: {
6301                                let mut props = BTreeMap::new();
6302                                // Binary file field - should be transformed
6303                                props.insert(
6304                                    "avatar".to_string(),
6305                                    ObjectOrReference::Object(ObjectSchema {
6306                                        schema_type: Some(SchemaTypeSet::Single(
6307                                            SchemaType::String,
6308                                        )),
6309                                        format: Some("binary".to_string()),
6310                                        description: Some("Profile avatar image".to_string()),
6311                                        ..Default::default()
6312                                    }),
6313                                );
6314                                // Regular string field - should remain unchanged
6315                                props.insert(
6316                                    "name".to_string(),
6317                                    ObjectOrReference::Object(ObjectSchema {
6318                                        schema_type: Some(SchemaTypeSet::Single(
6319                                            SchemaType::String,
6320                                        )),
6321                                        description: Some("User's display name".to_string()),
6322                                        ..Default::default()
6323                                    }),
6324                                );
6325                                // Regular integer field - should remain unchanged
6326                                props.insert(
6327                                    "age".to_string(),
6328                                    ObjectOrReference::Object(ObjectSchema {
6329                                        schema_type: Some(SchemaTypeSet::Single(
6330                                            SchemaType::Integer,
6331                                        )),
6332                                        description: Some("User's age".to_string()),
6333                                        ..Default::default()
6334                                    }),
6335                                );
6336                                // Email field with format - should remain unchanged
6337                                props.insert(
6338                                    "email".to_string(),
6339                                    ObjectOrReference::Object(ObjectSchema {
6340                                        schema_type: Some(SchemaTypeSet::Single(
6341                                            SchemaType::String,
6342                                        )),
6343                                        format: Some("email".to_string()),
6344                                        description: Some("User's email address".to_string()),
6345                                        ..Default::default()
6346                                    }),
6347                                );
6348                                props
6349                            },
6350                            required: vec!["name".to_string(), "avatar".to_string()],
6351                            ..Default::default()
6352                        })),
6353                        examples: None,
6354                        encoding: Default::default(),
6355                    },
6356                );
6357                content
6358            },
6359            required: Some(true),
6360        });
6361
6362        let spec = create_test_spec();
6363        let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
6364            .unwrap()
6365            .unwrap();
6366
6367        let (schema, annotations, _is_required) = result;
6368        let body_properties = schema.get("properties").unwrap();
6369
6370        // Verify avatar (binary) is transformed to file object schema
6371        let avatar_schema = body_properties.get("avatar").unwrap();
6372        assert_eq!(avatar_schema.get("type").unwrap(), "object");
6373        assert!(
6374            avatar_schema
6375                .get("properties")
6376                .unwrap()
6377                .get("content")
6378                .is_some()
6379        );
6380        assert!(
6381            avatar_schema
6382                .get("properties")
6383                .unwrap()
6384                .get("filename")
6385                .is_some()
6386        );
6387
6388        // Verify name (string) remains a simple string type
6389        let name_schema = body_properties.get("name").unwrap();
6390        assert_eq!(name_schema.get("type").unwrap(), "string");
6391        assert!(name_schema.get("properties").is_none()); // No nested properties
6392
6393        // Verify age (integer) remains an integer type
6394        let age_schema = body_properties.get("age").unwrap();
6395        assert_eq!(age_schema.get("type").unwrap(), "integer");
6396
6397        // Verify email (string with format: email) remains a string type
6398        let email_schema = body_properties.get("email").unwrap();
6399        assert_eq!(email_schema.get("type").unwrap(), "string");
6400        assert_eq!(email_schema.get("format").unwrap(), "email");
6401
6402        // Check annotations only contains avatar as file field
6403        let annotations_value = serde_json::to_value(&annotations).unwrap();
6404        let annotations_obj = annotations_value.as_object().unwrap();
6405
6406        let x_file_fields = annotations_obj
6407            .get("x-file-fields")
6408            .unwrap()
6409            .as_array()
6410            .unwrap();
6411        assert_eq!(x_file_fields.len(), 1);
6412        assert!(x_file_fields.contains(&json!("avatar")));
6413
6414        // Validate using snapshot
6415        insta::assert_json_snapshot!("test_multipart_form_data_mixed_fields", schema);
6416    }
6417
6418    #[test]
6419    fn test_multipart_format_byte_detection() {
6420        // Test that format: byte is also detected as a file field
6421        let request_body = ObjectOrReference::Object(RequestBody {
6422            description: Some("Base64 encoded file upload".to_string()),
6423            content: {
6424                let mut content = BTreeMap::new();
6425                content.insert(
6426                    "multipart/form-data".to_string(),
6427                    MediaType {
6428                        extensions: Default::default(),
6429                        schema: Some(ObjectOrReference::Object(ObjectSchema {
6430                            schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
6431                            properties: {
6432                                let mut props = BTreeMap::new();
6433                                // format: byte should be treated as a file field
6434                                props.insert(
6435                                    "data".to_string(),
6436                                    ObjectOrReference::Object(ObjectSchema {
6437                                        schema_type: Some(SchemaTypeSet::Single(
6438                                            SchemaType::String,
6439                                        )),
6440                                        format: Some("byte".to_string()),
6441                                        description: Some(
6442                                            "Base64 encoded file content".to_string(),
6443                                        ),
6444                                        ..Default::default()
6445                                    }),
6446                                );
6447                                // format: binary for comparison
6448                                props.insert(
6449                                    "attachment".to_string(),
6450                                    ObjectOrReference::Object(ObjectSchema {
6451                                        schema_type: Some(SchemaTypeSet::Single(
6452                                            SchemaType::String,
6453                                        )),
6454                                        format: Some("binary".to_string()),
6455                                        description: Some("Binary file attachment".to_string()),
6456                                        ..Default::default()
6457                                    }),
6458                                );
6459                                props
6460                            },
6461                            required: vec!["data".to_string()],
6462                            ..Default::default()
6463                        })),
6464                        examples: None,
6465                        encoding: Default::default(),
6466                    },
6467                );
6468                content
6469            },
6470            required: Some(true),
6471        });
6472
6473        let spec = create_test_spec();
6474        let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
6475            .unwrap()
6476            .unwrap();
6477
6478        let (schema, annotations, _is_required) = result;
6479        let body_properties = schema.get("properties").unwrap();
6480
6481        // Verify both byte and binary format fields are transformed
6482        let data_schema = body_properties.get("data").unwrap();
6483        assert_eq!(data_schema.get("type").unwrap(), "object");
6484        assert!(
6485            data_schema
6486                .get("properties")
6487                .unwrap()
6488                .get("content")
6489                .is_some()
6490        );
6491
6492        let attachment_schema = body_properties.get("attachment").unwrap();
6493        assert_eq!(attachment_schema.get("type").unwrap(), "object");
6494        assert!(
6495            attachment_schema
6496                .get("properties")
6497                .unwrap()
6498                .get("content")
6499                .is_some()
6500        );
6501
6502        // Check annotations contain both fields
6503        let annotations_value = serde_json::to_value(&annotations).unwrap();
6504        let annotations_obj = annotations_value.as_object().unwrap();
6505
6506        let x_file_fields = annotations_obj
6507            .get("x-file-fields")
6508            .unwrap()
6509            .as_array()
6510            .unwrap();
6511        assert_eq!(x_file_fields.len(), 2);
6512        assert!(x_file_fields.contains(&json!("data")));
6513        assert!(x_file_fields.contains(&json!("attachment")));
6514
6515        // Validate using snapshot
6516        insta::assert_json_snapshot!("test_multipart_format_byte_detection", schema);
6517    }
6518
6519    #[test]
6520    fn test_multipart_non_file_fields_unchanged() {
6521        // Test that non-file fields under multipart/form-data stay as their original types
6522        let request_body = ObjectOrReference::Object(RequestBody {
6523            description: Some("Form submission".to_string()),
6524            content: {
6525                let mut content = BTreeMap::new();
6526                content.insert(
6527                    "multipart/form-data".to_string(),
6528                    MediaType {
6529                        extensions: Default::default(),
6530                        schema: Some(ObjectOrReference::Object(ObjectSchema {
6531                            schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
6532                            properties: {
6533                                let mut props = BTreeMap::new();
6534                                // Various non-file field types
6535                                props.insert(
6536                                    "title".to_string(),
6537                                    ObjectOrReference::Object(ObjectSchema {
6538                                        schema_type: Some(SchemaTypeSet::Single(
6539                                            SchemaType::String,
6540                                        )),
6541                                        description: Some("Form title".to_string()),
6542                                        ..Default::default()
6543                                    }),
6544                                );
6545                                props.insert(
6546                                    "count".to_string(),
6547                                    ObjectOrReference::Object(ObjectSchema {
6548                                        schema_type: Some(SchemaTypeSet::Single(
6549                                            SchemaType::Integer,
6550                                        )),
6551                                        description: Some("Item count".to_string()),
6552                                        ..Default::default()
6553                                    }),
6554                                );
6555                                props.insert(
6556                                    "enabled".to_string(),
6557                                    ObjectOrReference::Object(ObjectSchema {
6558                                        schema_type: Some(SchemaTypeSet::Single(
6559                                            SchemaType::Boolean,
6560                                        )),
6561                                        description: Some("Enable flag".to_string()),
6562                                        ..Default::default()
6563                                    }),
6564                                );
6565                                props.insert(
6566                                    "price".to_string(),
6567                                    ObjectOrReference::Object(ObjectSchema {
6568                                        schema_type: Some(SchemaTypeSet::Single(
6569                                            SchemaType::Number,
6570                                        )),
6571                                        description: Some("Price value".to_string()),
6572                                        ..Default::default()
6573                                    }),
6574                                );
6575                                props.insert(
6576                                    "uuid".to_string(),
6577                                    ObjectOrReference::Object(ObjectSchema {
6578                                        schema_type: Some(SchemaTypeSet::Single(
6579                                            SchemaType::String,
6580                                        )),
6581                                        format: Some("uuid".to_string()),
6582                                        description: Some("UUID field".to_string()),
6583                                        ..Default::default()
6584                                    }),
6585                                );
6586                                props.insert(
6587                                    "date".to_string(),
6588                                    ObjectOrReference::Object(ObjectSchema {
6589                                        schema_type: Some(SchemaTypeSet::Single(
6590                                            SchemaType::String,
6591                                        )),
6592                                        format: Some("date".to_string()),
6593                                        description: Some("Date field".to_string()),
6594                                        ..Default::default()
6595                                    }),
6596                                );
6597                                props
6598                            },
6599                            required: vec!["title".to_string()],
6600                            ..Default::default()
6601                        })),
6602                        examples: None,
6603                        encoding: Default::default(),
6604                    },
6605                );
6606                content
6607            },
6608            required: Some(true),
6609        });
6610
6611        let spec = create_test_spec();
6612        let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
6613            .unwrap()
6614            .unwrap();
6615
6616        let (schema, annotations, _is_required) = result;
6617        let body_properties = schema.get("properties").unwrap();
6618
6619        // Verify string field stays as string
6620        let title_schema = body_properties.get("title").unwrap();
6621        assert_eq!(title_schema.get("type").unwrap(), "string");
6622        assert!(title_schema.get("properties").is_none());
6623
6624        // Verify integer field stays as integer
6625        let count_schema = body_properties.get("count").unwrap();
6626        assert_eq!(count_schema.get("type").unwrap(), "integer");
6627
6628        // Verify boolean field stays as boolean
6629        let enabled_schema = body_properties.get("enabled").unwrap();
6630        assert_eq!(enabled_schema.get("type").unwrap(), "boolean");
6631
6632        // Verify number field stays as number
6633        let price_schema = body_properties.get("price").unwrap();
6634        assert_eq!(price_schema.get("type").unwrap(), "number");
6635
6636        // Verify string with uuid format stays as string with format
6637        let uuid_schema = body_properties.get("uuid").unwrap();
6638        assert_eq!(uuid_schema.get("type").unwrap(), "string");
6639        assert_eq!(uuid_schema.get("format").unwrap(), "uuid");
6640
6641        // Verify string with date format stays as string with format
6642        let date_schema = body_properties.get("date").unwrap();
6643        assert_eq!(date_schema.get("type").unwrap(), "string");
6644        assert_eq!(date_schema.get("format").unwrap(), "date");
6645
6646        // Check that annotations do NOT contain x-file-fields (no file fields present)
6647        let annotations_value = serde_json::to_value(&annotations).unwrap();
6648        let annotations_obj = annotations_value.as_object().unwrap();
6649
6650        assert!(
6651            annotations_obj.get("x-file-fields").is_none(),
6652            "x-file-fields should not be present when there are no file fields"
6653        );
6654
6655        // Verify multipart/form-data content type is still set
6656        assert_eq!(
6657            annotations_obj.get("x-content-type").unwrap(),
6658            "multipart/form-data"
6659        );
6660
6661        // Validate using snapshot
6662        insta::assert_json_snapshot!("test_multipart_non_file_fields_unchanged", schema);
6663    }
6664}