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                    let resolved = Self::resolve_reference(ref_path, spec, visited)?;
989                    let result =
990                        Self::convert_object_schema_to_json_schema(&resolved, spec, visited);
991                    // Remove after conversion completes to allow the same schema to be
992                    // referenced again elsewhere (non-circular reuse is valid).
993                    // The ref was in `visited` during conversion to detect self-references.
994                    visited.remove(ref_path);
995                    result
996                }
997            },
998            Schema::Boolean(bool_schema) => {
999                // Boolean schemas in OpenAPI: true allows any value, false allows no value
1000                if bool_schema.0 {
1001                    Ok(json!({})) // Empty schema allows anything
1002                } else {
1003                    Ok(json!({"not": {}})) // Schema that matches nothing
1004                }
1005            }
1006        }
1007    }
1008
1009    /// Convert ObjectSchema to JSON Schema format
1010    ///
1011    /// This is the core converter that handles all schema types and properties.
1012    /// It processes object properties, arrays, primitives, and all OpenAPI schema attributes.
1013    ///
1014    /// # Arguments
1015    /// * `obj_schema` - The OpenAPI ObjectSchema to convert
1016    /// * `spec` - The full OpenAPI specification for resolving references
1017    /// * `visited` - Set of visited references to prevent infinite recursion
1018    fn convert_object_schema_to_json_schema(
1019        obj_schema: &ObjectSchema,
1020        spec: &Spec,
1021        visited: &mut HashSet<String>,
1022    ) -> Result<Value, Error> {
1023        let mut schema_obj = serde_json::Map::new();
1024
1025        // Add type if specified
1026        if let Some(schema_type) = &obj_schema.schema_type {
1027            match schema_type {
1028                SchemaTypeSet::Single(single_type) => {
1029                    schema_obj.insert(
1030                        "type".to_string(),
1031                        json!(Self::schema_type_to_string(single_type)),
1032                    );
1033                }
1034                SchemaTypeSet::Multiple(type_set) => {
1035                    let types: Vec<String> =
1036                        type_set.iter().map(Self::schema_type_to_string).collect();
1037                    schema_obj.insert("type".to_string(), json!(types));
1038                }
1039            }
1040        }
1041
1042        // Add description if present
1043        if let Some(desc) = &obj_schema.description {
1044            schema_obj.insert("description".to_string(), json!(desc));
1045        }
1046
1047        // Handle oneOf schemas - this takes precedence over other schema properties
1048        if !obj_schema.one_of.is_empty() {
1049            let mut one_of_schemas = Vec::new();
1050            for schema_ref in &obj_schema.one_of {
1051                let schema_json = match schema_ref {
1052                    ObjectOrReference::Object(schema) => {
1053                        Self::convert_object_schema_to_json_schema(schema, spec, visited)?
1054                    }
1055                    ObjectOrReference::Ref { ref_path, .. } => {
1056                        let resolved = Self::resolve_reference(ref_path, spec, visited)?;
1057                        let result =
1058                            Self::convert_object_schema_to_json_schema(&resolved, spec, visited)?;
1059                        // Remove after conversion to allow schema reuse (see convert_schema_to_json_schema)
1060                        visited.remove(ref_path);
1061                        result
1062                    }
1063                };
1064                one_of_schemas.push(schema_json);
1065            }
1066            schema_obj.insert("oneOf".to_string(), json!(one_of_schemas));
1067            // When oneOf is present, we typically don't include other properties
1068            // that would conflict with the oneOf semantics
1069            return Ok(Value::Object(schema_obj));
1070        }
1071
1072        // Handle object properties
1073        if !obj_schema.properties.is_empty() {
1074            let properties = &obj_schema.properties;
1075            let mut props_map = serde_json::Map::new();
1076            for (prop_name, prop_schema_or_ref) in properties {
1077                let prop_schema = match prop_schema_or_ref {
1078                    ObjectOrReference::Object(schema) => {
1079                        // Convert ObjectSchema to Schema for processing
1080                        Self::convert_schema_to_json_schema(
1081                            &Schema::Object(Box::new(ObjectOrReference::Object(schema.clone()))),
1082                            spec,
1083                            visited,
1084                        )?
1085                    }
1086                    ObjectOrReference::Ref { ref_path, .. } => {
1087                        let resolved = Self::resolve_reference(ref_path, spec, visited)?;
1088                        let result =
1089                            Self::convert_object_schema_to_json_schema(&resolved, spec, visited)?;
1090                        // Remove after conversion to allow schema reuse (see convert_schema_to_json_schema)
1091                        visited.remove(ref_path);
1092                        result
1093                    }
1094                };
1095
1096                // Sanitize property name - no longer add annotations
1097                let sanitized_name = sanitize_property_name(prop_name);
1098                props_map.insert(sanitized_name, prop_schema);
1099            }
1100            schema_obj.insert("properties".to_string(), Value::Object(props_map));
1101        }
1102
1103        // Add required fields
1104        if !obj_schema.required.is_empty() {
1105            schema_obj.insert("required".to_string(), json!(&obj_schema.required));
1106        }
1107
1108        // Handle additionalProperties for object schemas
1109        if let Some(schema_type) = &obj_schema.schema_type
1110            && matches!(schema_type, SchemaTypeSet::Single(SchemaType::Object))
1111        {
1112            // Handle additional_properties based on the OpenAPI schema
1113            match &obj_schema.additional_properties {
1114                None => {
1115                    // In OpenAPI 3.0, the default for additionalProperties is true
1116                    schema_obj.insert("additionalProperties".to_string(), json!(true));
1117                }
1118                Some(Schema::Boolean(BooleanSchema(value))) => {
1119                    // Explicit boolean value
1120                    schema_obj.insert("additionalProperties".to_string(), json!(value));
1121                }
1122                Some(Schema::Object(schema_ref)) => {
1123                    // Additional properties must match this schema
1124                    let additional_props_schema = Self::convert_schema_to_json_schema(
1125                        &Schema::Object(schema_ref.clone()),
1126                        spec,
1127                        visited,
1128                    )?;
1129                    schema_obj.insert("additionalProperties".to_string(), additional_props_schema);
1130                }
1131            }
1132        }
1133
1134        // Handle array-specific properties
1135        if let Some(schema_type) = &obj_schema.schema_type {
1136            if matches!(schema_type, SchemaTypeSet::Single(SchemaType::Array)) {
1137                // Handle prefix_items (OpenAPI 3.1 tuple-like arrays)
1138                if !obj_schema.prefix_items.is_empty() {
1139                    // Convert prefix_items to draft-07 compatible format
1140                    Self::convert_prefix_items_to_draft07(
1141                        &obj_schema.prefix_items,
1142                        &obj_schema.items,
1143                        &mut schema_obj,
1144                        spec,
1145                    )?;
1146                } else if let Some(items_schema) = &obj_schema.items {
1147                    // Handle regular items
1148                    let items_json =
1149                        Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
1150                    schema_obj.insert("items".to_string(), items_json);
1151                }
1152
1153                // Add array constraints
1154                if let Some(min_items) = obj_schema.min_items {
1155                    schema_obj.insert("minItems".to_string(), json!(min_items));
1156                }
1157                if let Some(max_items) = obj_schema.max_items {
1158                    schema_obj.insert("maxItems".to_string(), json!(max_items));
1159                }
1160            } else if let Some(items_schema) = &obj_schema.items {
1161                // Non-array types shouldn't have items, but handle it anyway
1162                let items_json = Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
1163                schema_obj.insert("items".to_string(), items_json);
1164            }
1165        }
1166
1167        // Handle other common properties
1168        if let Some(format) = &obj_schema.format {
1169            schema_obj.insert("format".to_string(), json!(format));
1170        }
1171
1172        if let Some(example) = &obj_schema.example {
1173            schema_obj.insert("example".to_string(), example.clone());
1174        }
1175
1176        // OpenAPI 3.1 plural `examples`, read alongside the deprecated singular `example`
1177        // so neither form is dropped from the generated JSON Schema.
1178        if !obj_schema.examples.is_empty() {
1179            schema_obj.insert("examples".to_string(), json!(&obj_schema.examples));
1180        }
1181
1182        if let Some(default) = &obj_schema.default {
1183            schema_obj.insert("default".to_string(), default.clone());
1184        }
1185
1186        if !obj_schema.enum_values.is_empty() {
1187            schema_obj.insert("enum".to_string(), json!(&obj_schema.enum_values));
1188        }
1189
1190        if let Some(min) = &obj_schema.minimum {
1191            schema_obj.insert("minimum".to_string(), json!(min));
1192        }
1193
1194        if let Some(max) = &obj_schema.maximum {
1195            schema_obj.insert("maximum".to_string(), json!(max));
1196        }
1197
1198        if let Some(min_length) = &obj_schema.min_length {
1199            schema_obj.insert("minLength".to_string(), json!(min_length));
1200        }
1201
1202        if let Some(max_length) = &obj_schema.max_length {
1203            schema_obj.insert("maxLength".to_string(), json!(max_length));
1204        }
1205
1206        if let Some(pattern) = &obj_schema.pattern {
1207            schema_obj.insert("pattern".to_string(), json!(pattern));
1208        }
1209
1210        Ok(Value::Object(schema_obj))
1211    }
1212
1213    /// Convert SchemaType to string representation
1214    fn schema_type_to_string(schema_type: &SchemaType) -> String {
1215        match schema_type {
1216            SchemaType::Boolean => "boolean",
1217            SchemaType::Integer => "integer",
1218            SchemaType::Number => "number",
1219            SchemaType::String => "string",
1220            SchemaType::Array => "array",
1221            SchemaType::Object => "object",
1222            SchemaType::Null => "null",
1223        }
1224        .to_string()
1225    }
1226
1227    /// Resolve a $ref reference to get the actual schema
1228    ///
1229    /// # Arguments
1230    /// * `ref_path` - The reference path (e.g., "#/components/schemas/Pet")
1231    /// * `spec` - The OpenAPI specification
1232    /// * `visited` - Set of already visited references to detect circular references
1233    ///
1234    /// # Returns
1235    /// The resolved ObjectSchema or an error if the reference is invalid or circular
1236    fn resolve_reference(
1237        ref_path: &str,
1238        spec: &Spec,
1239        visited: &mut HashSet<String>,
1240    ) -> Result<ObjectSchema, Error> {
1241        // Check for circular reference
1242        if visited.contains(ref_path) {
1243            return Err(Error::ToolGeneration(format!(
1244                "Circular reference detected: {ref_path}"
1245            )));
1246        }
1247
1248        // Add to visited set
1249        visited.insert(ref_path.to_string());
1250
1251        // Parse the reference path
1252        // Currently only supporting local references like "#/components/schemas/Pet"
1253        if !ref_path.starts_with("#/components/schemas/") {
1254            return Err(Error::ToolGeneration(format!(
1255                "Unsupported reference format: {ref_path}. Only #/components/schemas/ references are supported"
1256            )));
1257        }
1258
1259        let schema_name = ref_path.strip_prefix("#/components/schemas/").unwrap();
1260
1261        // Get the schema from components
1262        let components = spec.components.as_ref().ok_or_else(|| {
1263            Error::ToolGeneration(format!(
1264                "Reference {ref_path} points to components, but spec has no components section"
1265            ))
1266        })?;
1267
1268        let schema_ref = components.schemas.get(schema_name).ok_or_else(|| {
1269            Error::ToolGeneration(format!(
1270                "Schema '{schema_name}' not found in components/schemas"
1271            ))
1272        })?;
1273
1274        // Resolve the schema reference
1275        let resolved_schema = match schema_ref {
1276            ObjectOrReference::Object(obj_schema) => obj_schema.clone(),
1277            ObjectOrReference::Ref {
1278                ref_path: nested_ref,
1279                ..
1280            } => {
1281                // Recursively resolve nested references
1282                Self::resolve_reference(nested_ref, spec, visited)?
1283            }
1284        };
1285
1286        // NOTE: We intentionally do NOT remove from visited here.
1287        // The ref must stay in visited during the entire conversion process
1288        // to detect cycles when the converted schema contains self-references.
1289        // The caller is responsible for removing after conversion is complete.
1290
1291        Ok(resolved_schema)
1292    }
1293
1294    /// Resolve reference with metadata extraction
1295    ///
1296    /// Extracts summary and description from the reference before resolving,
1297    /// returning both the resolved schema and the preserved metadata.
1298    fn resolve_reference_with_metadata(
1299        ref_path: &str,
1300        summary: Option<String>,
1301        description: Option<String>,
1302        spec: &Spec,
1303        visited: &mut HashSet<String>,
1304    ) -> Result<(ObjectSchema, ReferenceMetadata), Error> {
1305        let resolved_schema = Self::resolve_reference(ref_path, spec, visited)?;
1306        let metadata = ReferenceMetadata::new(summary, description);
1307        Ok((resolved_schema, metadata))
1308    }
1309
1310    /// Generate JSON Schema for tool parameters
1311    fn generate_parameter_schema(
1312        parameters: &[ObjectOrReference<Parameter>],
1313        _method: &str,
1314        request_body: &Option<ObjectOrReference<RequestBody>>,
1315        spec: &Spec,
1316        skip_parameter_descriptions: bool,
1317        parameter_examples_in_description: bool,
1318    ) -> Result<
1319        (
1320            Value,
1321            std::collections::HashMap<String, crate::tool::ParameterMapping>,
1322        ),
1323        Error,
1324    > {
1325        let mut properties = serde_json::Map::new();
1326        let mut required = Vec::new();
1327        let mut parameter_mappings = std::collections::HashMap::new();
1328
1329        // Group parameters by location
1330        let mut path_params = Vec::new();
1331        let mut query_params = Vec::new();
1332        let mut header_params = Vec::new();
1333        let mut cookie_params = Vec::new();
1334
1335        for param_ref in parameters {
1336            let param = match param_ref {
1337                ObjectOrReference::Object(param) => param,
1338                ObjectOrReference::Ref { ref_path, .. } => {
1339                    // Try to resolve parameter reference
1340                    // Note: Parameter references are rare and not supported yet in this implementation
1341                    // For now, we'll continue to skip them but log a warning
1342                    warn!(
1343                        reference_path = %ref_path,
1344                        "Parameter reference not resolved"
1345                    );
1346                    continue;
1347                }
1348            };
1349
1350            match &param.location {
1351                ParameterIn::Query => query_params.push(param),
1352                ParameterIn::Header => header_params.push(param),
1353                ParameterIn::Path => path_params.push(param),
1354                ParameterIn::Cookie => cookie_params.push(param),
1355            }
1356        }
1357
1358        // Process path parameters (always required)
1359        for param in path_params {
1360            let (param_schema, mut annotations) = Self::convert_parameter_schema(
1361                param,
1362                ParameterIn::Path,
1363                spec,
1364                skip_parameter_descriptions,
1365                parameter_examples_in_description,
1366            )?;
1367
1368            // Sanitize parameter name and add original name annotation if needed
1369            let sanitized_name = sanitize_property_name(&param.name);
1370            if sanitized_name != param.name {
1371                annotations = annotations.with_original_name(param.name.clone());
1372            }
1373
1374            // Extract explode setting from annotations
1375            let explode = annotations
1376                .annotations
1377                .iter()
1378                .find_map(|a| {
1379                    if let Annotation::Explode(e) = a {
1380                        Some(*e)
1381                    } else {
1382                        None
1383                    }
1384                })
1385                .unwrap_or(true);
1386
1387            // Store parameter mapping
1388            parameter_mappings.insert(
1389                sanitized_name.clone(),
1390                crate::tool::ParameterMapping {
1391                    sanitized_name: sanitized_name.clone(),
1392                    original_name: param.name.clone(),
1393                    location: "path".to_string(),
1394                    explode,
1395                },
1396            );
1397
1398            // No longer apply annotations to schema - use parameter_mappings instead
1399            properties.insert(sanitized_name.clone(), param_schema);
1400            required.push(sanitized_name);
1401        }
1402
1403        // Process query parameters
1404        for param in &query_params {
1405            let (param_schema, mut annotations) = Self::convert_parameter_schema(
1406                param,
1407                ParameterIn::Query,
1408                spec,
1409                skip_parameter_descriptions,
1410                parameter_examples_in_description,
1411            )?;
1412
1413            // Sanitize parameter name and add original name annotation if needed
1414            let sanitized_name = sanitize_property_name(&param.name);
1415            if sanitized_name != param.name {
1416                annotations = annotations.with_original_name(param.name.clone());
1417            }
1418
1419            // Extract explode setting from annotations
1420            let explode = annotations
1421                .annotations
1422                .iter()
1423                .find_map(|a| {
1424                    if let Annotation::Explode(e) = a {
1425                        Some(*e)
1426                    } else {
1427                        None
1428                    }
1429                })
1430                .unwrap_or(true);
1431
1432            // Store parameter mapping
1433            parameter_mappings.insert(
1434                sanitized_name.clone(),
1435                crate::tool::ParameterMapping {
1436                    sanitized_name: sanitized_name.clone(),
1437                    original_name: param.name.clone(),
1438                    location: "query".to_string(),
1439                    explode,
1440                },
1441            );
1442
1443            // No longer apply annotations to schema - use parameter_mappings instead
1444            properties.insert(sanitized_name.clone(), param_schema);
1445            if param.required.unwrap_or(false) {
1446                required.push(sanitized_name);
1447            }
1448        }
1449
1450        // Process header parameters (optional by default unless explicitly required)
1451        for param in &header_params {
1452            let (param_schema, mut annotations) = Self::convert_parameter_schema(
1453                param,
1454                ParameterIn::Header,
1455                spec,
1456                skip_parameter_descriptions,
1457                parameter_examples_in_description,
1458            )?;
1459
1460            // Sanitize parameter name after prefixing and add original name annotation if needed
1461            let prefixed_name = format!("header_{}", param.name);
1462            let sanitized_name = sanitize_property_name(&prefixed_name);
1463            if sanitized_name != prefixed_name {
1464                annotations = annotations.with_original_name(param.name.clone());
1465            }
1466
1467            // Extract explode setting from annotations
1468            let explode = annotations
1469                .annotations
1470                .iter()
1471                .find_map(|a| {
1472                    if let Annotation::Explode(e) = a {
1473                        Some(*e)
1474                    } else {
1475                        None
1476                    }
1477                })
1478                .unwrap_or(true);
1479
1480            // Store parameter mapping
1481            parameter_mappings.insert(
1482                sanitized_name.clone(),
1483                crate::tool::ParameterMapping {
1484                    sanitized_name: sanitized_name.clone(),
1485                    original_name: param.name.clone(),
1486                    location: "header".to_string(),
1487                    explode,
1488                },
1489            );
1490
1491            // No longer apply annotations to schema - use parameter_mappings instead
1492            properties.insert(sanitized_name.clone(), param_schema);
1493            if param.required.unwrap_or(false) {
1494                required.push(sanitized_name);
1495            }
1496        }
1497
1498        // Process cookie parameters (rare, but supported)
1499        for param in &cookie_params {
1500            let (param_schema, mut annotations) = Self::convert_parameter_schema(
1501                param,
1502                ParameterIn::Cookie,
1503                spec,
1504                skip_parameter_descriptions,
1505                parameter_examples_in_description,
1506            )?;
1507
1508            // Sanitize parameter name after prefixing and add original name annotation if needed
1509            let prefixed_name = format!("cookie_{}", param.name);
1510            let sanitized_name = sanitize_property_name(&prefixed_name);
1511            if sanitized_name != prefixed_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: "cookie".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        // Add request body parameter if defined in the OpenAPI spec
1547        if let Some(request_body) = request_body
1548            && let Some((body_schema, _annotations, is_required)) =
1549                Self::convert_request_body_to_json_schema(request_body, spec)?
1550        {
1551            // Store parameter mapping for request_body
1552            parameter_mappings.insert(
1553                "request_body".to_string(),
1554                crate::tool::ParameterMapping {
1555                    sanitized_name: "request_body".to_string(),
1556                    original_name: "request_body".to_string(),
1557                    location: "body".to_string(),
1558                    explode: false,
1559                },
1560            );
1561
1562            // No longer apply annotations to schema - use parameter_mappings instead
1563            properties.insert("request_body".to_string(), body_schema);
1564            if is_required {
1565                required.push("request_body".to_string());
1566            }
1567        }
1568
1569        // Add special parameters for request configuration
1570        if !query_params.is_empty() || !header_params.is_empty() || !cookie_params.is_empty() {
1571            // Add optional timeout parameter
1572            properties.insert(
1573                "timeout_seconds".to_string(),
1574                json!({
1575                    "type": "integer",
1576                    "description": "Request timeout in seconds",
1577                    "minimum": 1,
1578                    "maximum": 300,
1579                    "default": 30
1580                }),
1581            );
1582        }
1583
1584        let schema = json!({
1585            "type": "object",
1586            "properties": properties,
1587            "required": required,
1588            "additionalProperties": false
1589        });
1590
1591        Ok((schema, parameter_mappings))
1592    }
1593
1594    /// Convert `OpenAPI` parameter schema to JSON Schema for MCP tools
1595    fn convert_parameter_schema(
1596        param: &Parameter,
1597        location: ParameterIn,
1598        spec: &Spec,
1599        skip_parameter_descriptions: bool,
1600        parameter_examples_in_description: bool,
1601    ) -> Result<(Value, Annotations), Error> {
1602        // Convert the parameter schema using the unified converter
1603        let base_schema = if let Some(schema_ref) = &param.schema {
1604            match schema_ref {
1605                ObjectOrReference::Object(obj_schema) => {
1606                    let mut visited = HashSet::new();
1607                    Self::convert_schema_to_json_schema(
1608                        &Schema::Object(Box::new(ObjectOrReference::Object(obj_schema.clone()))),
1609                        spec,
1610                        &mut visited,
1611                    )?
1612                }
1613                ObjectOrReference::Ref {
1614                    ref_path,
1615                    summary,
1616                    description,
1617                } => {
1618                    // Resolve the reference with metadata extraction
1619                    let mut visited = HashSet::new();
1620                    match Self::resolve_reference_with_metadata(
1621                        ref_path,
1622                        summary.clone(),
1623                        description.clone(),
1624                        spec,
1625                        &mut visited,
1626                    ) {
1627                        Ok((resolved_schema, ref_metadata)) => {
1628                            let mut schema_json = Self::convert_schema_to_json_schema(
1629                                &Schema::Object(Box::new(ObjectOrReference::Object(
1630                                    resolved_schema,
1631                                ))),
1632                                spec,
1633                                &mut visited,
1634                            )?;
1635
1636                            // Enhance schema with reference metadata if available
1637                            if let Value::Object(ref mut schema_obj) = schema_json {
1638                                // Reference metadata takes precedence over schema descriptions (OpenAPI 3.1 semantics)
1639                                if let Some(ref_desc) = ref_metadata.best_description() {
1640                                    schema_obj.insert("description".to_string(), json!(ref_desc));
1641                                }
1642                                // Fallback: if no reference metadata but schema lacks description, keep existing logic
1643                                // (This case is now handled by the reference metadata being None)
1644                            }
1645
1646                            schema_json
1647                        }
1648                        Err(_) => {
1649                            // Fallback to string for unresolvable references
1650                            json!({"type": "string"})
1651                        }
1652                    }
1653                }
1654            }
1655        } else {
1656            // Default to string if no schema
1657            json!({"type": "string"})
1658        };
1659
1660        // Merge the base schema properties with parameter metadata
1661        let mut result = match base_schema {
1662            Value::Object(obj) => obj,
1663            _ => {
1664                // This should never happen as our converter always returns objects
1665                return Err(Error::ToolGeneration(format!(
1666                    "Internal error: schema converter returned non-object for parameter '{}'",
1667                    param.name
1668                )));
1669            }
1670        };
1671
1672        // Collect examples from all sources, chained so neither the singular `example`
1673        // nor the plural `examples` is dropped at the parameter or schema level.
1674        let mut collected_examples: Vec<Value> = Vec::new();
1675
1676        // Parameter-level singular `example`.
1677        if let Some(example) = &param.example {
1678            collected_examples.push(example.clone());
1679        }
1680        // Parameter-level `examples` map.
1681        for example_ref in param.examples.values() {
1682            if let ObjectOrReference::Object(example_obj) = example_ref
1683                && let Some(value) = &example_obj.value
1684            {
1685                collected_examples.push(value.clone());
1686            }
1687            // References in the examples map are not resolved here.
1688        }
1689        // Schema-level singular `example` (added during base schema conversion).
1690        if let Some(example) = result.get("example") {
1691            collected_examples.push(example.clone());
1692        }
1693        // Schema-level plural `examples` (added during base schema conversion).
1694        if let Some(Value::Array(examples)) = result.get("examples") {
1695            collected_examples.extend(examples.iter().cloned());
1696        }
1697        // Array parameters: lift element-level `examples` to the parameter level, wrapping each
1698        // as a single-element array. A valid value for an array parameter is itself an array, and
1699        // MCP clients (and the inspector) read examples at the parameter level, not nested under
1700        // `items`. Clear them from `items` afterwards so the tokens are not duplicated.
1701        let lifted_item_examples: Option<Vec<Value>> = (result.get("type")
1702            == Some(&json!("array")))
1703        .then(|| {
1704            result
1705                .get("items")
1706                .and_then(|items| items.get("examples"))
1707                .and_then(Value::as_array)
1708                .cloned()
1709        })
1710        .flatten();
1711        if let Some(item_examples) = lifted_item_examples {
1712            for item_example in &item_examples {
1713                collected_examples.push(json!([item_example]));
1714            }
1715            if let Some(Value::Object(items)) = result.get_mut("items") {
1716                items.remove("examples");
1717            }
1718        }
1719        // De-duplicate while preserving first-seen order.
1720        let mut deduped: Vec<Value> = Vec::with_capacity(collected_examples.len());
1721        for example in collected_examples {
1722            if !deduped.contains(&example) {
1723                deduped.push(example);
1724            }
1725        }
1726        let collected_examples = deduped;
1727
1728        // Examples are emitted to exactly one channel to avoid duplicating their tokens.
1729        // Default: the structured `examples` field (the JSON Schema standard form). When
1730        // `parameter_examples_in_description` is set, they go into the parameter description
1731        // instead, for clients that do not read structured `examples` (e.g. OpenAI strict
1732        // mode). Ad-hoc example fields from the base schema conversion are cleared first so
1733        // examples cannot leak into both channels.
1734        result.remove("example");
1735        result.remove("examples");
1736
1737        let base_description = param
1738            .description
1739            .as_ref()
1740            .map(|d| d.to_string())
1741            .or_else(|| {
1742                result
1743                    .get("description")
1744                    .and_then(|d| d.as_str())
1745                    .map(|d| d.to_string())
1746            })
1747            .unwrap_or_else(|| format!("{} parameter", param.name));
1748
1749        let description = if parameter_examples_in_description {
1750            match Self::format_examples_for_description(&collected_examples) {
1751                Some(examples_str) => format!("{base_description}. {examples_str}"),
1752                None => base_description,
1753            }
1754        } else {
1755            base_description
1756        };
1757
1758        if !skip_parameter_descriptions {
1759            result.insert("description".to_string(), json!(description));
1760        }
1761
1762        if !parameter_examples_in_description && !collected_examples.is_empty() {
1763            result.insert("examples".to_string(), json!(collected_examples));
1764        }
1765
1766        // Create annotations instead of adding them to the JSON
1767        let mut annotations = Annotations::new()
1768            .with_location(Location::Parameter(location))
1769            .with_required(param.required.unwrap_or(false));
1770
1771        // Add explode annotation if present
1772        if let Some(explode) = param.explode {
1773            annotations = annotations.with_explode(explode);
1774        } else {
1775            // Default explode behavior based on OpenAPI spec:
1776            // - form style defaults to true
1777            // - other styles default to false
1778            let default_explode = match &param.style {
1779                Some(ParameterStyle::Form) | None => true, // form is default style
1780                _ => false,
1781            };
1782            annotations = annotations.with_explode(default_explode);
1783        }
1784
1785        Ok((Value::Object(result), annotations))
1786    }
1787
1788    /// Format examples for inclusion in parameter descriptions
1789    fn format_examples_for_description(examples: &[Value]) -> Option<String> {
1790        if examples.is_empty() {
1791            return None;
1792        }
1793
1794        if examples.len() == 1 {
1795            let example_str =
1796                serde_json::to_string(&examples[0]).unwrap_or_else(|_| "null".to_string());
1797            Some(format!("Example: `{example_str}`"))
1798        } else {
1799            let mut result = String::from("Examples:\n");
1800            for ex in examples {
1801                let json_str = serde_json::to_string(ex).unwrap_or_else(|_| "null".to_string());
1802                result.push_str(&format!("- `{json_str}`\n"));
1803            }
1804            // Remove trailing newline
1805            result.pop();
1806            Some(result)
1807        }
1808    }
1809
1810    /// Converts prefixItems (tuple-like arrays) to JSON Schema draft-07 compatible format.
1811    ///
1812    /// This handles OpenAPI 3.1 prefixItems which define specific schemas for each array position,
1813    /// converting them to draft-07 format that MCP tools can understand.
1814    ///
1815    /// Conversion strategy:
1816    /// - If items is `false`, set minItems=maxItems=prefix_items.len() for exact length
1817    /// - If all prefixItems have same type, use that type for items
1818    /// - If mixed types, use oneOf with all unique types from prefixItems
1819    /// - Add descriptive comment about tuple nature
1820    fn convert_prefix_items_to_draft07(
1821        prefix_items: &[ObjectOrReference<ObjectSchema>],
1822        items: &Option<Box<Schema>>,
1823        result: &mut serde_json::Map<String, Value>,
1824        spec: &Spec,
1825    ) -> Result<(), Error> {
1826        let prefix_count = prefix_items.len();
1827
1828        // Extract types from prefixItems
1829        let mut item_types = Vec::new();
1830        for prefix_item in prefix_items {
1831            match prefix_item {
1832                ObjectOrReference::Object(obj_schema) => {
1833                    if let Some(schema_type) = &obj_schema.schema_type {
1834                        match schema_type {
1835                            SchemaTypeSet::Single(SchemaType::String) => item_types.push("string"),
1836                            SchemaTypeSet::Single(SchemaType::Integer) => {
1837                                item_types.push("integer")
1838                            }
1839                            SchemaTypeSet::Single(SchemaType::Number) => item_types.push("number"),
1840                            SchemaTypeSet::Single(SchemaType::Boolean) => {
1841                                item_types.push("boolean")
1842                            }
1843                            SchemaTypeSet::Single(SchemaType::Array) => item_types.push("array"),
1844                            SchemaTypeSet::Single(SchemaType::Object) => item_types.push("object"),
1845                            _ => item_types.push("string"), // fallback
1846                        }
1847                    } else {
1848                        item_types.push("string"); // fallback
1849                    }
1850                }
1851                ObjectOrReference::Ref { ref_path, .. } => {
1852                    // Try to resolve the reference
1853                    let mut visited = HashSet::new();
1854                    match Self::resolve_reference(ref_path, spec, &mut visited) {
1855                        Ok(resolved_schema) => {
1856                            // Extract the type immediately and store it as a string
1857                            if let Some(schema_type_set) = &resolved_schema.schema_type {
1858                                match schema_type_set {
1859                                    SchemaTypeSet::Single(SchemaType::String) => {
1860                                        item_types.push("string")
1861                                    }
1862                                    SchemaTypeSet::Single(SchemaType::Integer) => {
1863                                        item_types.push("integer")
1864                                    }
1865                                    SchemaTypeSet::Single(SchemaType::Number) => {
1866                                        item_types.push("number")
1867                                    }
1868                                    SchemaTypeSet::Single(SchemaType::Boolean) => {
1869                                        item_types.push("boolean")
1870                                    }
1871                                    SchemaTypeSet::Single(SchemaType::Array) => {
1872                                        item_types.push("array")
1873                                    }
1874                                    SchemaTypeSet::Single(SchemaType::Object) => {
1875                                        item_types.push("object")
1876                                    }
1877                                    _ => item_types.push("string"), // fallback
1878                                }
1879                            } else {
1880                                item_types.push("string"); // fallback
1881                            }
1882                        }
1883                        Err(_) => {
1884                            // Fallback to string for unresolvable references
1885                            item_types.push("string");
1886                        }
1887                    }
1888                }
1889            }
1890        }
1891
1892        // Check if items is false (no additional items allowed)
1893        let items_is_false =
1894            matches!(items.as_ref().map(|i| i.as_ref()), Some(Schema::Boolean(b)) if !b.0);
1895
1896        if items_is_false {
1897            // Exact array length required
1898            result.insert("minItems".to_string(), json!(prefix_count));
1899            result.insert("maxItems".to_string(), json!(prefix_count));
1900        }
1901
1902        // Determine items schema based on prefixItems types
1903        let unique_types: std::collections::BTreeSet<_> = item_types.into_iter().collect();
1904
1905        if unique_types.len() == 1 {
1906            // All items have same type
1907            let item_type = unique_types.into_iter().next().unwrap();
1908            result.insert("items".to_string(), json!({"type": item_type}));
1909        } else if unique_types.len() > 1 {
1910            // Mixed types, use oneOf (sorted for consistent ordering)
1911            let one_of: Vec<Value> = unique_types
1912                .into_iter()
1913                .map(|t| json!({"type": t}))
1914                .collect();
1915            result.insert("items".to_string(), json!({"oneOf": one_of}));
1916        }
1917
1918        Ok(())
1919    }
1920
1921    /// Converts the new oas3 Schema enum (which can be Boolean or Object) to draft-07 format.
1922    ///
1923    /// The oas3 crate now supports:
1924    /// - Schema::Object(`ObjectOrReference<ObjectSchema>`) - regular object schemas
1925    /// - Schema::Boolean(BooleanSchema) - true/false schemas for validation control
1926    ///
1927    /// For MCP compatibility (draft-07), we convert:
1928    /// - Boolean true -> allow any items (no items constraint)
1929    /// - Boolean false -> not handled here (should be handled by caller with array constraints)
1930    ///
1931    /// Convert request body from OpenAPI to JSON Schema for MCP tools
1932    fn convert_request_body_to_json_schema(
1933        request_body_ref: &ObjectOrReference<RequestBody>,
1934        spec: &Spec,
1935    ) -> Result<Option<(Value, Annotations, bool)>, Error> {
1936        match request_body_ref {
1937            ObjectOrReference::Object(request_body) => {
1938                // Check for multipart/form-data first
1939                if let Some(media_type) = request_body.content.get("multipart/form-data") {
1940                    return Self::convert_multipart_request_body(request_body, media_type, spec);
1941                }
1942
1943                // Extract schema from request body content
1944                // Prioritize application/json content type
1945                let schema_info = request_body
1946                    .content
1947                    .get(mime::APPLICATION_JSON.as_ref())
1948                    .or_else(|| request_body.content.get("application/json"))
1949                    .or_else(|| {
1950                        // Fall back to first available content type
1951                        request_body.content.values().next()
1952                    });
1953
1954                if let Some(media_type) = schema_info {
1955                    if let Some(schema_ref) = &media_type.schema {
1956                        // Convert ObjectOrReference<ObjectSchema> to Schema
1957                        let schema = Schema::Object(Box::new(schema_ref.clone()));
1958
1959                        // Use the unified converter
1960                        let mut visited = HashSet::new();
1961                        let converted_schema =
1962                            Self::convert_schema_to_json_schema(&schema, spec, &mut visited)?;
1963
1964                        // Ensure we have an object schema
1965                        let mut schema_obj = match converted_schema {
1966                            Value::Object(obj) => obj,
1967                            _ => {
1968                                // If not an object, wrap it in an object
1969                                let mut obj = serde_json::Map::new();
1970                                obj.insert("type".to_string(), json!("object"));
1971                                obj.insert("additionalProperties".to_string(), json!(true));
1972                                obj
1973                            }
1974                        };
1975
1976                        // Add description following OpenAPI 3.1 precedence (schema description > request body description)
1977                        if !schema_obj.contains_key("description") {
1978                            let description = request_body
1979                                .description
1980                                .clone()
1981                                .unwrap_or_else(|| "Request body data".to_string());
1982                            schema_obj.insert("description".to_string(), json!(description));
1983                        }
1984
1985                        // Create annotations instead of adding them to the JSON
1986                        let annotations = Annotations::new()
1987                            .with_location(Location::Body)
1988                            .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
1989
1990                        let required = request_body.required.unwrap_or(false);
1991                        Ok(Some((Value::Object(schema_obj), annotations, required)))
1992                    } else {
1993                        Ok(None)
1994                    }
1995                } else {
1996                    Ok(None)
1997                }
1998            }
1999            ObjectOrReference::Ref {
2000                ref_path: _,
2001                summary,
2002                description,
2003            } => {
2004                // Use reference metadata to enhance request body description
2005                let ref_metadata = ReferenceMetadata::new(summary.clone(), description.clone());
2006                let enhanced_description = ref_metadata
2007                    .best_description()
2008                    .map(|desc| desc.to_string())
2009                    .unwrap_or_else(|| "Request body data".to_string());
2010
2011                let mut result = serde_json::Map::new();
2012                result.insert("type".to_string(), json!("object"));
2013                result.insert("additionalProperties".to_string(), json!(true));
2014                result.insert("description".to_string(), json!(enhanced_description));
2015
2016                // Create annotations instead of adding them to the JSON
2017                let annotations = Annotations::new()
2018                    .with_location(Location::Body)
2019                    .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
2020
2021                Ok(Some((Value::Object(result), annotations, false)))
2022            }
2023        }
2024    }
2025
2026    /// Convert multipart/form-data request body to JSON Schema.
2027    ///
2028    /// This function handles multipart/form-data content types by:
2029    /// 1. Iterating over all properties in the schema
2030    /// 2. Detecting file fields (format: binary or byte)
2031    /// 3. Transforming file fields to structured file object schemas
2032    /// 4. Keeping non-file fields as-is
2033    /// 5. Adding file_fields annotation for HTTP client processing
2034    fn convert_multipart_request_body(
2035        request_body: &RequestBody,
2036        media_type: &oas3::spec::MediaType,
2037        spec: &Spec,
2038    ) -> Result<Option<(Value, Annotations, bool)>, Error> {
2039        let Some(schema_ref) = &media_type.schema else {
2040            return Ok(None);
2041        };
2042
2043        // Get the properties from the schema
2044        let obj_schema = match schema_ref {
2045            ObjectOrReference::Object(obj) => obj.clone(),
2046            ObjectOrReference::Ref { ref_path, .. } => {
2047                // Resolve the reference
2048                let mut visited = HashSet::new();
2049                Self::resolve_reference(ref_path, spec, &mut visited)?
2050            }
2051        };
2052
2053        // Build properties with file field transformation
2054        let mut props_map = serde_json::Map::new();
2055        let mut file_fields = Vec::new();
2056
2057        for (prop_name, prop_schema_or_ref) in &obj_schema.properties {
2058            let sanitized_name = sanitize_property_name(prop_name);
2059
2060            let prop_schema = if Self::is_file_field_property(prop_schema_or_ref) {
2061                // Track this as a file field
2062                file_fields.push(sanitized_name.clone());
2063
2064                // Get the description from the original schema
2065                let description = match prop_schema_or_ref {
2066                    ObjectOrReference::Object(obj) => obj.description.as_deref(),
2067                    ObjectOrReference::Ref { .. } => None,
2068                };
2069
2070                // Transform to file object schema
2071                Self::convert_file_field_to_schema(description)
2072            } else {
2073                // Convert non-file field using standard conversion
2074                let schema = Schema::Object(Box::new(prop_schema_or_ref.clone()));
2075                let mut visited = HashSet::new();
2076                Self::convert_schema_to_json_schema(&schema, spec, &mut visited)?
2077            };
2078
2079            props_map.insert(sanitized_name, prop_schema);
2080        }
2081
2082        // Build the result schema
2083        let mut schema_obj = serde_json::Map::new();
2084        schema_obj.insert("type".to_string(), json!("object"));
2085
2086        if !props_map.is_empty() {
2087            schema_obj.insert("properties".to_string(), Value::Object(props_map));
2088        }
2089
2090        // Add required fields
2091        if !obj_schema.required.is_empty() {
2092            // Sanitize required field names
2093            let sanitized_required: Vec<String> = obj_schema
2094                .required
2095                .iter()
2096                .map(|name| sanitize_property_name(name))
2097                .collect();
2098            schema_obj.insert("required".to_string(), json!(sanitized_required));
2099        }
2100
2101        // Add description
2102        let description = obj_schema
2103            .description
2104            .clone()
2105            .or_else(|| request_body.description.clone())
2106            .unwrap_or_else(|| "Request body data".to_string());
2107        schema_obj.insert("description".to_string(), json!(description));
2108
2109        // Create annotations with multipart/form-data content type and file fields
2110        let mut annotations = Annotations::new()
2111            .with_location(Location::Body)
2112            .with_content_type("multipart/form-data".to_string());
2113
2114        if !file_fields.is_empty() {
2115            annotations = annotations.with_file_fields(file_fields);
2116        }
2117
2118        let required = request_body.required.unwrap_or(false);
2119        Ok(Some((Value::Object(schema_obj), annotations, required)))
2120    }
2121
2122    /// Extract parameter values from MCP tool call arguments
2123    ///
2124    /// # Errors
2125    ///
2126    /// Returns an error if the arguments are invalid or missing required parameters
2127    pub fn extract_parameters(
2128        tool_metadata: &ToolMetadata,
2129        arguments: &Value,
2130    ) -> Result<ExtractedParameters, ToolCallValidationError> {
2131        let args = arguments.as_object().ok_or_else(|| {
2132            ToolCallValidationError::RequestConstructionError {
2133                reason: "Arguments must be an object".to_string(),
2134            }
2135        })?;
2136
2137        trace!(
2138            tool_name = %tool_metadata.name,
2139            raw_arguments = ?arguments,
2140            "Starting parameter extraction"
2141        );
2142
2143        let mut path_params = HashMap::new();
2144        let mut query_params = HashMap::new();
2145        let mut header_params = HashMap::new();
2146        let mut cookie_params = HashMap::new();
2147        let mut body_params = HashMap::new();
2148        let mut config = RequestConfig::default();
2149
2150        // Extract timeout if provided
2151        if let Some(timeout) = args.get("timeout_seconds").and_then(Value::as_u64) {
2152            config.timeout_seconds = u32::try_from(timeout).unwrap_or(u32::MAX);
2153        }
2154
2155        // Process each argument
2156        for (key, value) in args {
2157            if key == "timeout_seconds" {
2158                continue; // Already processed
2159            }
2160
2161            // Handle special request_body parameter
2162            if key == "request_body" {
2163                body_params.insert("request_body".to_string(), value.clone());
2164                continue;
2165            }
2166
2167            // Get parameter mapping from tool metadata
2168            let mapping = tool_metadata.parameter_mappings.get(key);
2169
2170            if let Some(mapping) = mapping {
2171                // Use server-side parameter mapping
2172                match mapping.location.as_str() {
2173                    "path" => {
2174                        path_params.insert(mapping.original_name.clone(), value.clone());
2175                    }
2176                    "query" => {
2177                        query_params.insert(
2178                            mapping.original_name.clone(),
2179                            QueryParameter::new(value.clone(), mapping.explode),
2180                        );
2181                    }
2182                    "header" => {
2183                        header_params.insert(mapping.original_name.clone(), value.clone());
2184                    }
2185                    "cookie" => {
2186                        cookie_params.insert(mapping.original_name.clone(), value.clone());
2187                    }
2188                    "body" => {
2189                        body_params.insert(mapping.original_name.clone(), value.clone());
2190                    }
2191                    _ => {
2192                        return Err(ToolCallValidationError::RequestConstructionError {
2193                            reason: format!("Unknown parameter location for parameter: {key}"),
2194                        });
2195                    }
2196                }
2197            } else {
2198                // Fallback to schema annotations for backward compatibility
2199                let location = Self::get_parameter_location(tool_metadata, key).map_err(|e| {
2200                    ToolCallValidationError::RequestConstructionError {
2201                        reason: e.to_string(),
2202                    }
2203                })?;
2204
2205                let original_name = Self::get_original_parameter_name(tool_metadata, key);
2206
2207                match location.as_str() {
2208                    "path" => {
2209                        path_params
2210                            .insert(original_name.unwrap_or_else(|| key.clone()), value.clone());
2211                    }
2212                    "query" => {
2213                        let param_name = original_name.unwrap_or_else(|| key.clone());
2214                        let explode = Self::get_parameter_explode(tool_metadata, key);
2215                        query_params
2216                            .insert(param_name, QueryParameter::new(value.clone(), explode));
2217                    }
2218                    "header" => {
2219                        let header_name = if let Some(orig) = original_name {
2220                            orig
2221                        } else if key.starts_with("header_") {
2222                            key.strip_prefix("header_").unwrap_or(key).to_string()
2223                        } else {
2224                            key.clone()
2225                        };
2226                        header_params.insert(header_name, value.clone());
2227                    }
2228                    "cookie" => {
2229                        let cookie_name = if let Some(orig) = original_name {
2230                            orig
2231                        } else if key.starts_with("cookie_") {
2232                            key.strip_prefix("cookie_").unwrap_or(key).to_string()
2233                        } else {
2234                            key.clone()
2235                        };
2236                        cookie_params.insert(cookie_name, value.clone());
2237                    }
2238                    "body" => {
2239                        let body_name = if key.starts_with("body_") {
2240                            key.strip_prefix("body_").unwrap_or(key).to_string()
2241                        } else {
2242                            key.clone()
2243                        };
2244                        body_params.insert(body_name, value.clone());
2245                    }
2246                    _ => {
2247                        return Err(ToolCallValidationError::RequestConstructionError {
2248                            reason: format!("Unknown parameter location for parameter: {key}"),
2249                        });
2250                    }
2251                }
2252            }
2253        }
2254
2255        let extracted = ExtractedParameters {
2256            path: path_params,
2257            query: query_params,
2258            headers: header_params,
2259            cookies: cookie_params,
2260            body: body_params,
2261            config,
2262        };
2263
2264        trace!(
2265            tool_name = %tool_metadata.name,
2266            extracted_parameters = ?extracted,
2267            "Parameter extraction completed"
2268        );
2269
2270        // Validate parameters against tool metadata using the original arguments
2271        Self::validate_parameters(tool_metadata, arguments)?;
2272
2273        Ok(extracted)
2274    }
2275
2276    /// Get the original parameter name from x-original-name annotation if it exists
2277    fn get_original_parameter_name(
2278        tool_metadata: &ToolMetadata,
2279        param_name: &str,
2280    ) -> Option<String> {
2281        tool_metadata
2282            .parameters
2283            .get("properties")
2284            .and_then(|p| p.as_object())
2285            .and_then(|props| props.get(param_name))
2286            .and_then(|schema| schema.get(X_ORIGINAL_NAME))
2287            .and_then(|v| v.as_str())
2288            .map(|s| s.to_string())
2289    }
2290
2291    /// Get parameter explode setting from tool metadata
2292    fn get_parameter_explode(tool_metadata: &ToolMetadata, param_name: &str) -> bool {
2293        tool_metadata
2294            .parameters
2295            .get("properties")
2296            .and_then(|p| p.as_object())
2297            .and_then(|props| props.get(param_name))
2298            .and_then(|schema| schema.get(X_PARAMETER_EXPLODE))
2299            .and_then(|v| v.as_bool())
2300            .unwrap_or(true) // Default to true (OpenAPI default for form style)
2301    }
2302
2303    /// Get parameter location from tool metadata
2304    fn get_parameter_location(
2305        tool_metadata: &ToolMetadata,
2306        param_name: &str,
2307    ) -> Result<String, Error> {
2308        let properties = tool_metadata
2309            .parameters
2310            .get("properties")
2311            .and_then(|p| p.as_object())
2312            .ok_or_else(|| Error::ToolGeneration("Invalid tool parameters schema".to_string()))?;
2313
2314        if let Some(param_schema) = properties.get(param_name)
2315            && let Some(location) = param_schema
2316                .get(X_PARAMETER_LOCATION)
2317                .and_then(|v| v.as_str())
2318        {
2319            return Ok(location.to_string());
2320        }
2321
2322        // Fallback: infer from parameter name prefix
2323        if param_name.starts_with("header_") {
2324            Ok("header".to_string())
2325        } else if param_name.starts_with("cookie_") {
2326            Ok("cookie".to_string())
2327        } else if param_name.starts_with("body_") {
2328            Ok("body".to_string())
2329        } else {
2330            // Default to query for unknown parameters
2331            Ok("query".to_string())
2332        }
2333    }
2334
2335    /// Validate parameters against tool metadata
2336    fn validate_parameters(
2337        tool_metadata: &ToolMetadata,
2338        arguments: &Value,
2339    ) -> Result<(), ToolCallValidationError> {
2340        let schema = &tool_metadata.parameters;
2341
2342        // Get required parameters from schema
2343        let required_params = schema
2344            .get("required")
2345            .and_then(|r| r.as_array())
2346            .map(|arr| {
2347                arr.iter()
2348                    .filter_map(|v| v.as_str())
2349                    .collect::<std::collections::HashSet<_>>()
2350            })
2351            .unwrap_or_default();
2352
2353        let properties = schema
2354            .get("properties")
2355            .and_then(|p| p.as_object())
2356            .ok_or_else(|| ToolCallValidationError::RequestConstructionError {
2357                reason: "Tool schema missing properties".to_string(),
2358            })?;
2359
2360        let args = arguments.as_object().ok_or_else(|| {
2361            ToolCallValidationError::RequestConstructionError {
2362                reason: "Arguments must be an object".to_string(),
2363            }
2364        })?;
2365
2366        // Collect ALL validation errors before returning
2367        let mut all_errors = Vec::new();
2368
2369        // Check for unknown parameters
2370        all_errors.extend(Self::check_unknown_parameters(args, properties));
2371
2372        // Check all required parameters are provided in the arguments
2373        all_errors.extend(Self::check_missing_required(
2374            args,
2375            properties,
2376            &required_params,
2377        ));
2378
2379        // Validate parameter values against their schemas
2380        all_errors.extend(Self::validate_parameter_values(
2381            args,
2382            properties,
2383            &required_params,
2384        ));
2385
2386        // Return all errors if any were found
2387        if !all_errors.is_empty() {
2388            return Err(ToolCallValidationError::InvalidParameters {
2389                violations: all_errors,
2390            });
2391        }
2392
2393        Ok(())
2394    }
2395
2396    /// Check for unknown parameters in the provided arguments
2397    fn check_unknown_parameters(
2398        args: &serde_json::Map<String, Value>,
2399        properties: &serde_json::Map<String, Value>,
2400    ) -> Vec<ValidationError> {
2401        let mut errors = Vec::new();
2402
2403        // Get list of valid parameter names
2404        let valid_params: Vec<String> = properties.keys().map(|s| s.to_string()).collect();
2405
2406        // Check each provided argument
2407        for (arg_name, _) in args.iter() {
2408            if !properties.contains_key(arg_name) {
2409                // Create InvalidParameter error with suggestions
2410                errors.push(ValidationError::invalid_parameter(
2411                    arg_name.clone(),
2412                    &valid_params,
2413                ));
2414            }
2415        }
2416
2417        errors
2418    }
2419
2420    /// Check for missing required parameters
2421    fn check_missing_required(
2422        args: &serde_json::Map<String, Value>,
2423        properties: &serde_json::Map<String, Value>,
2424        required_params: &HashSet<&str>,
2425    ) -> Vec<ValidationError> {
2426        let mut errors = Vec::new();
2427
2428        for required_param in required_params {
2429            if !args.contains_key(*required_param) {
2430                // Get the parameter schema to extract description and type
2431                let param_schema = properties.get(*required_param);
2432
2433                let description = param_schema
2434                    .and_then(|schema| schema.get("description"))
2435                    .and_then(|d| d.as_str())
2436                    .map(|s| s.to_string());
2437
2438                let expected_type = param_schema
2439                    .and_then(Self::get_expected_type)
2440                    .unwrap_or_else(|| "unknown".to_string());
2441
2442                errors.push(ValidationError::MissingRequiredParameter {
2443                    parameter: (*required_param).to_string(),
2444                    description,
2445                    expected_type,
2446                });
2447            }
2448        }
2449
2450        errors
2451    }
2452
2453    /// Validate parameter values against their schemas
2454    fn validate_parameter_values(
2455        args: &serde_json::Map<String, Value>,
2456        properties: &serde_json::Map<String, Value>,
2457        required_params: &std::collections::HashSet<&str>,
2458    ) -> Vec<ValidationError> {
2459        let mut errors = Vec::new();
2460
2461        for (param_name, param_value) in args {
2462            if let Some(param_schema) = properties.get(param_name) {
2463                // Check if this is a null value to provide better error messages
2464                let is_null_value = param_value.is_null();
2465                let is_required = required_params.contains(param_name.as_str());
2466
2467                // Create a schema that wraps the parameter schema
2468                let schema = json!({
2469                    "type": "object",
2470                    "properties": {
2471                        param_name: param_schema
2472                    }
2473                });
2474
2475                // Compile the schema
2476                let compiled = match jsonschema::validator_for(&schema) {
2477                    Ok(compiled) => compiled,
2478                    Err(e) => {
2479                        errors.push(ValidationError::ConstraintViolation {
2480                            parameter: param_name.clone(),
2481                            message: format!(
2482                                "Failed to compile schema for parameter '{param_name}': {e}"
2483                            ),
2484                            field_path: None,
2485                            actual_value: None,
2486                            expected_type: None,
2487                            constraints: vec![],
2488                        });
2489                        continue;
2490                    }
2491                };
2492
2493                // Create an object with just this parameter to validate
2494                let instance = json!({ param_name: param_value });
2495
2496                // Validate and collect all errors for this parameter
2497                let validation_errors: Vec<_> =
2498                    compiled.validate(&instance).err().into_iter().collect();
2499
2500                for validation_error in validation_errors {
2501                    // Extract error details
2502                    let error_message = validation_error.to_string();
2503                    let instance_path_str = validation_error.instance_path().to_string();
2504                    let field_path = if instance_path_str.is_empty() || instance_path_str == "/" {
2505                        Some(param_name.clone())
2506                    } else {
2507                        Some(instance_path_str.trim_start_matches('/').to_string())
2508                    };
2509
2510                    // Extract constraints from the schema
2511                    let constraints = Self::extract_constraints_from_schema(param_schema);
2512
2513                    // Determine expected type
2514                    let expected_type = Self::get_expected_type(param_schema);
2515
2516                    // Generate context-aware error message for null values
2517                    // Check if this is a null value error (either top-level null or nested null in message)
2518                    // This is important because some LLMs might confuse "not required" with "nullable"
2519                    let maybe_type_error = match &validation_error.kind() {
2520                        ValidationErrorKind::Type { kind } => Some(kind),
2521                        _ => None,
2522                    };
2523                    let is_type_error = maybe_type_error.is_some();
2524                    let is_null_error = is_null_value
2525                        || (is_type_error && validation_error.instance().as_null().is_some());
2526                    let message = if is_null_error && let Some(type_error) = maybe_type_error {
2527                        // Extract the field name from field_path if available
2528                        let field_name = field_path.as_ref().unwrap_or(param_name);
2529
2530                        // Determine the expected type from the error message if not available from schema
2531                        let final_expected_type =
2532                            expected_type.clone().unwrap_or_else(|| match type_error {
2533                                TypeKind::Single(json_type) => json_type.to_string(),
2534                                TypeKind::Multiple(json_type_set) => json_type_set
2535                                    .iter()
2536                                    .map(|t| t.to_string())
2537                                    .collect::<Vec<_>>()
2538                                    .join(", "),
2539                            });
2540
2541                        // Check if this field is required by looking at the constraints
2542                        // Extract the actual field name from field_path (e.g., "request_body/name" -> "name")
2543                        let actual_field_name = field_path
2544                            .as_ref()
2545                            .and_then(|path| path.split('/').next_back())
2546                            .unwrap_or(param_name);
2547
2548                        // For nested fields (field_path contains '/'), only check the constraint
2549                        // For top-level fields, use the is_required parameter
2550                        let is_nested_field = field_path.as_ref().is_some_and(|p| p.contains('/'));
2551
2552                        let field_is_required = if is_nested_field {
2553                            constraints.iter().any(|c| {
2554                                if let ValidationConstraint::Required { properties } = c {
2555                                    properties.contains(&actual_field_name.to_string())
2556                                } else {
2557                                    false
2558                                }
2559                            })
2560                        } else {
2561                            is_required
2562                        };
2563
2564                        if field_is_required {
2565                            format!(
2566                                "Parameter '{field_name}' is required and must not be null (expected: {final_expected_type})"
2567                            )
2568                        } else {
2569                            format!(
2570                                "Parameter '{field_name}' is optional but must not be null (expected: {final_expected_type})"
2571                            )
2572                        }
2573                    } else {
2574                        error_message
2575                    };
2576
2577                    errors.push(ValidationError::ConstraintViolation {
2578                        parameter: param_name.clone(),
2579                        message,
2580                        field_path,
2581                        actual_value: Some(Box::new(param_value.clone())),
2582                        expected_type,
2583                        constraints,
2584                    });
2585                }
2586            }
2587        }
2588
2589        errors
2590    }
2591
2592    /// Extract validation constraints from a schema
2593    fn extract_constraints_from_schema(schema: &Value) -> Vec<ValidationConstraint> {
2594        let mut constraints = Vec::new();
2595
2596        // Minimum value constraint
2597        if let Some(min_value) = schema.get("minimum").and_then(|v| v.as_f64()) {
2598            let exclusive = schema
2599                .get("exclusiveMinimum")
2600                .and_then(|v| v.as_bool())
2601                .unwrap_or(false);
2602            constraints.push(ValidationConstraint::Minimum {
2603                value: min_value,
2604                exclusive,
2605            });
2606        }
2607
2608        // Maximum value constraint
2609        if let Some(max_value) = schema.get("maximum").and_then(|v| v.as_f64()) {
2610            let exclusive = schema
2611                .get("exclusiveMaximum")
2612                .and_then(|v| v.as_bool())
2613                .unwrap_or(false);
2614            constraints.push(ValidationConstraint::Maximum {
2615                value: max_value,
2616                exclusive,
2617            });
2618        }
2619
2620        // Minimum length constraint
2621        if let Some(min_len) = schema
2622            .get("minLength")
2623            .and_then(|v| v.as_u64())
2624            .map(|v| v as usize)
2625        {
2626            constraints.push(ValidationConstraint::MinLength { value: min_len });
2627        }
2628
2629        // Maximum length constraint
2630        if let Some(max_len) = schema
2631            .get("maxLength")
2632            .and_then(|v| v.as_u64())
2633            .map(|v| v as usize)
2634        {
2635            constraints.push(ValidationConstraint::MaxLength { value: max_len });
2636        }
2637
2638        // Pattern constraint
2639        if let Some(pattern) = schema
2640            .get("pattern")
2641            .and_then(|v| v.as_str())
2642            .map(|s| s.to_string())
2643        {
2644            constraints.push(ValidationConstraint::Pattern { pattern });
2645        }
2646
2647        // Enum values constraint
2648        if let Some(enum_values) = schema.get("enum").and_then(|v| v.as_array()).cloned() {
2649            constraints.push(ValidationConstraint::EnumValues {
2650                values: enum_values,
2651            });
2652        }
2653
2654        // Format constraint
2655        if let Some(format) = schema
2656            .get("format")
2657            .and_then(|v| v.as_str())
2658            .map(|s| s.to_string())
2659        {
2660            constraints.push(ValidationConstraint::Format { format });
2661        }
2662
2663        // Multiple of constraint
2664        if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
2665            constraints.push(ValidationConstraint::MultipleOf { value: multiple_of });
2666        }
2667
2668        // Minimum items constraint
2669        if let Some(min_items) = schema
2670            .get("minItems")
2671            .and_then(|v| v.as_u64())
2672            .map(|v| v as usize)
2673        {
2674            constraints.push(ValidationConstraint::MinItems { value: min_items });
2675        }
2676
2677        // Maximum items constraint
2678        if let Some(max_items) = schema
2679            .get("maxItems")
2680            .and_then(|v| v.as_u64())
2681            .map(|v| v as usize)
2682        {
2683            constraints.push(ValidationConstraint::MaxItems { value: max_items });
2684        }
2685
2686        // Unique items constraint
2687        if let Some(true) = schema.get("uniqueItems").and_then(|v| v.as_bool()) {
2688            constraints.push(ValidationConstraint::UniqueItems);
2689        }
2690
2691        // Minimum properties constraint
2692        if let Some(min_props) = schema
2693            .get("minProperties")
2694            .and_then(|v| v.as_u64())
2695            .map(|v| v as usize)
2696        {
2697            constraints.push(ValidationConstraint::MinProperties { value: min_props });
2698        }
2699
2700        // Maximum properties constraint
2701        if let Some(max_props) = schema
2702            .get("maxProperties")
2703            .and_then(|v| v.as_u64())
2704            .map(|v| v as usize)
2705        {
2706            constraints.push(ValidationConstraint::MaxProperties { value: max_props });
2707        }
2708
2709        // Constant value constraint
2710        if let Some(const_value) = schema.get("const").cloned() {
2711            constraints.push(ValidationConstraint::ConstValue { value: const_value });
2712        }
2713
2714        // Required properties constraint
2715        if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
2716            let properties: Vec<String> = required
2717                .iter()
2718                .filter_map(|v| v.as_str().map(|s| s.to_string()))
2719                .collect();
2720            if !properties.is_empty() {
2721                constraints.push(ValidationConstraint::Required { properties });
2722            }
2723        }
2724
2725        constraints
2726    }
2727
2728    /// Get the expected type from a schema
2729    fn get_expected_type(schema: &Value) -> Option<String> {
2730        if let Some(type_value) = schema.get("type") {
2731            if let Some(type_str) = type_value.as_str() {
2732                return Some(type_str.to_string());
2733            } else if let Some(type_array) = type_value.as_array() {
2734                // Handle multiple types (e.g., ["string", "null"])
2735                let types: Vec<String> = type_array
2736                    .iter()
2737                    .filter_map(|v| v.as_str())
2738                    .map(|s| s.to_string())
2739                    .collect();
2740                if !types.is_empty() {
2741                    return Some(types.join(" | "));
2742                }
2743            }
2744        }
2745        None
2746    }
2747
2748    /// Wrap an output schema to include both success and error responses
2749    ///
2750    /// This function creates a unified response schema that can represent both successful
2751    /// responses and error responses. It uses `json!()` macro instead of `schema_for!()`
2752    /// for several important reasons:
2753    ///
2754    /// 1. **Dynamic Schema Construction**: The success schema is dynamically converted from
2755    ///    OpenAPI specifications at runtime, not from a static Rust type. The `schema_for!()`
2756    ///    macro requires a compile-time type, but we're working with schemas that are only
2757    ///    known when parsing the OpenAPI spec.
2758    ///
2759    /// 2. **Composite Schema Building**: The function builds a complex wrapper schema that:
2760    ///    - Contains a dynamically-converted OpenAPI schema for success responses
2761    ///    - Includes a statically-typed error schema (which does use `schema_for!()`)
2762    ///    - Adds metadata fields like HTTP status codes and descriptions
2763    ///    - Uses JSON Schema's `oneOf` to allow either success or error responses
2764    ///
2765    /// 3. **Runtime Flexibility**: OpenAPI schemas can have arbitrary complexity and types
2766    ///    that don't map directly to Rust types. Using `json!()` allows us to construct
2767    ///    the exact JSON Schema structure needed without being constrained by Rust's type system.
2768    ///
2769    /// The error schema component does use `schema_for!(ErrorResponse)` (via `create_error_response_schema()`)
2770    /// because `ErrorResponse` is a known Rust type, but the overall wrapper must be built dynamically.
2771    fn wrap_output_schema(
2772        body_schema: &ObjectOrReference<ObjectSchema>,
2773        spec: &Spec,
2774    ) -> Result<Value, Error> {
2775        // Convert the body schema to JSON
2776        let mut visited = HashSet::new();
2777        let body_schema_json = match body_schema {
2778            ObjectOrReference::Object(obj_schema) => {
2779                Self::convert_object_schema_to_json_schema(obj_schema, spec, &mut visited)?
2780            }
2781            ObjectOrReference::Ref { ref_path, .. } => {
2782                let resolved = Self::resolve_reference(ref_path, spec, &mut visited)?;
2783                let result =
2784                    Self::convert_object_schema_to_json_schema(&resolved, spec, &mut visited)?;
2785                // Remove after conversion to allow schema reuse (see convert_schema_to_json_schema)
2786                visited.remove(ref_path);
2787                result
2788            }
2789        };
2790
2791        let error_schema = create_error_response_schema();
2792
2793        Ok(json!({
2794            "type": "object",
2795            "description": "Unified response structure with success and error variants",
2796            "required": ["status", "body"],
2797            "additionalProperties": false,
2798            "properties": {
2799                "status": {
2800                    "type": "integer",
2801                    "description": "HTTP status code",
2802                    "minimum": 100,
2803                    "maximum": 599
2804                },
2805                "body": {
2806                    "description": "Response body - either success data or error information",
2807                    "oneOf": [
2808                        body_schema_json,
2809                        error_schema
2810                    ]
2811                }
2812            }
2813        }))
2814    }
2815
2816    /// Check if a schema represents a file field based on its format.
2817    ///
2818    /// Returns `true` if the schema has `format: binary` or `format: byte`,
2819    /// which indicates a file upload field in multipart/form-data requests.
2820    ///
2821    /// # Arguments
2822    /// * `schema` - The OpenAPI Schema to check
2823    ///
2824    /// # Returns
2825    /// `true` if the schema represents a file field, `false` otherwise
2826    #[must_use]
2827    pub fn is_file_field(schema: &Schema) -> bool {
2828        match schema {
2829            Schema::Object(obj_or_ref) => match obj_or_ref.as_ref() {
2830                ObjectOrReference::Object(obj_schema) => {
2831                    Self::is_file_field_object_schema(obj_schema)
2832                }
2833                ObjectOrReference::Ref { .. } => {
2834                    // References need to be resolved first; return false for unresolved refs
2835                    false
2836                }
2837            },
2838            Schema::Boolean(_) => false,
2839        }
2840    }
2841
2842    /// Check if an ObjectSchema represents a file field based on its format.
2843    ///
2844    /// Returns `true` if the schema has `format: binary` or `format: byte`,
2845    /// which indicates a file upload field in multipart/form-data requests.
2846    fn is_file_field_object_schema(obj_schema: &ObjectSchema) -> bool {
2847        if let Some(format) = &obj_schema.format {
2848            format == "binary" || format == "byte"
2849        } else {
2850            false
2851        }
2852    }
2853
2854    /// Check if an ObjectOrReference<ObjectSchema> represents a file field.
2855    ///
2856    /// This is a convenience method for checking file fields when iterating
2857    /// over properties in a multipart/form-data schema.
2858    fn is_file_field_property(prop_schema: &ObjectOrReference<ObjectSchema>) -> bool {
2859        match prop_schema {
2860            ObjectOrReference::Object(obj_schema) => Self::is_file_field_object_schema(obj_schema),
2861            ObjectOrReference::Ref { .. } => {
2862                // References need to be resolved first; return false for unresolved refs
2863                false
2864            }
2865        }
2866    }
2867
2868    /// Convert a file field to the structured file object schema.
2869    ///
2870    /// Transforms a file field (format: binary or byte) into a structured
2871    /// object schema with `content` (required) and `filename` (optional) properties.
2872    /// The content field expects a data URI format (e.g., `data:image/png;base64,...`).
2873    ///
2874    /// # Arguments
2875    /// * `original_description` - The original description from the OpenAPI schema
2876    ///
2877    /// # Returns
2878    /// A JSON Schema value representing the file object structure
2879    fn convert_file_field_to_schema(original_description: Option<&str>) -> Value {
2880        let description = original_description.unwrap_or("File upload");
2881        json!({
2882            "type": "object",
2883            "description": description,
2884            "properties": {
2885                "content": {
2886                    "type": "string",
2887                    "description": "File content as data URI (e.g., data:image/png;base64,...)"
2888                },
2889                "filename": {
2890                    "type": "string",
2891                    "description": "Optional filename for the upload"
2892                }
2893            },
2894            "required": ["content"]
2895        })
2896    }
2897}
2898
2899/// Create the error schema structure that all tool errors conform to
2900fn create_error_response_schema() -> Value {
2901    let root_schema = schema_for!(ErrorResponse);
2902    let schema_json = serde_json::to_value(root_schema).expect("Valid error schema");
2903
2904    // Extract definitions/defs for inlining
2905    let definitions = schema_json
2906        .get("$defs")
2907        .or_else(|| schema_json.get("definitions"))
2908        .cloned()
2909        .unwrap_or_else(|| json!({}));
2910
2911    // Clone the schema and remove metadata
2912    let mut result = schema_json.clone();
2913    if let Some(obj) = result.as_object_mut() {
2914        obj.remove("$schema");
2915        obj.remove("$defs");
2916        obj.remove("definitions");
2917        obj.remove("title");
2918    }
2919
2920    // Inline all references
2921    inline_refs(&mut result, &definitions);
2922
2923    result
2924}
2925
2926/// Recursively inline all $ref references in a JSON Schema
2927fn inline_refs(schema: &mut Value, definitions: &Value) {
2928    match schema {
2929        Value::Object(obj) => {
2930            // Check if this object has a $ref
2931            if let Some(ref_value) = obj.get("$ref").cloned()
2932                && let Some(ref_str) = ref_value.as_str()
2933            {
2934                // Extract the definition name from the ref
2935                let def_name = ref_str
2936                    .strip_prefix("#/$defs/")
2937                    .or_else(|| ref_str.strip_prefix("#/definitions/"));
2938
2939                if let Some(name) = def_name
2940                    && let Some(definition) = definitions.get(name)
2941                {
2942                    // Replace the entire object with the definition
2943                    *schema = definition.clone();
2944                    // Continue to inline any refs in the definition
2945                    inline_refs(schema, definitions);
2946                    return;
2947                }
2948            }
2949
2950            // Recursively process all values in the object
2951            for (_, value) in obj.iter_mut() {
2952                inline_refs(value, definitions);
2953            }
2954        }
2955        Value::Array(arr) => {
2956            // Recursively process all items in the array
2957            for item in arr.iter_mut() {
2958                inline_refs(item, definitions);
2959            }
2960        }
2961        _ => {} // Other types don't contain refs
2962    }
2963}
2964
2965/// Query parameter with explode information
2966#[derive(Debug, Clone)]
2967pub struct QueryParameter {
2968    pub value: Value,
2969    pub explode: bool,
2970}
2971
2972impl QueryParameter {
2973    pub fn new(value: Value, explode: bool) -> Self {
2974        Self { value, explode }
2975    }
2976}
2977
2978/// Extracted parameters from MCP tool call
2979#[derive(Debug, Clone)]
2980pub struct ExtractedParameters {
2981    pub path: HashMap<String, Value>,
2982    pub query: HashMap<String, QueryParameter>,
2983    pub headers: HashMap<String, Value>,
2984    pub cookies: HashMap<String, Value>,
2985    pub body: HashMap<String, Value>,
2986    pub config: RequestConfig,
2987}
2988
2989/// Request configuration options
2990#[derive(Debug, Clone)]
2991pub struct RequestConfig {
2992    pub timeout_seconds: u32,
2993    pub content_type: String,
2994}
2995
2996impl Default for RequestConfig {
2997    fn default() -> Self {
2998        Self {
2999            timeout_seconds: 30,
3000            content_type: mime::APPLICATION_JSON.to_string(),
3001        }
3002    }
3003}
3004
3005#[cfg(test)]
3006mod tests {
3007    use super::*;
3008
3009    use insta::assert_json_snapshot;
3010    use oas3::spec::{
3011        BooleanSchema, Components, MediaType, ObjectOrReference, ObjectSchema, Operation,
3012        Parameter, ParameterIn, RequestBody, Schema, SchemaType, SchemaTypeSet, Spec,
3013    };
3014    use rmcp::model::Tool;
3015    use serde_json::{Value, json};
3016    use std::collections::BTreeMap;
3017
3018    #[test]
3019    fn converter_preserves_schema_level_examples_plural() {
3020        let spec = create_test_spec();
3021        let schema: ObjectSchema = serde_json::from_value(json!({
3022            "type": "string",
3023            "examples": ["a", "a.b", "a.b.c"],
3024        }))
3025        .expect("valid object schema");
3026        let mut visited = std::collections::HashSet::new();
3027        let result =
3028            ToolGenerator::convert_object_schema_to_json_schema(&schema, &spec, &mut visited)
3029                .expect("conversion succeeds");
3030        assert_eq!(result["type"], json!("string"));
3031        assert_eq!(
3032            result["examples"],
3033            json!(["a", "a.b", "a.b.c"]),
3034            "schema-level plural `examples` must be preserved: {result}"
3035        );
3036    }
3037
3038    fn parameter_with_singular_and_named_map_examples() -> Parameter {
3039        serde_json::from_value(json!({
3040            "name": "q",
3041            "in": "query",
3042            "schema": { "type": "string" },
3043            "example": "alpha",
3044            "examples": {
3045                "beta": { "value": "beta" },
3046                "gamma": { "value": "gamma" },
3047            },
3048        }))
3049        .expect("valid parameter")
3050    }
3051
3052    #[test]
3053    fn parameter_examples_default_to_structured_field() {
3054        let spec = create_test_spec();
3055        let param = parameter_with_singular_and_named_map_examples();
3056        // Default: examples chained from all sources into the structured `examples` field,
3057        // not duplicated into the description.
3058        let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3059            &param,
3060            ParameterIn::Query,
3061            &spec,
3062            false,
3063            false,
3064        )
3065        .expect("conversion succeeds");
3066        let values: Vec<String> = result["examples"]
3067            .as_array()
3068            .expect("structured `examples` present")
3069            .iter()
3070            .filter_map(|value| value.as_str().map(ToString::to_string))
3071            .collect();
3072        assert!(
3073            values.iter().any(|v| v == "alpha")
3074                && values.iter().any(|v| v == "beta")
3075                && values.iter().any(|v| v == "gamma"),
3076            "all sources chained into structured `examples`: {result}"
3077        );
3078        let description = result["description"].as_str().unwrap_or_default();
3079        assert!(
3080            !description.contains("alpha") && !description.contains("beta"),
3081            "examples must not be duplicated into the description by default: {description}"
3082        );
3083    }
3084
3085    #[test]
3086    fn parameter_examples_in_description_when_flag_set() {
3087        let spec = create_test_spec();
3088        let param = parameter_with_singular_and_named_map_examples();
3089        // Flag on: examples folded into the description, omitted from the structured field.
3090        let (result, _annotations) =
3091            ToolGenerator::convert_parameter_schema(&param, ParameterIn::Query, &spec, false, true)
3092                .expect("conversion succeeds");
3093        let description = result["description"].as_str().unwrap_or_default();
3094        assert!(
3095            description.contains("alpha")
3096                && description.contains("beta")
3097                && description.contains("gamma"),
3098            "examples folded into description: {description}"
3099        );
3100        assert!(
3101            result.get("examples").is_none(),
3102            "structured `examples` omitted when folding into the description: {result}"
3103        );
3104    }
3105
3106    #[test]
3107    fn array_parameter_lifts_item_examples_to_parameter_level() {
3108        let spec = create_test_spec();
3109        // An array parameter whose element schema carries `examples` (the natural place for
3110        // per-element examples). MCP clients and the inspector read examples at the parameter
3111        // level, not nested under `items`, so they must be surfaced there.
3112        let param: Parameter = serde_json::from_value(json!({
3113            "name": "include",
3114            "in": "query",
3115            "schema": {
3116                "type": "array",
3117                "items": { "type": "string", "examples": ["camera", "mesh.primitives"] },
3118            },
3119        }))
3120        .expect("valid parameter");
3121        let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3122            &param,
3123            ParameterIn::Query,
3124            &spec,
3125            false,
3126            false,
3127        )
3128        .expect("conversion succeeds");
3129        // Each element example is lifted to the parameter level wrapped as a single-element
3130        // array, since a valid value for an array parameter is itself an array.
3131        assert_eq!(
3132            result["examples"],
3133            json!([["camera"], ["mesh.primitives"]]),
3134            "array element examples must be lifted to parameter-level examples: {result}"
3135        );
3136    }
3137
3138    #[test]
3139    fn lifting_array_item_examples_clears_them_from_items() {
3140        let spec = create_test_spec();
3141        let param: Parameter = serde_json::from_value(json!({
3142            "name": "include",
3143            "in": "query",
3144            "schema": {
3145                "type": "array",
3146                "items": { "type": "string", "examples": ["camera", "mesh.primitives"] },
3147            },
3148        }))
3149        .expect("valid parameter");
3150        let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3151            &param,
3152            ParameterIn::Query,
3153            &spec,
3154            false,
3155            false,
3156        )
3157        .expect("conversion succeeds");
3158        // Once lifted to the parameter level, the examples must not also remain under `items`:
3159        // a single channel, so their tokens are not duplicated.
3160        assert!(
3161            result["items"].get("examples").is_none(),
3162            "item-level examples must be cleared once lifted to the parameter level: {result}"
3163        );
3164    }
3165
3166    #[test]
3167    fn array_parameter_examples_lift_snapshot() {
3168        let spec = create_test_spec();
3169        // Mirrors a JSON:API `include` parameter: an array of relationship-path strings whose
3170        // element schema carries representative `examples` and a grammar description. The snapshot
3171        // renders the converted MCP input schema so the lifted, parameter-level shape is reviewable.
3172        let param: Parameter = serde_json::from_value(json!({
3173            "name": "include",
3174            "in": "query",
3175            "description": "Relationship paths to include.",
3176            "schema": {
3177                "type": "array",
3178                "items": {
3179                    "type": "string",
3180                    "description": "A relationship path: a dot-separated chain of relationship names.",
3181                    "examples": ["camera", "mesh.primitives.material", "mesh.primitives.indices"],
3182                },
3183            },
3184        }))
3185        .expect("valid parameter");
3186        let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3187            &param,
3188            ParameterIn::Query,
3189            &spec,
3190            false,
3191            false,
3192        )
3193        .expect("conversion succeeds");
3194        assert_json_snapshot!("array_parameter_examples_lift", result);
3195    }
3196
3197    /// Create a minimal test OpenAPI spec for testing purposes
3198    fn create_test_spec() -> Spec {
3199        Spec {
3200            openapi: "3.0.0".to_string(),
3201            info: oas3::spec::Info {
3202                title: "Test API".to_string(),
3203                version: "1.0.0".to_string(),
3204                summary: None,
3205                description: Some("Test API for unit tests".to_string()),
3206                terms_of_service: None,
3207                contact: None,
3208                license: None,
3209                extensions: Default::default(),
3210            },
3211            components: Some(Components {
3212                schemas: BTreeMap::new(),
3213                responses: BTreeMap::new(),
3214                parameters: BTreeMap::new(),
3215                examples: BTreeMap::new(),
3216                request_bodies: BTreeMap::new(),
3217                headers: BTreeMap::new(),
3218                security_schemes: BTreeMap::new(),
3219                links: BTreeMap::new(),
3220                callbacks: BTreeMap::new(),
3221                path_items: BTreeMap::new(),
3222                extensions: Default::default(),
3223            }),
3224            servers: vec![],
3225            paths: None,
3226            external_docs: None,
3227            tags: vec![],
3228            security: vec![],
3229            webhooks: BTreeMap::new(),
3230            extensions: Default::default(),
3231        }
3232    }
3233
3234    fn validate_tool_against_mcp_schema(metadata: &ToolMetadata) {
3235        let schema_content = std::fs::read_to_string("schema/2025-06-18/schema.json")
3236            .expect("Failed to read MCP schema file");
3237        let full_schema: Value =
3238            serde_json::from_str(&schema_content).expect("Failed to parse MCP schema JSON");
3239
3240        // Create a schema that references the Tool definition from the full schema
3241        let tool_schema = json!({
3242            "$schema": "http://json-schema.org/draft-07/schema#",
3243            "definitions": full_schema.get("definitions"),
3244            "$ref": "#/definitions/Tool"
3245        });
3246
3247        let validator =
3248            jsonschema::validator_for(&tool_schema).expect("Failed to compile MCP Tool schema");
3249
3250        // Convert ToolMetadata to MCP Tool format using the From trait
3251        let tool = Tool::from(metadata);
3252
3253        // Serialize the Tool to JSON for validation
3254        let mcp_tool_json = serde_json::to_value(&tool).expect("Failed to serialize Tool to JSON");
3255
3256        // Validate the generated tool against MCP schema
3257        let errors: Vec<String> = validator
3258            .iter_errors(&mcp_tool_json)
3259            .map(|e| e.to_string())
3260            .collect();
3261
3262        if !errors.is_empty() {
3263            panic!("Generated tool failed MCP schema validation: {errors:?}");
3264        }
3265    }
3266
3267    #[test]
3268    fn test_error_schema_structure() {
3269        let error_schema = create_error_response_schema();
3270
3271        // Should not contain $schema or definitions at top level
3272        assert!(error_schema.get("$schema").is_none());
3273        assert!(error_schema.get("definitions").is_none());
3274
3275        // Verify the structure using snapshot
3276        assert_json_snapshot!(error_schema);
3277    }
3278
3279    #[test]
3280    fn test_petstore_get_pet_by_id() {
3281        use oas3::spec::Response;
3282
3283        let mut operation = Operation {
3284            operation_id: Some("getPetById".to_string()),
3285            summary: Some("Find pet by ID".to_string()),
3286            description: Some("Returns a single pet".to_string()),
3287            tags: vec![],
3288            external_docs: None,
3289            parameters: vec![],
3290            request_body: None,
3291            responses: Default::default(),
3292            callbacks: Default::default(),
3293            deprecated: Some(false),
3294            security: vec![],
3295            servers: vec![],
3296            extensions: Default::default(),
3297        };
3298
3299        // Create a path parameter
3300        let param = Parameter {
3301            name: "petId".to_string(),
3302            location: ParameterIn::Path,
3303            description: Some("ID of pet to return".to_string()),
3304            required: Some(true),
3305            deprecated: Some(false),
3306            allow_empty_value: Some(false),
3307            style: None,
3308            explode: None,
3309            allow_reserved: Some(false),
3310            schema: Some(ObjectOrReference::Object(ObjectSchema {
3311                schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3312                minimum: Some(serde_json::Number::from(1_i64)),
3313                format: Some("int64".to_string()),
3314                ..Default::default()
3315            })),
3316            example: None,
3317            examples: Default::default(),
3318            content: None,
3319            extensions: Default::default(),
3320        };
3321
3322        operation.parameters.push(ObjectOrReference::Object(param));
3323
3324        // Add a 200 response with Pet schema
3325        let mut responses = BTreeMap::new();
3326        let mut content = BTreeMap::new();
3327        content.insert(
3328            "application/json".to_string(),
3329            MediaType {
3330                extensions: Default::default(),
3331                schema: Some(ObjectOrReference::Object(ObjectSchema {
3332                    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3333                    properties: {
3334                        let mut props = BTreeMap::new();
3335                        props.insert(
3336                            "id".to_string(),
3337                            ObjectOrReference::Object(ObjectSchema {
3338                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3339                                format: Some("int64".to_string()),
3340                                ..Default::default()
3341                            }),
3342                        );
3343                        props.insert(
3344                            "name".to_string(),
3345                            ObjectOrReference::Object(ObjectSchema {
3346                                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3347                                ..Default::default()
3348                            }),
3349                        );
3350                        props.insert(
3351                            "status".to_string(),
3352                            ObjectOrReference::Object(ObjectSchema {
3353                                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3354                                ..Default::default()
3355                            }),
3356                        );
3357                        props
3358                    },
3359                    required: vec!["id".to_string(), "name".to_string()],
3360                    ..Default::default()
3361                })),
3362                examples: None,
3363                encoding: Default::default(),
3364            },
3365        );
3366
3367        responses.insert(
3368            "200".to_string(),
3369            ObjectOrReference::Object(Response {
3370                description: Some("successful operation".to_string()),
3371                headers: Default::default(),
3372                content,
3373                links: Default::default(),
3374                extensions: Default::default(),
3375            }),
3376        );
3377        operation.responses = Some(responses);
3378
3379        let spec = create_test_spec();
3380        let metadata = ToolGenerator::generate_tool_metadata(
3381            &operation,
3382            "get".to_string(),
3383            "/pet/{petId}".to_string(),
3384            &spec,
3385            false,
3386            false,
3387            false,
3388        )
3389        .unwrap();
3390
3391        assert_eq!(metadata.name, "getPetById");
3392        assert_eq!(metadata.method, "get");
3393        assert_eq!(metadata.path, "/pet/{petId}");
3394        assert!(
3395            metadata
3396                .description
3397                .clone()
3398                .unwrap()
3399                .contains("Find pet by ID")
3400        );
3401
3402        // Check output_schema is included and correct
3403        assert!(metadata.output_schema.is_some());
3404        let output_schema = metadata.output_schema.as_ref().unwrap();
3405
3406        // Use snapshot testing for the output schema
3407        insta::assert_json_snapshot!("test_petstore_get_pet_by_id_output_schema", output_schema);
3408
3409        // Validate against MCP Tool schema
3410        validate_tool_against_mcp_schema(&metadata);
3411    }
3412
3413    #[test]
3414    fn test_convert_prefix_items_to_draft07_mixed_types() {
3415        // Test prefixItems with mixed types and items:false
3416
3417        let prefix_items = vec![
3418            ObjectOrReference::Object(ObjectSchema {
3419                schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3420                format: Some("int32".to_string()),
3421                ..Default::default()
3422            }),
3423            ObjectOrReference::Object(ObjectSchema {
3424                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3425                ..Default::default()
3426            }),
3427        ];
3428
3429        // items: false (no additional items allowed)
3430        let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
3431
3432        let mut result = serde_json::Map::new();
3433        let spec = create_test_spec();
3434        ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
3435            .unwrap();
3436
3437        // Use JSON snapshot for the schema
3438        insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_mixed_types", result);
3439    }
3440
3441    #[test]
3442    fn test_convert_prefix_items_to_draft07_uniform_types() {
3443        // Test prefixItems with uniform types
3444        let prefix_items = vec![
3445            ObjectOrReference::Object(ObjectSchema {
3446                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3447                ..Default::default()
3448            }),
3449            ObjectOrReference::Object(ObjectSchema {
3450                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3451                ..Default::default()
3452            }),
3453        ];
3454
3455        // items: false
3456        let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
3457
3458        let mut result = serde_json::Map::new();
3459        let spec = create_test_spec();
3460        ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
3461            .unwrap();
3462
3463        // Use JSON snapshot for the schema
3464        insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_uniform_types", result);
3465    }
3466
3467    #[test]
3468    fn test_array_with_prefix_items_integration() {
3469        // Integration test: parameter with prefixItems and items:false
3470        let param = Parameter {
3471            name: "coordinates".to_string(),
3472            location: ParameterIn::Query,
3473            description: Some("X,Y coordinates as tuple".to_string()),
3474            required: Some(true),
3475            deprecated: Some(false),
3476            allow_empty_value: Some(false),
3477            style: None,
3478            explode: None,
3479            allow_reserved: Some(false),
3480            schema: Some(ObjectOrReference::Object(ObjectSchema {
3481                schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3482                prefix_items: vec![
3483                    ObjectOrReference::Object(ObjectSchema {
3484                        schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
3485                        format: Some("double".to_string()),
3486                        ..Default::default()
3487                    }),
3488                    ObjectOrReference::Object(ObjectSchema {
3489                        schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
3490                        format: Some("double".to_string()),
3491                        ..Default::default()
3492                    }),
3493                ],
3494                items: Some(Box::new(Schema::Boolean(BooleanSchema(false)))),
3495                ..Default::default()
3496            })),
3497            example: None,
3498            examples: Default::default(),
3499            content: None,
3500            extensions: Default::default(),
3501        };
3502
3503        let spec = create_test_spec();
3504        let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3505            &param,
3506            ParameterIn::Query,
3507            &spec,
3508            false,
3509            false,
3510        )
3511        .unwrap();
3512
3513        // Use JSON snapshot for the schema
3514        insta::assert_json_snapshot!("test_array_with_prefix_items_integration", result);
3515    }
3516
3517    #[test]
3518    fn test_skip_tool_description() {
3519        let operation = Operation {
3520            operation_id: Some("getPetById".to_string()),
3521            summary: Some("Find pet by ID".to_string()),
3522            description: Some("Returns a single pet".to_string()),
3523            tags: vec![],
3524            external_docs: None,
3525            parameters: vec![],
3526            request_body: None,
3527            responses: Default::default(),
3528            callbacks: Default::default(),
3529            deprecated: Some(false),
3530            security: vec![],
3531            servers: vec![],
3532            extensions: Default::default(),
3533        };
3534
3535        let spec = create_test_spec();
3536        let metadata = ToolGenerator::generate_tool_metadata(
3537            &operation,
3538            "get".to_string(),
3539            "/pet/{petId}".to_string(),
3540            &spec,
3541            true,
3542            false,
3543            false,
3544        )
3545        .unwrap();
3546
3547        assert_eq!(metadata.name, "getPetById");
3548        assert_eq!(metadata.method, "get");
3549        assert_eq!(metadata.path, "/pet/{petId}");
3550        assert!(metadata.description.is_none());
3551
3552        // Use snapshot testing for the output schema
3553        insta::assert_json_snapshot!("test_skip_tool_description", metadata);
3554
3555        // Validate against MCP Tool schema
3556        validate_tool_against_mcp_schema(&metadata);
3557    }
3558
3559    #[test]
3560    fn test_keep_tool_description() {
3561        let description = Some("Returns a single pet".to_string());
3562        let operation = Operation {
3563            operation_id: Some("getPetById".to_string()),
3564            summary: Some("Find pet by ID".to_string()),
3565            description: description.clone(),
3566            tags: vec![],
3567            external_docs: None,
3568            parameters: vec![],
3569            request_body: None,
3570            responses: Default::default(),
3571            callbacks: Default::default(),
3572            deprecated: Some(false),
3573            security: vec![],
3574            servers: vec![],
3575            extensions: Default::default(),
3576        };
3577
3578        let spec = create_test_spec();
3579        let metadata = ToolGenerator::generate_tool_metadata(
3580            &operation,
3581            "get".to_string(),
3582            "/pet/{petId}".to_string(),
3583            &spec,
3584            false,
3585            false,
3586            false,
3587        )
3588        .unwrap();
3589
3590        assert_eq!(metadata.name, "getPetById");
3591        assert_eq!(metadata.method, "get");
3592        assert_eq!(metadata.path, "/pet/{petId}");
3593        assert!(metadata.description.is_some());
3594
3595        // Use snapshot testing for the output schema
3596        insta::assert_json_snapshot!("test_keep_tool_description", metadata);
3597
3598        // Validate against MCP Tool schema
3599        validate_tool_against_mcp_schema(&metadata);
3600    }
3601
3602    #[test]
3603    fn test_skip_parameter_descriptions() {
3604        let param = Parameter {
3605            name: "status".to_string(),
3606            location: ParameterIn::Query,
3607            description: Some("Filter by status".to_string()),
3608            required: Some(false),
3609            deprecated: Some(false),
3610            allow_empty_value: Some(false),
3611            style: None,
3612            explode: None,
3613            allow_reserved: Some(false),
3614            schema: Some(ObjectOrReference::Object(ObjectSchema {
3615                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3616                enum_values: vec![json!("available"), json!("pending"), json!("sold")],
3617                ..Default::default()
3618            })),
3619            example: Some(json!("available")),
3620            examples: Default::default(),
3621            content: None,
3622            extensions: Default::default(),
3623        };
3624
3625        let spec = create_test_spec();
3626        let (schema, _) =
3627            ToolGenerator::convert_parameter_schema(&param, ParameterIn::Query, &spec, true, false)
3628                .unwrap();
3629
3630        // When skip_parameter_descriptions is true, description should not be present
3631        assert!(schema.get("description").is_none());
3632
3633        // Other properties should still be present; the example surfaces in the structured
3634        // `examples` field by default (not the deprecated singular `example`).
3635        assert_eq!(schema.get("type").unwrap(), "string");
3636        assert!(schema.get("example").is_none());
3637        assert_eq!(schema.get("examples").unwrap(), &json!(["available"]));
3638
3639        insta::assert_json_snapshot!("test_skip_parameter_descriptions", schema);
3640    }
3641
3642    #[test]
3643    fn test_keep_parameter_descriptions() {
3644        let param = Parameter {
3645            name: "status".to_string(),
3646            location: ParameterIn::Query,
3647            description: Some("Filter by status".to_string()),
3648            required: Some(false),
3649            deprecated: Some(false),
3650            allow_empty_value: Some(false),
3651            style: None,
3652            explode: None,
3653            allow_reserved: Some(false),
3654            schema: Some(ObjectOrReference::Object(ObjectSchema {
3655                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3656                enum_values: vec![json!("available"), json!("pending"), json!("sold")],
3657                ..Default::default()
3658            })),
3659            example: Some(json!("available")),
3660            examples: Default::default(),
3661            content: None,
3662            extensions: Default::default(),
3663        };
3664
3665        let spec = create_test_spec();
3666        let (schema, _) = ToolGenerator::convert_parameter_schema(
3667            &param,
3668            ParameterIn::Query,
3669            &spec,
3670            false,
3671            false,
3672        )
3673        .unwrap();
3674
3675        // When skip_parameter_descriptions is false, description should be present — but by
3676        // default it carries no examples (those go to the structured `examples` field).
3677        assert!(schema.get("description").is_some());
3678        let description = schema.get("description").unwrap().as_str().unwrap();
3679        assert!(description.contains("Filter by status"));
3680        assert!(!description.contains("Example:"));
3681
3682        // Other properties should also be present; the example is structured.
3683        assert_eq!(schema.get("type").unwrap(), "string");
3684        assert!(schema.get("example").is_none());
3685        assert_eq!(schema.get("examples").unwrap(), &json!(["available"]));
3686
3687        insta::assert_json_snapshot!("test_keep_parameter_descriptions", schema);
3688    }
3689
3690    #[test]
3691    fn test_array_with_regular_items_schema() {
3692        // Test regular array with object schema items (not boolean)
3693        let param = Parameter {
3694            name: "tags".to_string(),
3695            location: ParameterIn::Query,
3696            description: Some("List of tags".to_string()),
3697            required: Some(false),
3698            deprecated: Some(false),
3699            allow_empty_value: Some(false),
3700            style: None,
3701            explode: None,
3702            allow_reserved: Some(false),
3703            schema: Some(ObjectOrReference::Object(ObjectSchema {
3704                schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3705                items: Some(Box::new(Schema::Object(Box::new(
3706                    ObjectOrReference::Object(ObjectSchema {
3707                        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3708                        min_length: Some(1),
3709                        max_length: Some(50),
3710                        ..Default::default()
3711                    }),
3712                )))),
3713                ..Default::default()
3714            })),
3715            example: None,
3716            examples: Default::default(),
3717            content: None,
3718            extensions: Default::default(),
3719        };
3720
3721        let spec = create_test_spec();
3722        let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3723            &param,
3724            ParameterIn::Query,
3725            &spec,
3726            false,
3727            false,
3728        )
3729        .unwrap();
3730
3731        // Use JSON snapshot for the schema
3732        insta::assert_json_snapshot!("test_array_with_regular_items_schema", result);
3733    }
3734
3735    #[test]
3736    fn test_request_body_object_schema() {
3737        // Test with object request body
3738        let operation = Operation {
3739            operation_id: Some("createPet".to_string()),
3740            summary: Some("Create a new pet".to_string()),
3741            description: Some("Creates a new pet in the store".to_string()),
3742            tags: vec![],
3743            external_docs: None,
3744            parameters: vec![],
3745            request_body: Some(ObjectOrReference::Object(RequestBody {
3746                description: Some("Pet object that needs to be added to the store".to_string()),
3747                content: {
3748                    let mut content = BTreeMap::new();
3749                    content.insert(
3750                        "application/json".to_string(),
3751                        MediaType {
3752                            extensions: Default::default(),
3753                            schema: Some(ObjectOrReference::Object(ObjectSchema {
3754                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3755                                ..Default::default()
3756                            })),
3757                            examples: None,
3758                            encoding: Default::default(),
3759                        },
3760                    );
3761                    content
3762                },
3763                required: Some(true),
3764            })),
3765            responses: Default::default(),
3766            callbacks: Default::default(),
3767            deprecated: Some(false),
3768            security: vec![],
3769            servers: vec![],
3770            extensions: Default::default(),
3771        };
3772
3773        let spec = create_test_spec();
3774        let metadata = ToolGenerator::generate_tool_metadata(
3775            &operation,
3776            "post".to_string(),
3777            "/pets".to_string(),
3778            &spec,
3779            false,
3780            false,
3781            false,
3782        )
3783        .unwrap();
3784
3785        // Check that request_body is in properties
3786        let properties = metadata
3787            .parameters
3788            .get("properties")
3789            .unwrap()
3790            .as_object()
3791            .unwrap();
3792        assert!(properties.contains_key("request_body"));
3793
3794        // Check that request_body is required
3795        let required = metadata
3796            .parameters
3797            .get("required")
3798            .unwrap()
3799            .as_array()
3800            .unwrap();
3801        assert!(required.contains(&json!("request_body")));
3802
3803        // Check request body schema using snapshot
3804        let request_body_schema = properties.get("request_body").unwrap();
3805        insta::assert_json_snapshot!("test_request_body_object_schema", request_body_schema);
3806
3807        // Validate against MCP Tool schema
3808        validate_tool_against_mcp_schema(&metadata);
3809    }
3810
3811    #[test]
3812    fn test_request_body_array_schema() {
3813        // Test with array request body
3814        let operation = Operation {
3815            operation_id: Some("createPets".to_string()),
3816            summary: Some("Create multiple pets".to_string()),
3817            description: None,
3818            tags: vec![],
3819            external_docs: None,
3820            parameters: vec![],
3821            request_body: Some(ObjectOrReference::Object(RequestBody {
3822                description: Some("Array of pet objects".to_string()),
3823                content: {
3824                    let mut content = BTreeMap::new();
3825                    content.insert(
3826                        "application/json".to_string(),
3827                        MediaType {
3828                            extensions: Default::default(),
3829                            schema: Some(ObjectOrReference::Object(ObjectSchema {
3830                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3831                                items: Some(Box::new(Schema::Object(Box::new(
3832                                    ObjectOrReference::Object(ObjectSchema {
3833                                        schema_type: Some(SchemaTypeSet::Single(
3834                                            SchemaType::Object,
3835                                        )),
3836                                        ..Default::default()
3837                                    }),
3838                                )))),
3839                                ..Default::default()
3840                            })),
3841                            examples: None,
3842                            encoding: Default::default(),
3843                        },
3844                    );
3845                    content
3846                },
3847                required: Some(false),
3848            })),
3849            responses: Default::default(),
3850            callbacks: Default::default(),
3851            deprecated: Some(false),
3852            security: vec![],
3853            servers: vec![],
3854            extensions: Default::default(),
3855        };
3856
3857        let spec = create_test_spec();
3858        let metadata = ToolGenerator::generate_tool_metadata(
3859            &operation,
3860            "post".to_string(),
3861            "/pets/batch".to_string(),
3862            &spec,
3863            false,
3864            false,
3865            false,
3866        )
3867        .unwrap();
3868
3869        // Check that request_body is in properties
3870        let properties = metadata
3871            .parameters
3872            .get("properties")
3873            .unwrap()
3874            .as_object()
3875            .unwrap();
3876        assert!(properties.contains_key("request_body"));
3877
3878        // Check that request_body is NOT required (required: false)
3879        let required = metadata
3880            .parameters
3881            .get("required")
3882            .unwrap()
3883            .as_array()
3884            .unwrap();
3885        assert!(!required.contains(&json!("request_body")));
3886
3887        // Check request body schema using snapshot
3888        let request_body_schema = properties.get("request_body").unwrap();
3889        insta::assert_json_snapshot!("test_request_body_array_schema", request_body_schema);
3890
3891        // Validate against MCP Tool schema
3892        validate_tool_against_mcp_schema(&metadata);
3893    }
3894
3895    #[test]
3896    fn test_request_body_string_schema() {
3897        // Test with string request body
3898        let operation = Operation {
3899            operation_id: Some("updatePetName".to_string()),
3900            summary: Some("Update pet name".to_string()),
3901            description: None,
3902            tags: vec![],
3903            external_docs: None,
3904            parameters: vec![],
3905            request_body: Some(ObjectOrReference::Object(RequestBody {
3906                description: None,
3907                content: {
3908                    let mut content = BTreeMap::new();
3909                    content.insert(
3910                        "text/plain".to_string(),
3911                        MediaType {
3912                            extensions: Default::default(),
3913                            schema: Some(ObjectOrReference::Object(ObjectSchema {
3914                                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3915                                min_length: Some(1),
3916                                max_length: Some(100),
3917                                ..Default::default()
3918                            })),
3919                            examples: None,
3920                            encoding: Default::default(),
3921                        },
3922                    );
3923                    content
3924                },
3925                required: Some(true),
3926            })),
3927            responses: Default::default(),
3928            callbacks: Default::default(),
3929            deprecated: Some(false),
3930            security: vec![],
3931            servers: vec![],
3932            extensions: Default::default(),
3933        };
3934
3935        let spec = create_test_spec();
3936        let metadata = ToolGenerator::generate_tool_metadata(
3937            &operation,
3938            "put".to_string(),
3939            "/pets/{petId}/name".to_string(),
3940            &spec,
3941            false,
3942            false,
3943            false,
3944        )
3945        .unwrap();
3946
3947        // Check request body schema
3948        let properties = metadata
3949            .parameters
3950            .get("properties")
3951            .unwrap()
3952            .as_object()
3953            .unwrap();
3954        let request_body_schema = properties.get("request_body").unwrap();
3955        insta::assert_json_snapshot!("test_request_body_string_schema", request_body_schema);
3956
3957        // Validate against MCP Tool schema
3958        validate_tool_against_mcp_schema(&metadata);
3959    }
3960
3961    #[test]
3962    fn test_request_body_ref_schema() {
3963        // Test with reference request body
3964        let operation = Operation {
3965            operation_id: Some("updatePet".to_string()),
3966            summary: Some("Update existing pet".to_string()),
3967            description: None,
3968            tags: vec![],
3969            external_docs: None,
3970            parameters: vec![],
3971            request_body: Some(ObjectOrReference::Ref {
3972                ref_path: "#/components/requestBodies/PetBody".to_string(),
3973                summary: None,
3974                description: None,
3975            }),
3976            responses: Default::default(),
3977            callbacks: Default::default(),
3978            deprecated: Some(false),
3979            security: vec![],
3980            servers: vec![],
3981            extensions: Default::default(),
3982        };
3983
3984        let spec = create_test_spec();
3985        let metadata = ToolGenerator::generate_tool_metadata(
3986            &operation,
3987            "put".to_string(),
3988            "/pets/{petId}".to_string(),
3989            &spec,
3990            false,
3991            false,
3992            false,
3993        )
3994        .unwrap();
3995
3996        // Check that request_body uses generic object schema for refs
3997        let properties = metadata
3998            .parameters
3999            .get("properties")
4000            .unwrap()
4001            .as_object()
4002            .unwrap();
4003        let request_body_schema = properties.get("request_body").unwrap();
4004        insta::assert_json_snapshot!("test_request_body_ref_schema", request_body_schema);
4005
4006        // Validate against MCP Tool schema
4007        validate_tool_against_mcp_schema(&metadata);
4008    }
4009
4010    #[test]
4011    fn test_no_request_body_for_get() {
4012        // Test that GET operations don't get request body by default
4013        let operation = Operation {
4014            operation_id: Some("listPets".to_string()),
4015            summary: Some("List all pets".to_string()),
4016            description: None,
4017            tags: vec![],
4018            external_docs: None,
4019            parameters: vec![],
4020            request_body: None,
4021            responses: Default::default(),
4022            callbacks: Default::default(),
4023            deprecated: Some(false),
4024            security: vec![],
4025            servers: vec![],
4026            extensions: Default::default(),
4027        };
4028
4029        let spec = create_test_spec();
4030        let metadata = ToolGenerator::generate_tool_metadata(
4031            &operation,
4032            "get".to_string(),
4033            "/pets".to_string(),
4034            &spec,
4035            false,
4036            false,
4037            false,
4038        )
4039        .unwrap();
4040
4041        // Check that request_body is NOT in properties
4042        let properties = metadata
4043            .parameters
4044            .get("properties")
4045            .unwrap()
4046            .as_object()
4047            .unwrap();
4048        assert!(!properties.contains_key("request_body"));
4049
4050        // Validate against MCP Tool schema
4051        validate_tool_against_mcp_schema(&metadata);
4052    }
4053
4054    #[test]
4055    fn test_request_body_simple_object_with_properties() {
4056        // Test with simple object schema with a few properties
4057        let operation = Operation {
4058            operation_id: Some("updatePetStatus".to_string()),
4059            summary: Some("Update pet status".to_string()),
4060            description: None,
4061            tags: vec![],
4062            external_docs: None,
4063            parameters: vec![],
4064            request_body: Some(ObjectOrReference::Object(RequestBody {
4065                description: Some("Pet status update".to_string()),
4066                content: {
4067                    let mut content = BTreeMap::new();
4068                    content.insert(
4069                        "application/json".to_string(),
4070                        MediaType {
4071                            extensions: Default::default(),
4072                            schema: Some(ObjectOrReference::Object(ObjectSchema {
4073                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4074                                properties: {
4075                                    let mut props = BTreeMap::new();
4076                                    props.insert(
4077                                        "status".to_string(),
4078                                        ObjectOrReference::Object(ObjectSchema {
4079                                            schema_type: Some(SchemaTypeSet::Single(
4080                                                SchemaType::String,
4081                                            )),
4082                                            ..Default::default()
4083                                        }),
4084                                    );
4085                                    props.insert(
4086                                        "reason".to_string(),
4087                                        ObjectOrReference::Object(ObjectSchema {
4088                                            schema_type: Some(SchemaTypeSet::Single(
4089                                                SchemaType::String,
4090                                            )),
4091                                            ..Default::default()
4092                                        }),
4093                                    );
4094                                    props
4095                                },
4096                                required: vec!["status".to_string()],
4097                                ..Default::default()
4098                            })),
4099                            examples: None,
4100                            encoding: Default::default(),
4101                        },
4102                    );
4103                    content
4104                },
4105                required: Some(false),
4106            })),
4107            responses: Default::default(),
4108            callbacks: Default::default(),
4109            deprecated: Some(false),
4110            security: vec![],
4111            servers: vec![],
4112            extensions: Default::default(),
4113        };
4114
4115        let spec = create_test_spec();
4116        let metadata = ToolGenerator::generate_tool_metadata(
4117            &operation,
4118            "patch".to_string(),
4119            "/pets/{petId}/status".to_string(),
4120            &spec,
4121            false,
4122            false,
4123            false,
4124        )
4125        .unwrap();
4126
4127        // Check request body schema - should have actual properties
4128        let properties = metadata
4129            .parameters
4130            .get("properties")
4131            .unwrap()
4132            .as_object()
4133            .unwrap();
4134        let request_body_schema = properties.get("request_body").unwrap();
4135        insta::assert_json_snapshot!(
4136            "test_request_body_simple_object_with_properties",
4137            request_body_schema
4138        );
4139
4140        // Should not be in top-level required since request body itself is optional
4141        let required = metadata
4142            .parameters
4143            .get("required")
4144            .unwrap()
4145            .as_array()
4146            .unwrap();
4147        assert!(!required.contains(&json!("request_body")));
4148
4149        // Validate against MCP Tool schema
4150        validate_tool_against_mcp_schema(&metadata);
4151    }
4152
4153    #[test]
4154    fn test_request_body_with_nested_properties() {
4155        // Test with complex nested object schema
4156        let operation = Operation {
4157            operation_id: Some("createUser".to_string()),
4158            summary: Some("Create a new user".to_string()),
4159            description: None,
4160            tags: vec![],
4161            external_docs: None,
4162            parameters: vec![],
4163            request_body: Some(ObjectOrReference::Object(RequestBody {
4164                description: Some("User creation data".to_string()),
4165                content: {
4166                    let mut content = BTreeMap::new();
4167                    content.insert(
4168                        "application/json".to_string(),
4169                        MediaType {
4170                            extensions: Default::default(),
4171                            schema: Some(ObjectOrReference::Object(ObjectSchema {
4172                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4173                                properties: {
4174                                    let mut props = BTreeMap::new();
4175                                    props.insert(
4176                                        "name".to_string(),
4177                                        ObjectOrReference::Object(ObjectSchema {
4178                                            schema_type: Some(SchemaTypeSet::Single(
4179                                                SchemaType::String,
4180                                            )),
4181                                            ..Default::default()
4182                                        }),
4183                                    );
4184                                    props.insert(
4185                                        "age".to_string(),
4186                                        ObjectOrReference::Object(ObjectSchema {
4187                                            schema_type: Some(SchemaTypeSet::Single(
4188                                                SchemaType::Integer,
4189                                            )),
4190                                            minimum: Some(serde_json::Number::from(0)),
4191                                            maximum: Some(serde_json::Number::from(150)),
4192                                            ..Default::default()
4193                                        }),
4194                                    );
4195                                    props
4196                                },
4197                                required: vec!["name".to_string()],
4198                                ..Default::default()
4199                            })),
4200                            examples: None,
4201                            encoding: Default::default(),
4202                        },
4203                    );
4204                    content
4205                },
4206                required: Some(true),
4207            })),
4208            responses: Default::default(),
4209            callbacks: Default::default(),
4210            deprecated: Some(false),
4211            security: vec![],
4212            servers: vec![],
4213            extensions: Default::default(),
4214        };
4215
4216        let spec = create_test_spec();
4217        let metadata = ToolGenerator::generate_tool_metadata(
4218            &operation,
4219            "post".to_string(),
4220            "/users".to_string(),
4221            &spec,
4222            false,
4223            false,
4224            false,
4225        )
4226        .unwrap();
4227
4228        // Check request body schema
4229        let properties = metadata
4230            .parameters
4231            .get("properties")
4232            .unwrap()
4233            .as_object()
4234            .unwrap();
4235        let request_body_schema = properties.get("request_body").unwrap();
4236        insta::assert_json_snapshot!(
4237            "test_request_body_with_nested_properties",
4238            request_body_schema
4239        );
4240
4241        // Validate against MCP Tool schema
4242        validate_tool_against_mcp_schema(&metadata);
4243    }
4244
4245    #[test]
4246    fn test_operation_without_responses_has_no_output_schema() {
4247        let operation = Operation {
4248            operation_id: Some("testOperation".to_string()),
4249            summary: Some("Test operation".to_string()),
4250            description: None,
4251            tags: vec![],
4252            external_docs: None,
4253            parameters: vec![],
4254            request_body: None,
4255            responses: None,
4256            callbacks: Default::default(),
4257            deprecated: Some(false),
4258            security: vec![],
4259            servers: vec![],
4260            extensions: Default::default(),
4261        };
4262
4263        let spec = create_test_spec();
4264        let metadata = ToolGenerator::generate_tool_metadata(
4265            &operation,
4266            "get".to_string(),
4267            "/test".to_string(),
4268            &spec,
4269            false,
4270            false,
4271            false,
4272        )
4273        .unwrap();
4274
4275        // When no responses are defined, output_schema should be None
4276        assert!(metadata.output_schema.is_none());
4277
4278        // Validate against MCP Tool schema
4279        validate_tool_against_mcp_schema(&metadata);
4280    }
4281
4282    #[test]
4283    fn test_extract_output_schema_with_200_response() {
4284        use oas3::spec::Response;
4285
4286        // Create a 200 response with schema
4287        let mut responses = BTreeMap::new();
4288        let mut content = BTreeMap::new();
4289        content.insert(
4290            "application/json".to_string(),
4291            MediaType {
4292                extensions: Default::default(),
4293                schema: Some(ObjectOrReference::Object(ObjectSchema {
4294                    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4295                    properties: {
4296                        let mut props = BTreeMap::new();
4297                        props.insert(
4298                            "id".to_string(),
4299                            ObjectOrReference::Object(ObjectSchema {
4300                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4301                                ..Default::default()
4302                            }),
4303                        );
4304                        props.insert(
4305                            "name".to_string(),
4306                            ObjectOrReference::Object(ObjectSchema {
4307                                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4308                                ..Default::default()
4309                            }),
4310                        );
4311                        props
4312                    },
4313                    required: vec!["id".to_string(), "name".to_string()],
4314                    ..Default::default()
4315                })),
4316                examples: None,
4317                encoding: Default::default(),
4318            },
4319        );
4320
4321        responses.insert(
4322            "200".to_string(),
4323            ObjectOrReference::Object(Response {
4324                description: Some("Successful response".to_string()),
4325                headers: Default::default(),
4326                content,
4327                links: Default::default(),
4328                extensions: Default::default(),
4329            }),
4330        );
4331
4332        let spec = create_test_spec();
4333        let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4334
4335        // Result is already a JSON Value
4336        insta::assert_json_snapshot!(result);
4337    }
4338
4339    #[test]
4340    fn test_extract_output_schema_with_201_response() {
4341        use oas3::spec::Response;
4342
4343        // Create only a 201 response (no 200)
4344        let mut responses = BTreeMap::new();
4345        let mut content = BTreeMap::new();
4346        content.insert(
4347            "application/json".to_string(),
4348            MediaType {
4349                extensions: Default::default(),
4350                schema: Some(ObjectOrReference::Object(ObjectSchema {
4351                    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4352                    properties: {
4353                        let mut props = BTreeMap::new();
4354                        props.insert(
4355                            "created".to_string(),
4356                            ObjectOrReference::Object(ObjectSchema {
4357                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Boolean)),
4358                                ..Default::default()
4359                            }),
4360                        );
4361                        props
4362                    },
4363                    ..Default::default()
4364                })),
4365                examples: None,
4366                encoding: Default::default(),
4367            },
4368        );
4369
4370        responses.insert(
4371            "201".to_string(),
4372            ObjectOrReference::Object(Response {
4373                description: Some("Created".to_string()),
4374                headers: Default::default(),
4375                content,
4376                links: Default::default(),
4377                extensions: Default::default(),
4378            }),
4379        );
4380
4381        let spec = create_test_spec();
4382        let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4383
4384        // Result is already a JSON Value
4385        insta::assert_json_snapshot!(result);
4386    }
4387
4388    #[test]
4389    fn test_extract_output_schema_with_2xx_response() {
4390        use oas3::spec::Response;
4391
4392        // Create only a 2XX response
4393        let mut responses = BTreeMap::new();
4394        let mut content = BTreeMap::new();
4395        content.insert(
4396            "application/json".to_string(),
4397            MediaType {
4398                extensions: Default::default(),
4399                schema: Some(ObjectOrReference::Object(ObjectSchema {
4400                    schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
4401                    items: Some(Box::new(Schema::Object(Box::new(
4402                        ObjectOrReference::Object(ObjectSchema {
4403                            schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4404                            ..Default::default()
4405                        }),
4406                    )))),
4407                    ..Default::default()
4408                })),
4409                examples: None,
4410                encoding: Default::default(),
4411            },
4412        );
4413
4414        responses.insert(
4415            "2XX".to_string(),
4416            ObjectOrReference::Object(Response {
4417                description: Some("Success".to_string()),
4418                headers: Default::default(),
4419                content,
4420                links: Default::default(),
4421                extensions: Default::default(),
4422            }),
4423        );
4424
4425        let spec = create_test_spec();
4426        let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4427
4428        // Result is already a JSON Value
4429        insta::assert_json_snapshot!(result);
4430    }
4431
4432    #[test]
4433    fn test_extract_output_schema_no_responses() {
4434        let spec = create_test_spec();
4435        let result = ToolGenerator::extract_output_schema(&None, &spec).unwrap();
4436
4437        // Result is already a JSON Value
4438        insta::assert_json_snapshot!(result);
4439    }
4440
4441    #[test]
4442    fn test_extract_output_schema_only_error_responses() {
4443        use oas3::spec::Response;
4444
4445        // Create only error responses
4446        let mut responses = BTreeMap::new();
4447        responses.insert(
4448            "404".to_string(),
4449            ObjectOrReference::Object(Response {
4450                description: Some("Not found".to_string()),
4451                headers: Default::default(),
4452                content: Default::default(),
4453                links: Default::default(),
4454                extensions: Default::default(),
4455            }),
4456        );
4457        responses.insert(
4458            "500".to_string(),
4459            ObjectOrReference::Object(Response {
4460                description: Some("Server error".to_string()),
4461                headers: Default::default(),
4462                content: Default::default(),
4463                links: Default::default(),
4464                extensions: Default::default(),
4465            }),
4466        );
4467
4468        let spec = create_test_spec();
4469        let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4470
4471        // Result is already a JSON Value
4472        insta::assert_json_snapshot!(result);
4473    }
4474
4475    #[test]
4476    fn test_extract_output_schema_with_ref() {
4477        use oas3::spec::Response;
4478
4479        // Create a spec with schema reference
4480        let mut spec = create_test_spec();
4481        let mut schemas = BTreeMap::new();
4482        schemas.insert(
4483            "Pet".to_string(),
4484            ObjectOrReference::Object(ObjectSchema {
4485                schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4486                properties: {
4487                    let mut props = BTreeMap::new();
4488                    props.insert(
4489                        "name".to_string(),
4490                        ObjectOrReference::Object(ObjectSchema {
4491                            schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4492                            ..Default::default()
4493                        }),
4494                    );
4495                    props
4496                },
4497                ..Default::default()
4498            }),
4499        );
4500        spec.components.as_mut().unwrap().schemas = schemas;
4501
4502        // Create response with $ref
4503        let mut responses = BTreeMap::new();
4504        let mut content = BTreeMap::new();
4505        content.insert(
4506            "application/json".to_string(),
4507            MediaType {
4508                extensions: Default::default(),
4509                schema: Some(ObjectOrReference::Ref {
4510                    ref_path: "#/components/schemas/Pet".to_string(),
4511                    summary: None,
4512                    description: None,
4513                }),
4514                examples: None,
4515                encoding: Default::default(),
4516            },
4517        );
4518
4519        responses.insert(
4520            "200".to_string(),
4521            ObjectOrReference::Object(Response {
4522                description: Some("Success".to_string()),
4523                headers: Default::default(),
4524                content,
4525                links: Default::default(),
4526                extensions: Default::default(),
4527            }),
4528        );
4529
4530        let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4531
4532        // Result is already a JSON Value
4533        insta::assert_json_snapshot!(result);
4534    }
4535
4536    #[test]
4537    fn test_generate_tool_metadata_includes_output_schema() {
4538        use oas3::spec::Response;
4539
4540        let mut operation = Operation {
4541            operation_id: Some("getPet".to_string()),
4542            summary: Some("Get a pet".to_string()),
4543            description: None,
4544            tags: vec![],
4545            external_docs: None,
4546            parameters: vec![],
4547            request_body: None,
4548            responses: Default::default(),
4549            callbacks: Default::default(),
4550            deprecated: Some(false),
4551            security: vec![],
4552            servers: vec![],
4553            extensions: Default::default(),
4554        };
4555
4556        // Add a response
4557        let mut responses = BTreeMap::new();
4558        let mut content = BTreeMap::new();
4559        content.insert(
4560            "application/json".to_string(),
4561            MediaType {
4562                extensions: Default::default(),
4563                schema: Some(ObjectOrReference::Object(ObjectSchema {
4564                    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4565                    properties: {
4566                        let mut props = BTreeMap::new();
4567                        props.insert(
4568                            "id".to_string(),
4569                            ObjectOrReference::Object(ObjectSchema {
4570                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4571                                ..Default::default()
4572                            }),
4573                        );
4574                        props
4575                    },
4576                    ..Default::default()
4577                })),
4578                examples: None,
4579                encoding: Default::default(),
4580            },
4581        );
4582
4583        responses.insert(
4584            "200".to_string(),
4585            ObjectOrReference::Object(Response {
4586                description: Some("Success".to_string()),
4587                headers: Default::default(),
4588                content,
4589                links: Default::default(),
4590                extensions: Default::default(),
4591            }),
4592        );
4593        operation.responses = Some(responses);
4594
4595        let spec = create_test_spec();
4596        let metadata = ToolGenerator::generate_tool_metadata(
4597            &operation,
4598            "get".to_string(),
4599            "/pets/{id}".to_string(),
4600            &spec,
4601            false,
4602            false,
4603            false,
4604        )
4605        .unwrap();
4606
4607        // Check that output_schema is included
4608        assert!(metadata.output_schema.is_some());
4609        let output_schema = metadata.output_schema.as_ref().unwrap();
4610
4611        // Use JSON snapshot for the output schema
4612        insta::assert_json_snapshot!(
4613            "test_generate_tool_metadata_includes_output_schema",
4614            output_schema
4615        );
4616
4617        // Validate against MCP Tool schema (this also validates output_schema if present)
4618        validate_tool_against_mcp_schema(&metadata);
4619    }
4620
4621    #[test]
4622    fn test_sanitize_property_name() {
4623        // Test spaces are replaced with underscores
4624        assert_eq!(sanitize_property_name("user name"), "user_name");
4625        assert_eq!(
4626            sanitize_property_name("first name last name"),
4627            "first_name_last_name"
4628        );
4629
4630        // Test special characters are replaced
4631        assert_eq!(sanitize_property_name("user(admin)"), "user_admin");
4632        assert_eq!(sanitize_property_name("user[admin]"), "user_admin");
4633        assert_eq!(sanitize_property_name("price($)"), "price");
4634        assert_eq!(sanitize_property_name("email@address"), "email_address");
4635        assert_eq!(sanitize_property_name("item#1"), "item_1");
4636        assert_eq!(sanitize_property_name("a/b/c"), "a_b_c");
4637
4638        // Test valid characters are preserved
4639        assert_eq!(sanitize_property_name("user_name"), "user_name");
4640        assert_eq!(sanitize_property_name("userName123"), "userName123");
4641        assert_eq!(sanitize_property_name("user.name"), "user.name");
4642        assert_eq!(sanitize_property_name("user-name"), "user-name");
4643
4644        // Test numeric starting names
4645        assert_eq!(sanitize_property_name("123name"), "param_123name");
4646        assert_eq!(sanitize_property_name("1st_place"), "param_1st_place");
4647
4648        // Test empty string
4649        assert_eq!(sanitize_property_name(""), "param_");
4650
4651        // Test length limit (64 characters)
4652        let long_name = "a".repeat(100);
4653        assert_eq!(sanitize_property_name(&long_name).len(), 64);
4654
4655        // Test all special characters become underscores
4656        // Note: After collapsing and trimming, this becomes empty and gets "param_" prefix
4657        assert_eq!(sanitize_property_name("!@#$%^&*()"), "param_");
4658    }
4659
4660    #[test]
4661    fn test_sanitize_property_name_trailing_underscores() {
4662        // Basic trailing underscore removal
4663        assert_eq!(sanitize_property_name("page[size]"), "page_size");
4664        assert_eq!(sanitize_property_name("user[id]"), "user_id");
4665        assert_eq!(sanitize_property_name("field[]"), "field");
4666
4667        // Multiple trailing underscores
4668        assert_eq!(sanitize_property_name("field___"), "field");
4669        assert_eq!(sanitize_property_name("test[[["), "test");
4670    }
4671
4672    #[test]
4673    fn test_sanitize_property_name_consecutive_underscores() {
4674        // Consecutive underscores in the middle
4675        assert_eq!(sanitize_property_name("user__name"), "user_name");
4676        assert_eq!(sanitize_property_name("first___last"), "first_last");
4677        assert_eq!(sanitize_property_name("a____b____c"), "a_b_c");
4678
4679        // Mix of special characters creating consecutive underscores
4680        assert_eq!(sanitize_property_name("user[[name]]"), "user_name");
4681        assert_eq!(sanitize_property_name("field@#$value"), "field_value");
4682    }
4683
4684    #[test]
4685    fn test_sanitize_property_name_edge_cases() {
4686        // Leading underscores (preserved)
4687        assert_eq!(sanitize_property_name("_private"), "_private");
4688        assert_eq!(sanitize_property_name("__dunder"), "_dunder");
4689
4690        // Only special characters
4691        assert_eq!(sanitize_property_name("[[["), "param_");
4692        assert_eq!(sanitize_property_name("@@@"), "param_");
4693
4694        // Empty after sanitization
4695        assert_eq!(sanitize_property_name(""), "param_");
4696
4697        // Mix of leading and trailing
4698        assert_eq!(sanitize_property_name("_field[size]"), "_field_size");
4699        assert_eq!(sanitize_property_name("__test__"), "_test");
4700    }
4701
4702    #[test]
4703    fn test_sanitize_property_name_complex_cases() {
4704        // Real-world examples
4705        assert_eq!(sanitize_property_name("page[size]"), "page_size");
4706        assert_eq!(sanitize_property_name("filter[status]"), "filter_status");
4707        assert_eq!(
4708            sanitize_property_name("sort[-created_at]"),
4709            "sort_-created_at"
4710        );
4711        assert_eq!(
4712            sanitize_property_name("include[author.posts]"),
4713            "include_author.posts"
4714        );
4715
4716        // Very long names with special characters
4717        let long_name = "very_long_field_name_with_special[characters]_that_needs_truncation_____";
4718        let expected = "very_long_field_name_with_special_characters_that_needs_truncat";
4719        assert_eq!(sanitize_property_name(long_name), expected);
4720    }
4721
4722    #[test]
4723    fn test_property_sanitization_with_annotations() {
4724        let spec = create_test_spec();
4725        let mut visited = HashSet::new();
4726
4727        // Create an object schema with properties that need sanitization
4728        let obj_schema = ObjectSchema {
4729            schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4730            properties: {
4731                let mut props = BTreeMap::new();
4732                // Property with space
4733                props.insert(
4734                    "user name".to_string(),
4735                    ObjectOrReference::Object(ObjectSchema {
4736                        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4737                        ..Default::default()
4738                    }),
4739                );
4740                // Property with special characters
4741                props.insert(
4742                    "price($)".to_string(),
4743                    ObjectOrReference::Object(ObjectSchema {
4744                        schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
4745                        ..Default::default()
4746                    }),
4747                );
4748                // Valid property name
4749                props.insert(
4750                    "validName".to_string(),
4751                    ObjectOrReference::Object(ObjectSchema {
4752                        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4753                        ..Default::default()
4754                    }),
4755                );
4756                props
4757            },
4758            ..Default::default()
4759        };
4760
4761        let result =
4762            ToolGenerator::convert_object_schema_to_json_schema(&obj_schema, &spec, &mut visited)
4763                .unwrap();
4764
4765        // Use JSON snapshot for the schema
4766        insta::assert_json_snapshot!("test_property_sanitization_with_annotations", result);
4767    }
4768
4769    #[test]
4770    fn test_parameter_sanitization_and_extraction() {
4771        let spec = create_test_spec();
4772
4773        // Create an operation with parameters that need sanitization
4774        let operation = Operation {
4775            operation_id: Some("testOp".to_string()),
4776            parameters: vec![
4777                // Path parameter with special characters
4778                ObjectOrReference::Object(Parameter {
4779                    name: "user(id)".to_string(),
4780                    location: ParameterIn::Path,
4781                    description: Some("User ID".to_string()),
4782                    required: Some(true),
4783                    deprecated: Some(false),
4784                    allow_empty_value: Some(false),
4785                    style: None,
4786                    explode: None,
4787                    allow_reserved: Some(false),
4788                    schema: Some(ObjectOrReference::Object(ObjectSchema {
4789                        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4790                        ..Default::default()
4791                    })),
4792                    example: None,
4793                    examples: Default::default(),
4794                    content: None,
4795                    extensions: Default::default(),
4796                }),
4797                // Query parameter with spaces
4798                ObjectOrReference::Object(Parameter {
4799                    name: "page size".to_string(),
4800                    location: ParameterIn::Query,
4801                    description: Some("Page size".to_string()),
4802                    required: Some(false),
4803                    deprecated: Some(false),
4804                    allow_empty_value: Some(false),
4805                    style: None,
4806                    explode: None,
4807                    allow_reserved: Some(false),
4808                    schema: Some(ObjectOrReference::Object(ObjectSchema {
4809                        schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4810                        ..Default::default()
4811                    })),
4812                    example: None,
4813                    examples: Default::default(),
4814                    content: None,
4815                    extensions: Default::default(),
4816                }),
4817                // Header parameter with special characters
4818                ObjectOrReference::Object(Parameter {
4819                    name: "auth-token!".to_string(),
4820                    location: ParameterIn::Header,
4821                    description: Some("Auth token".to_string()),
4822                    required: Some(false),
4823                    deprecated: Some(false),
4824                    allow_empty_value: Some(false),
4825                    style: None,
4826                    explode: None,
4827                    allow_reserved: Some(false),
4828                    schema: Some(ObjectOrReference::Object(ObjectSchema {
4829                        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4830                        ..Default::default()
4831                    })),
4832                    example: None,
4833                    examples: Default::default(),
4834                    content: None,
4835                    extensions: Default::default(),
4836                }),
4837            ],
4838            ..Default::default()
4839        };
4840
4841        let tool_metadata = ToolGenerator::generate_tool_metadata(
4842            &operation,
4843            "get".to_string(),
4844            "/users/{user(id)}".to_string(),
4845            &spec,
4846            false,
4847            false,
4848            false,
4849        )
4850        .unwrap();
4851
4852        // Check sanitized parameter names in schema
4853        let properties = tool_metadata
4854            .parameters
4855            .get("properties")
4856            .unwrap()
4857            .as_object()
4858            .unwrap();
4859
4860        assert!(properties.contains_key("user_id"));
4861        assert!(properties.contains_key("page_size"));
4862        assert!(properties.contains_key("header_auth-token"));
4863
4864        // Check that required array contains the sanitized name
4865        let required = tool_metadata
4866            .parameters
4867            .get("required")
4868            .unwrap()
4869            .as_array()
4870            .unwrap();
4871        assert!(required.contains(&json!("user_id")));
4872
4873        // Test parameter extraction with original names
4874        let arguments = json!({
4875            "user_id": "123",
4876            "page_size": 10,
4877            "header_auth-token": "secret"
4878        });
4879
4880        let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
4881
4882        // Path parameter should use original name
4883        assert_eq!(extracted.path.get("user(id)"), Some(&json!("123")));
4884
4885        // Query parameter should use original name
4886        assert_eq!(
4887            extracted.query.get("page size").map(|q| &q.value),
4888            Some(&json!(10))
4889        );
4890
4891        // Header parameter should use original name (without prefix)
4892        assert_eq!(extracted.headers.get("auth-token!"), Some(&json!("secret")));
4893    }
4894
4895    #[test]
4896    fn test_check_unknown_parameters() {
4897        // Test with unknown parameter that has a suggestion
4898        let mut properties = serde_json::Map::new();
4899        properties.insert("page_size".to_string(), json!({"type": "integer"}));
4900        properties.insert("user_id".to_string(), json!({"type": "string"}));
4901
4902        let mut args = serde_json::Map::new();
4903        args.insert("page_sixe".to_string(), json!(10)); // typo
4904
4905        let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4906        assert!(!result.is_empty());
4907        assert_eq!(result.len(), 1);
4908
4909        match &result[0] {
4910            ValidationError::InvalidParameter {
4911                parameter,
4912                suggestions,
4913                valid_parameters,
4914            } => {
4915                assert_eq!(parameter, "page_sixe");
4916                assert_eq!(suggestions, &vec!["page_size".to_string()]);
4917                assert_eq!(
4918                    valid_parameters,
4919                    &vec!["page_size".to_string(), "user_id".to_string()]
4920                );
4921            }
4922            _ => panic!("Expected InvalidParameter variant"),
4923        }
4924    }
4925
4926    #[test]
4927    fn test_check_unknown_parameters_no_suggestions() {
4928        // Test with unknown parameter that has no suggestions
4929        let mut properties = serde_json::Map::new();
4930        properties.insert("limit".to_string(), json!({"type": "integer"}));
4931        properties.insert("offset".to_string(), json!({"type": "integer"}));
4932
4933        let mut args = serde_json::Map::new();
4934        args.insert("xyz123".to_string(), json!("value"));
4935
4936        let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4937        assert!(!result.is_empty());
4938        assert_eq!(result.len(), 1);
4939
4940        match &result[0] {
4941            ValidationError::InvalidParameter {
4942                parameter,
4943                suggestions,
4944                valid_parameters,
4945            } => {
4946                assert_eq!(parameter, "xyz123");
4947                assert!(suggestions.is_empty());
4948                assert!(valid_parameters.contains(&"limit".to_string()));
4949                assert!(valid_parameters.contains(&"offset".to_string()));
4950            }
4951            _ => panic!("Expected InvalidParameter variant"),
4952        }
4953    }
4954
4955    #[test]
4956    fn test_check_unknown_parameters_multiple_suggestions() {
4957        // Test with unknown parameter that has multiple suggestions
4958        let mut properties = serde_json::Map::new();
4959        properties.insert("user_id".to_string(), json!({"type": "string"}));
4960        properties.insert("user_iid".to_string(), json!({"type": "string"}));
4961        properties.insert("user_name".to_string(), json!({"type": "string"}));
4962
4963        let mut args = serde_json::Map::new();
4964        args.insert("usr_id".to_string(), json!("123"));
4965
4966        let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4967        assert!(!result.is_empty());
4968        assert_eq!(result.len(), 1);
4969
4970        match &result[0] {
4971            ValidationError::InvalidParameter {
4972                parameter,
4973                suggestions,
4974                valid_parameters,
4975            } => {
4976                assert_eq!(parameter, "usr_id");
4977                assert!(!suggestions.is_empty());
4978                assert!(suggestions.contains(&"user_id".to_string()));
4979                assert_eq!(valid_parameters.len(), 3);
4980            }
4981            _ => panic!("Expected InvalidParameter variant"),
4982        }
4983    }
4984
4985    #[test]
4986    fn test_check_unknown_parameters_valid() {
4987        // Test with all valid parameters
4988        let mut properties = serde_json::Map::new();
4989        properties.insert("name".to_string(), json!({"type": "string"}));
4990        properties.insert("email".to_string(), json!({"type": "string"}));
4991
4992        let mut args = serde_json::Map::new();
4993        args.insert("name".to_string(), json!("John"));
4994        args.insert("email".to_string(), json!("john@example.com"));
4995
4996        let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4997        assert!(result.is_empty());
4998    }
4999
5000    #[test]
5001    fn test_check_unknown_parameters_empty() {
5002        // Test with no parameters defined
5003        let properties = serde_json::Map::new();
5004
5005        let mut args = serde_json::Map::new();
5006        args.insert("any_param".to_string(), json!("value"));
5007
5008        let result = ToolGenerator::check_unknown_parameters(&args, &properties);
5009        assert!(!result.is_empty());
5010        assert_eq!(result.len(), 1);
5011
5012        match &result[0] {
5013            ValidationError::InvalidParameter {
5014                parameter,
5015                suggestions,
5016                valid_parameters,
5017            } => {
5018                assert_eq!(parameter, "any_param");
5019                assert!(suggestions.is_empty());
5020                assert!(valid_parameters.is_empty());
5021            }
5022            _ => panic!("Expected InvalidParameter variant"),
5023        }
5024    }
5025
5026    #[test]
5027    fn test_check_unknown_parameters_gltf_pagination() {
5028        // Test the GLTF Live pagination scenario
5029        let mut properties = serde_json::Map::new();
5030        properties.insert(
5031            "page_number".to_string(),
5032            json!({
5033                "type": "integer",
5034                "x-original-name": "page[number]"
5035            }),
5036        );
5037        properties.insert(
5038            "page_size".to_string(),
5039            json!({
5040                "type": "integer",
5041                "x-original-name": "page[size]"
5042            }),
5043        );
5044
5045        // User passes page/per_page (common pagination params)
5046        let mut args = serde_json::Map::new();
5047        args.insert("page".to_string(), json!(1));
5048        args.insert("per_page".to_string(), json!(10));
5049
5050        let result = ToolGenerator::check_unknown_parameters(&args, &properties);
5051        assert_eq!(result.len(), 2, "Should have 2 unknown parameters");
5052
5053        // Check that both parameters are flagged as invalid
5054        let page_error = result
5055            .iter()
5056            .find(|e| {
5057                if let ValidationError::InvalidParameter { parameter, .. } = e {
5058                    parameter == "page"
5059                } else {
5060                    false
5061                }
5062            })
5063            .expect("Should have error for 'page'");
5064
5065        let per_page_error = result
5066            .iter()
5067            .find(|e| {
5068                if let ValidationError::InvalidParameter { parameter, .. } = e {
5069                    parameter == "per_page"
5070                } else {
5071                    false
5072                }
5073            })
5074            .expect("Should have error for 'per_page'");
5075
5076        // Verify suggestions are provided for 'page'
5077        match page_error {
5078            ValidationError::InvalidParameter {
5079                suggestions,
5080                valid_parameters,
5081                ..
5082            } => {
5083                assert!(
5084                    suggestions.contains(&"page_number".to_string()),
5085                    "Should suggest 'page_number' for 'page'"
5086                );
5087                assert_eq!(valid_parameters.len(), 2);
5088                assert!(valid_parameters.contains(&"page_number".to_string()));
5089                assert!(valid_parameters.contains(&"page_size".to_string()));
5090            }
5091            _ => panic!("Expected InvalidParameter"),
5092        }
5093
5094        // Verify error for 'per_page' (may not have suggestions due to low similarity)
5095        match per_page_error {
5096            ValidationError::InvalidParameter {
5097                parameter,
5098                suggestions,
5099                valid_parameters,
5100                ..
5101            } => {
5102                assert_eq!(parameter, "per_page");
5103                assert_eq!(valid_parameters.len(), 2);
5104                // per_page might not get suggestions if the similarity algorithm
5105                // doesn't find it similar enough to page_size
5106                if !suggestions.is_empty() {
5107                    assert!(suggestions.contains(&"page_size".to_string()));
5108                }
5109            }
5110            _ => panic!("Expected InvalidParameter"),
5111        }
5112    }
5113
5114    #[test]
5115    fn test_validate_parameters_with_invalid_params() {
5116        // Create a tool metadata with sanitized parameter names
5117        let tool_metadata = ToolMetadata {
5118            name: "listItems".to_string(),
5119            title: None,
5120            description: Some("List items".to_string()),
5121            parameters: json!({
5122                "type": "object",
5123                "properties": {
5124                    "page_number": {
5125                        "type": "integer",
5126                        "x-original-name": "page[number]"
5127                    },
5128                    "page_size": {
5129                        "type": "integer",
5130                        "x-original-name": "page[size]"
5131                    }
5132                },
5133                "required": []
5134            }),
5135            output_schema: None,
5136            method: "GET".to_string(),
5137            path: "/items".to_string(),
5138            security: None,
5139            parameter_mappings: std::collections::HashMap::new(),
5140        };
5141
5142        // Pass incorrect parameter names
5143        let arguments = json!({
5144            "page": 1,
5145            "per_page": 10
5146        });
5147
5148        let result = ToolGenerator::validate_parameters(&tool_metadata, &arguments);
5149        assert!(
5150            result.is_err(),
5151            "Should fail validation with unknown parameters"
5152        );
5153
5154        let error = result.unwrap_err();
5155        match error {
5156            ToolCallValidationError::InvalidParameters { violations } => {
5157                assert_eq!(violations.len(), 2, "Should have 2 validation errors");
5158
5159                // Check that both parameters are in the error
5160                let has_page_error = violations.iter().any(|v| {
5161                    if let ValidationError::InvalidParameter { parameter, .. } = v {
5162                        parameter == "page"
5163                    } else {
5164                        false
5165                    }
5166                });
5167
5168                let has_per_page_error = violations.iter().any(|v| {
5169                    if let ValidationError::InvalidParameter { parameter, .. } = v {
5170                        parameter == "per_page"
5171                    } else {
5172                        false
5173                    }
5174                });
5175
5176                assert!(has_page_error, "Should have error for 'page' parameter");
5177                assert!(
5178                    has_per_page_error,
5179                    "Should have error for 'per_page' parameter"
5180                );
5181            }
5182            _ => panic!("Expected InvalidParameters"),
5183        }
5184    }
5185
5186    #[test]
5187    fn test_cookie_parameter_sanitization() {
5188        let spec = create_test_spec();
5189
5190        let operation = Operation {
5191            operation_id: Some("testCookie".to_string()),
5192            parameters: vec![ObjectOrReference::Object(Parameter {
5193                name: "session[id]".to_string(),
5194                location: ParameterIn::Cookie,
5195                description: Some("Session ID".to_string()),
5196                required: Some(false),
5197                deprecated: Some(false),
5198                allow_empty_value: Some(false),
5199                style: None,
5200                explode: None,
5201                allow_reserved: Some(false),
5202                schema: Some(ObjectOrReference::Object(ObjectSchema {
5203                    schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5204                    ..Default::default()
5205                })),
5206                example: None,
5207                examples: Default::default(),
5208                content: None,
5209                extensions: Default::default(),
5210            })],
5211            ..Default::default()
5212        };
5213
5214        let tool_metadata = ToolGenerator::generate_tool_metadata(
5215            &operation,
5216            "get".to_string(),
5217            "/data".to_string(),
5218            &spec,
5219            false,
5220            false,
5221            false,
5222        )
5223        .unwrap();
5224
5225        let properties = tool_metadata
5226            .parameters
5227            .get("properties")
5228            .unwrap()
5229            .as_object()
5230            .unwrap();
5231
5232        // Check sanitized cookie parameter name
5233        assert!(properties.contains_key("cookie_session_id"));
5234
5235        // Test extraction
5236        let arguments = json!({
5237            "cookie_session_id": "abc123"
5238        });
5239
5240        let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
5241
5242        // Cookie should use original name
5243        assert_eq!(extracted.cookies.get("session[id]"), Some(&json!("abc123")));
5244    }
5245
5246    #[test]
5247    fn test_parameter_description_with_examples() {
5248        let spec = create_test_spec();
5249
5250        // Test parameter with single example
5251        let param_with_example = Parameter {
5252            name: "status".to_string(),
5253            location: ParameterIn::Query,
5254            description: Some("Filter by status".to_string()),
5255            required: Some(false),
5256            deprecated: Some(false),
5257            allow_empty_value: Some(false),
5258            style: None,
5259            explode: None,
5260            allow_reserved: Some(false),
5261            schema: Some(ObjectOrReference::Object(ObjectSchema {
5262                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5263                ..Default::default()
5264            })),
5265            example: Some(json!("active")),
5266            examples: Default::default(),
5267            content: None,
5268            extensions: Default::default(),
5269        };
5270
5271        let (schema, _) = ToolGenerator::convert_parameter_schema(
5272            &param_with_example,
5273            ParameterIn::Query,
5274            &spec,
5275            false,
5276            true,
5277        )
5278        .unwrap();
5279        let description = schema.get("description").unwrap().as_str().unwrap();
5280        assert_eq!(description, "Filter by status. Example: `\"active\"`");
5281
5282        // Test parameter with multiple examples
5283        let mut examples_map = std::collections::BTreeMap::new();
5284        examples_map.insert(
5285            "example1".to_string(),
5286            ObjectOrReference::Object(oas3::spec::Example {
5287                value: Some(json!("pending")),
5288                ..Default::default()
5289            }),
5290        );
5291        examples_map.insert(
5292            "example2".to_string(),
5293            ObjectOrReference::Object(oas3::spec::Example {
5294                value: Some(json!("completed")),
5295                ..Default::default()
5296            }),
5297        );
5298
5299        let param_with_examples = Parameter {
5300            name: "status".to_string(),
5301            location: ParameterIn::Query,
5302            description: Some("Filter by status".to_string()),
5303            required: Some(false),
5304            deprecated: Some(false),
5305            allow_empty_value: Some(false),
5306            style: None,
5307            explode: None,
5308            allow_reserved: Some(false),
5309            schema: Some(ObjectOrReference::Object(ObjectSchema {
5310                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5311                ..Default::default()
5312            })),
5313            example: None,
5314            examples: examples_map,
5315            content: None,
5316            extensions: Default::default(),
5317        };
5318
5319        let (schema, _) = ToolGenerator::convert_parameter_schema(
5320            &param_with_examples,
5321            ParameterIn::Query,
5322            &spec,
5323            false,
5324            true,
5325        )
5326        .unwrap();
5327        let description = schema.get("description").unwrap().as_str().unwrap();
5328        assert!(description.starts_with("Filter by status. Examples:\n"));
5329        assert!(description.contains("`\"pending\"`"));
5330        assert!(description.contains("`\"completed\"`"));
5331
5332        // Test parameter with no description but with example
5333        let param_no_desc = Parameter {
5334            name: "limit".to_string(),
5335            location: ParameterIn::Query,
5336            description: None,
5337            required: Some(false),
5338            deprecated: Some(false),
5339            allow_empty_value: Some(false),
5340            style: None,
5341            explode: None,
5342            allow_reserved: Some(false),
5343            schema: Some(ObjectOrReference::Object(ObjectSchema {
5344                schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
5345                ..Default::default()
5346            })),
5347            example: Some(json!(100)),
5348            examples: Default::default(),
5349            content: None,
5350            extensions: Default::default(),
5351        };
5352
5353        let (schema, _) = ToolGenerator::convert_parameter_schema(
5354            &param_no_desc,
5355            ParameterIn::Query,
5356            &spec,
5357            false,
5358            true,
5359        )
5360        .unwrap();
5361        let description = schema.get("description").unwrap().as_str().unwrap();
5362        assert_eq!(description, "limit parameter. Example: `100`");
5363    }
5364
5365    #[test]
5366    fn test_format_examples_for_description() {
5367        // Test single string example
5368        let examples = vec![json!("active")];
5369        let result = ToolGenerator::format_examples_for_description(&examples);
5370        assert_eq!(result, Some("Example: `\"active\"`".to_string()));
5371
5372        // Test single number example
5373        let examples = vec![json!(42)];
5374        let result = ToolGenerator::format_examples_for_description(&examples);
5375        assert_eq!(result, Some("Example: `42`".to_string()));
5376
5377        // Test single boolean example
5378        let examples = vec![json!(true)];
5379        let result = ToolGenerator::format_examples_for_description(&examples);
5380        assert_eq!(result, Some("Example: `true`".to_string()));
5381
5382        // Test multiple examples
5383        let examples = vec![json!("active"), json!("pending"), json!("completed")];
5384        let result = ToolGenerator::format_examples_for_description(&examples);
5385        assert_eq!(
5386            result,
5387            Some("Examples:\n- `\"active\"`\n- `\"pending\"`\n- `\"completed\"`".to_string())
5388        );
5389
5390        // Test array example
5391        let examples = vec![json!(["a", "b", "c"])];
5392        let result = ToolGenerator::format_examples_for_description(&examples);
5393        assert_eq!(result, Some("Example: `[\"a\",\"b\",\"c\"]`".to_string()));
5394
5395        // Test object example
5396        let examples = vec![json!({"key": "value"})];
5397        let result = ToolGenerator::format_examples_for_description(&examples);
5398        assert_eq!(result, Some("Example: `{\"key\":\"value\"}`".to_string()));
5399
5400        // Test empty examples
5401        let examples = vec![];
5402        let result = ToolGenerator::format_examples_for_description(&examples);
5403        assert_eq!(result, None);
5404
5405        // Test null example
5406        let examples = vec![json!(null)];
5407        let result = ToolGenerator::format_examples_for_description(&examples);
5408        assert_eq!(result, Some("Example: `null`".to_string()));
5409
5410        // Test mixed type examples
5411        let examples = vec![json!("text"), json!(123), json!(true)];
5412        let result = ToolGenerator::format_examples_for_description(&examples);
5413        assert_eq!(
5414            result,
5415            Some("Examples:\n- `\"text\"`\n- `123`\n- `true`".to_string())
5416        );
5417
5418        // Test long array (should be truncated)
5419        let examples = vec![json!(["a", "b", "c", "d", "e", "f"])];
5420        let result = ToolGenerator::format_examples_for_description(&examples);
5421        assert_eq!(
5422            result,
5423            Some("Example: `[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\"]`".to_string())
5424        );
5425
5426        // Test short array (should show full content)
5427        let examples = vec![json!([1, 2])];
5428        let result = ToolGenerator::format_examples_for_description(&examples);
5429        assert_eq!(result, Some("Example: `[1,2]`".to_string()));
5430
5431        // Test nested object
5432        let examples = vec![json!({"user": {"name": "John", "age": 30}})];
5433        let result = ToolGenerator::format_examples_for_description(&examples);
5434        assert_eq!(
5435            result,
5436            Some("Example: `{\"user\":{\"name\":\"John\",\"age\":30}}`".to_string())
5437        );
5438
5439        // Test more than 3 examples (should only show first 3)
5440        let examples = vec![json!("a"), json!("b"), json!("c"), json!("d"), json!("e")];
5441        let result = ToolGenerator::format_examples_for_description(&examples);
5442        assert_eq!(
5443            result,
5444            Some("Examples:\n- `\"a\"`\n- `\"b\"`\n- `\"c\"`\n- `\"d\"`\n- `\"e\"`".to_string())
5445        );
5446
5447        // Test float number
5448        let examples = vec![json!(3.5)];
5449        let result = ToolGenerator::format_examples_for_description(&examples);
5450        assert_eq!(result, Some("Example: `3.5`".to_string()));
5451
5452        // Test negative number
5453        let examples = vec![json!(-42)];
5454        let result = ToolGenerator::format_examples_for_description(&examples);
5455        assert_eq!(result, Some("Example: `-42`".to_string()));
5456
5457        // Test false boolean
5458        let examples = vec![json!(false)];
5459        let result = ToolGenerator::format_examples_for_description(&examples);
5460        assert_eq!(result, Some("Example: `false`".to_string()));
5461
5462        // Test string with special characters
5463        let examples = vec![json!("hello \"world\"")];
5464        let result = ToolGenerator::format_examples_for_description(&examples);
5465        // The format function just wraps strings in quotes, it doesn't escape them
5466        assert_eq!(result, Some(r#"Example: `"hello \"world\""`"#.to_string()));
5467
5468        // Test empty string
5469        let examples = vec![json!("")];
5470        let result = ToolGenerator::format_examples_for_description(&examples);
5471        assert_eq!(result, Some("Example: `\"\"`".to_string()));
5472
5473        // Test empty array
5474        let examples = vec![json!([])];
5475        let result = ToolGenerator::format_examples_for_description(&examples);
5476        assert_eq!(result, Some("Example: `[]`".to_string()));
5477
5478        // Test empty object
5479        let examples = vec![json!({})];
5480        let result = ToolGenerator::format_examples_for_description(&examples);
5481        assert_eq!(result, Some("Example: `{}`".to_string()));
5482    }
5483
5484    #[test]
5485    fn test_reference_metadata_functionality() {
5486        // Test ReferenceMetadata creation and methods
5487        let metadata = ReferenceMetadata::new(
5488            Some("User Reference".to_string()),
5489            Some("A reference to user data with additional context".to_string()),
5490        );
5491
5492        assert!(!metadata.is_empty());
5493        assert_eq!(metadata.summary(), Some("User Reference"));
5494        assert_eq!(
5495            metadata.best_description(),
5496            Some("A reference to user data with additional context")
5497        );
5498
5499        // Test metadata with only summary
5500        let summary_only = ReferenceMetadata::new(Some("Pet Summary".to_string()), None);
5501        assert_eq!(summary_only.best_description(), Some("Pet Summary"));
5502
5503        // Test empty metadata
5504        let empty_metadata = ReferenceMetadata::new(None, None);
5505        assert!(empty_metadata.is_empty());
5506        assert_eq!(empty_metadata.best_description(), None);
5507
5508        // Test merge_with_description
5509        let metadata = ReferenceMetadata::new(
5510            Some("Reference Summary".to_string()),
5511            Some("Reference Description".to_string()),
5512        );
5513
5514        // Test with no existing description
5515        let result = metadata.merge_with_description(None, false);
5516        assert_eq!(result, Some("Reference Description".to_string()));
5517
5518        // Test with existing description and no prepend - reference description takes precedence
5519        let result = metadata.merge_with_description(Some("Existing desc"), false);
5520        assert_eq!(result, Some("Reference Description".to_string()));
5521
5522        // Test with existing description and prepend summary - reference description still takes precedence
5523        let result = metadata.merge_with_description(Some("Existing desc"), true);
5524        assert_eq!(result, Some("Reference Description".to_string()));
5525
5526        // Test enhance_parameter_description - reference description takes precedence with proper formatting
5527        let result = metadata.enhance_parameter_description("userId", Some("User ID parameter"));
5528        assert_eq!(result, Some("userId: Reference Description".to_string()));
5529
5530        let result = metadata.enhance_parameter_description("userId", None);
5531        assert_eq!(result, Some("userId: Reference Description".to_string()));
5532
5533        // Test precedence: summary-only metadata should use summary when no description
5534        let summary_only = ReferenceMetadata::new(Some("API Token".to_string()), None);
5535
5536        let result = summary_only.merge_with_description(Some("Generic token"), false);
5537        assert_eq!(result, Some("API Token".to_string()));
5538
5539        let result = summary_only.merge_with_description(Some("Different desc"), true);
5540        assert_eq!(result, Some("API Token".to_string())); // Summary takes precedence via best_description()
5541
5542        let result = summary_only.enhance_parameter_description("token", Some("Token field"));
5543        assert_eq!(result, Some("token: API Token".to_string()));
5544
5545        // Test fallback behavior: no reference metadata should use schema description
5546        let empty_meta = ReferenceMetadata::new(None, None);
5547
5548        let result = empty_meta.merge_with_description(Some("Schema description"), false);
5549        assert_eq!(result, Some("Schema description".to_string()));
5550
5551        let result = empty_meta.enhance_parameter_description("param", Some("Schema param"));
5552        assert_eq!(result, Some("Schema param".to_string()));
5553
5554        let result = empty_meta.enhance_parameter_description("param", None);
5555        assert_eq!(result, Some("param parameter".to_string()));
5556    }
5557
5558    #[test]
5559    fn test_parameter_schema_with_reference_metadata() {
5560        let mut spec = create_test_spec();
5561
5562        // Add a Pet schema to resolve the reference
5563        spec.components.as_mut().unwrap().schemas.insert(
5564            "Pet".to_string(),
5565            ObjectOrReference::Object(ObjectSchema {
5566                description: None, // No description so reference metadata should be used as fallback
5567                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5568                ..Default::default()
5569            }),
5570        );
5571
5572        // Create a parameter with a reference that has metadata
5573        let param_with_ref = Parameter {
5574            name: "user".to_string(),
5575            location: ParameterIn::Query,
5576            description: None,
5577            required: Some(true),
5578            deprecated: Some(false),
5579            allow_empty_value: Some(false),
5580            style: None,
5581            explode: None,
5582            allow_reserved: Some(false),
5583            schema: Some(ObjectOrReference::Ref {
5584                ref_path: "#/components/schemas/Pet".to_string(),
5585                summary: Some("Pet Reference".to_string()),
5586                description: Some("A reference to pet schema with additional context".to_string()),
5587            }),
5588            example: None,
5589            examples: BTreeMap::new(),
5590            content: None,
5591            extensions: Default::default(),
5592        };
5593
5594        // Convert the parameter schema
5595        let result = ToolGenerator::convert_parameter_schema(
5596            &param_with_ref,
5597            ParameterIn::Query,
5598            &spec,
5599            false,
5600            false,
5601        );
5602
5603        assert!(result.is_ok());
5604        let (schema, _annotations) = result.unwrap();
5605
5606        // Check that the schema includes the reference description as fallback
5607        let description = schema.get("description").and_then(|v| v.as_str());
5608        assert!(description.is_some());
5609        // The description should be the reference metadata since resolved schema may not have one
5610        assert!(
5611            description.unwrap().contains("Pet Reference")
5612                || description
5613                    .unwrap()
5614                    .contains("A reference to pet schema with additional context")
5615        );
5616    }
5617
5618    #[test]
5619    fn test_request_body_with_reference_metadata() {
5620        let spec = create_test_spec();
5621
5622        // Create request body reference with metadata
5623        let request_body_ref = ObjectOrReference::Ref {
5624            ref_path: "#/components/requestBodies/PetBody".to_string(),
5625            summary: Some("Pet Request Body".to_string()),
5626            description: Some(
5627                "Request body containing pet information for API operations".to_string(),
5628            ),
5629        };
5630
5631        let result = ToolGenerator::convert_request_body_to_json_schema(&request_body_ref, &spec);
5632
5633        assert!(result.is_ok());
5634        let schema_result = result.unwrap();
5635        assert!(schema_result.is_some());
5636
5637        let (schema, _annotations, _required) = schema_result.unwrap();
5638        let description = schema.get("description").and_then(|v| v.as_str());
5639
5640        assert!(description.is_some());
5641        // Should use the reference description
5642        assert_eq!(
5643            description.unwrap(),
5644            "Request body containing pet information for API operations"
5645        );
5646    }
5647
5648    #[test]
5649    fn test_response_schema_with_reference_metadata() {
5650        let spec = create_test_spec();
5651
5652        // Create responses with a reference that has metadata
5653        let mut responses = BTreeMap::new();
5654        responses.insert(
5655            "200".to_string(),
5656            ObjectOrReference::Ref {
5657                ref_path: "#/components/responses/PetResponse".to_string(),
5658                summary: Some("Successful Pet Response".to_string()),
5659                description: Some(
5660                    "Response containing pet data on successful operation".to_string(),
5661                ),
5662            },
5663        );
5664        let responses_option = Some(responses);
5665
5666        let result = ToolGenerator::extract_output_schema(&responses_option, &spec);
5667
5668        assert!(result.is_ok());
5669        let schema = result.unwrap();
5670        assert!(schema.is_some());
5671
5672        let schema_value = schema.unwrap();
5673        let body_desc = schema_value
5674            .get("properties")
5675            .and_then(|props| props.get("body"))
5676            .and_then(|body| body.get("description"))
5677            .and_then(|desc| desc.as_str());
5678
5679        assert!(body_desc.is_some());
5680        // Should contain the reference description
5681        assert_eq!(
5682            body_desc.unwrap(),
5683            "Response containing pet data on successful operation"
5684        );
5685    }
5686
5687    #[test]
5688    fn test_self_referencing_schema_does_not_overflow() {
5689        // Create a spec with a self-referencing schema (like a tree node)
5690        // This should be handled gracefully, not cause a stack overflow
5691        let mut spec = create_test_spec();
5692
5693        // Create a "Node" schema that references itself via children
5694        let node_schema = ObjectSchema {
5695            schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5696            properties: {
5697                let mut props = BTreeMap::new();
5698                props.insert(
5699                    "name".to_string(),
5700                    ObjectOrReference::Object(ObjectSchema {
5701                        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5702                        ..Default::default()
5703                    }),
5704                );
5705                // Self-reference: children is an array of Node
5706                props.insert(
5707                    "children".to_string(),
5708                    ObjectOrReference::Object(ObjectSchema {
5709                        schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
5710                        items: Some(Box::new(Schema::Object(Box::new(ObjectOrReference::Ref {
5711                            ref_path: "#/components/schemas/Node".to_string(),
5712                            summary: None,
5713                            description: None,
5714                        })))),
5715                        ..Default::default()
5716                    }),
5717                );
5718                props
5719            },
5720            ..Default::default()
5721        };
5722
5723        // Add the schema to components
5724        if let Some(ref mut components) = spec.components {
5725            components
5726                .schemas
5727                .insert("Node".to_string(), ObjectOrReference::Object(node_schema));
5728        }
5729
5730        // Now try to convert a reference to this self-referencing schema
5731        let mut visited = HashSet::new();
5732        let result = ToolGenerator::convert_schema_to_json_schema(
5733            &Schema::Object(Box::new(ObjectOrReference::Ref {
5734                ref_path: "#/components/schemas/Node".to_string(),
5735                summary: None,
5736                description: None,
5737            })),
5738            &spec,
5739            &mut visited,
5740        );
5741
5742        // Should return an error about circular reference, not overflow the stack
5743        assert!(
5744            result.is_err(),
5745            "Expected circular reference error, got: {result:?}"
5746        );
5747        let error = result.unwrap_err();
5748        assert!(
5749            error.to_string().contains("Circular reference"),
5750            "Expected circular reference error message, got: {error}"
5751        );
5752    }
5753
5754    // ==================== Multipart Form Data Tests ====================
5755
5756    #[test]
5757    fn test_multipart_form_data_with_single_file() {
5758        // Test that a single binary file field in multipart/form-data is transformed
5759        // to the structured file object schema with content and filename properties
5760        let request_body = ObjectOrReference::Object(RequestBody {
5761            description: Some("File upload request".to_string()),
5762            content: {
5763                let mut content = BTreeMap::new();
5764                content.insert(
5765                    "multipart/form-data".to_string(),
5766                    MediaType {
5767                        extensions: Default::default(),
5768                        schema: Some(ObjectOrReference::Object(ObjectSchema {
5769                            schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5770                            properties: {
5771                                let mut props = BTreeMap::new();
5772                                props.insert(
5773                                    "file".to_string(),
5774                                    ObjectOrReference::Object(ObjectSchema {
5775                                        schema_type: Some(SchemaTypeSet::Single(
5776                                            SchemaType::String,
5777                                        )),
5778                                        format: Some("binary".to_string()),
5779                                        description: Some("The file to upload".to_string()),
5780                                        ..Default::default()
5781                                    }),
5782                                );
5783                                props
5784                            },
5785                            required: vec!["file".to_string()],
5786                            ..Default::default()
5787                        })),
5788                        examples: None,
5789                        encoding: Default::default(),
5790                    },
5791                );
5792                content
5793            },
5794            required: Some(true),
5795        });
5796
5797        let spec = create_test_spec();
5798        let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
5799            .unwrap()
5800            .unwrap();
5801
5802        let (schema, annotations, is_required) = result;
5803
5804        // Verify the schema structure
5805        let schema_obj = schema.as_object().unwrap();
5806        assert_eq!(schema_obj.get("type").unwrap(), "object");
5807
5808        // Verify the file field is transformed to the file object schema
5809        let file_schema = schema_obj.get("properties").unwrap().get("file").unwrap();
5810
5811        // Check that it has the expected structure for file fields
5812        assert_eq!(file_schema.get("type").unwrap(), "object");
5813        assert!(
5814            file_schema
5815                .get("properties")
5816                .unwrap()
5817                .get("content")
5818                .is_some()
5819        );
5820        assert!(
5821            file_schema
5822                .get("properties")
5823                .unwrap()
5824                .get("filename")
5825                .is_some()
5826        );
5827        assert!(
5828            file_schema
5829                .get("required")
5830                .unwrap()
5831                .as_array()
5832                .unwrap()
5833                .contains(&json!("content"))
5834        );
5835
5836        // Check the annotations
5837        let annotations_value = serde_json::to_value(&annotations).unwrap();
5838        let annotations_obj = annotations_value.as_object().unwrap();
5839
5840        // Check x-content-type annotation
5841        assert_eq!(
5842            annotations_obj.get("x-content-type").unwrap(),
5843            "multipart/form-data"
5844        );
5845
5846        // Check x-file-fields annotation
5847        let x_file_fields = annotations_obj
5848            .get("x-file-fields")
5849            .unwrap()
5850            .as_array()
5851            .unwrap();
5852        assert_eq!(x_file_fields.len(), 1);
5853        assert!(x_file_fields.contains(&json!("file")));
5854
5855        // Check required flag
5856        assert!(is_required);
5857
5858        // Validate using snapshot
5859        insta::assert_json_snapshot!("test_multipart_form_data_with_single_file", schema);
5860    }
5861
5862    #[test]
5863    fn test_multipart_form_data_with_multiple_files() {
5864        // Test that multiple binary file fields are all transformed correctly
5865        let request_body = ObjectOrReference::Object(RequestBody {
5866            description: Some("Multiple file upload request".to_string()),
5867            content: {
5868                let mut content = BTreeMap::new();
5869                content.insert(
5870                    "multipart/form-data".to_string(),
5871                    MediaType {
5872                        extensions: Default::default(),
5873                        schema: Some(ObjectOrReference::Object(ObjectSchema {
5874                            schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5875                            properties: {
5876                                let mut props = BTreeMap::new();
5877                                props.insert(
5878                                    "avatar".to_string(),
5879                                    ObjectOrReference::Object(ObjectSchema {
5880                                        schema_type: Some(SchemaTypeSet::Single(
5881                                            SchemaType::String,
5882                                        )),
5883                                        format: Some("binary".to_string()),
5884                                        description: Some("Profile avatar image".to_string()),
5885                                        ..Default::default()
5886                                    }),
5887                                );
5888                                props.insert(
5889                                    "document".to_string(),
5890                                    ObjectOrReference::Object(ObjectSchema {
5891                                        schema_type: Some(SchemaTypeSet::Single(
5892                                            SchemaType::String,
5893                                        )),
5894                                        format: Some("binary".to_string()),
5895                                        description: Some("Supporting document".to_string()),
5896                                        ..Default::default()
5897                                    }),
5898                                );
5899                                props.insert(
5900                                    "resume".to_string(),
5901                                    ObjectOrReference::Object(ObjectSchema {
5902                                        schema_type: Some(SchemaTypeSet::Single(
5903                                            SchemaType::String,
5904                                        )),
5905                                        format: Some("binary".to_string()),
5906                                        description: Some("Resume file".to_string()),
5907                                        ..Default::default()
5908                                    }),
5909                                );
5910                                props
5911                            },
5912                            required: vec!["avatar".to_string(), "resume".to_string()],
5913                            ..Default::default()
5914                        })),
5915                        examples: None,
5916                        encoding: Default::default(),
5917                    },
5918                );
5919                content
5920            },
5921            required: Some(true),
5922        });
5923
5924        let spec = create_test_spec();
5925        let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
5926            .unwrap()
5927            .unwrap();
5928
5929        let (schema, annotations, _is_required) = result;
5930
5931        // Verify all file fields are transformed
5932        let body_properties = schema.get("properties").unwrap();
5933        for field_name in ["avatar", "document", "resume"] {
5934            let field_schema = body_properties.get(field_name).unwrap();
5935            assert_eq!(
5936                field_schema.get("type").unwrap(),
5937                "object",
5938                "Field {field_name} should be transformed to object type"
5939            );
5940            assert!(
5941                field_schema
5942                    .get("properties")
5943                    .unwrap()
5944                    .get("content")
5945                    .is_some(),
5946                "Field {field_name} should have content property"
5947            );
5948        }
5949
5950        // Check the annotations contain all file fields
5951        let annotations_value = serde_json::to_value(&annotations).unwrap();
5952        let annotations_obj = annotations_value.as_object().unwrap();
5953
5954        let x_file_fields = annotations_obj
5955            .get("x-file-fields")
5956            .unwrap()
5957            .as_array()
5958            .unwrap();
5959        assert_eq!(x_file_fields.len(), 3);
5960        assert!(x_file_fields.contains(&json!("avatar")));
5961        assert!(x_file_fields.contains(&json!("document")));
5962        assert!(x_file_fields.contains(&json!("resume")));
5963
5964        // Validate using snapshot
5965        insta::assert_json_snapshot!("test_multipart_form_data_with_multiple_files", schema);
5966    }
5967
5968    #[test]
5969    fn test_multipart_form_data_mixed_fields() {
5970        // Test that binary fields are transformed but regular fields remain unchanged
5971        let request_body = ObjectOrReference::Object(RequestBody {
5972            description: Some("Profile creation with file upload".to_string()),
5973            content: {
5974                let mut content = BTreeMap::new();
5975                content.insert(
5976                    "multipart/form-data".to_string(),
5977                    MediaType {
5978                        extensions: Default::default(),
5979                        schema: Some(ObjectOrReference::Object(ObjectSchema {
5980                            schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5981                            properties: {
5982                                let mut props = BTreeMap::new();
5983                                // Binary file field - should be transformed
5984                                props.insert(
5985                                    "avatar".to_string(),
5986                                    ObjectOrReference::Object(ObjectSchema {
5987                                        schema_type: Some(SchemaTypeSet::Single(
5988                                            SchemaType::String,
5989                                        )),
5990                                        format: Some("binary".to_string()),
5991                                        description: Some("Profile avatar image".to_string()),
5992                                        ..Default::default()
5993                                    }),
5994                                );
5995                                // Regular string field - should remain unchanged
5996                                props.insert(
5997                                    "name".to_string(),
5998                                    ObjectOrReference::Object(ObjectSchema {
5999                                        schema_type: Some(SchemaTypeSet::Single(
6000                                            SchemaType::String,
6001                                        )),
6002                                        description: Some("User's display name".to_string()),
6003                                        ..Default::default()
6004                                    }),
6005                                );
6006                                // Regular integer field - should remain unchanged
6007                                props.insert(
6008                                    "age".to_string(),
6009                                    ObjectOrReference::Object(ObjectSchema {
6010                                        schema_type: Some(SchemaTypeSet::Single(
6011                                            SchemaType::Integer,
6012                                        )),
6013                                        description: Some("User's age".to_string()),
6014                                        ..Default::default()
6015                                    }),
6016                                );
6017                                // Email field with format - should remain unchanged
6018                                props.insert(
6019                                    "email".to_string(),
6020                                    ObjectOrReference::Object(ObjectSchema {
6021                                        schema_type: Some(SchemaTypeSet::Single(
6022                                            SchemaType::String,
6023                                        )),
6024                                        format: Some("email".to_string()),
6025                                        description: Some("User's email address".to_string()),
6026                                        ..Default::default()
6027                                    }),
6028                                );
6029                                props
6030                            },
6031                            required: vec!["name".to_string(), "avatar".to_string()],
6032                            ..Default::default()
6033                        })),
6034                        examples: None,
6035                        encoding: Default::default(),
6036                    },
6037                );
6038                content
6039            },
6040            required: Some(true),
6041        });
6042
6043        let spec = create_test_spec();
6044        let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
6045            .unwrap()
6046            .unwrap();
6047
6048        let (schema, annotations, _is_required) = result;
6049        let body_properties = schema.get("properties").unwrap();
6050
6051        // Verify avatar (binary) is transformed to file object schema
6052        let avatar_schema = body_properties.get("avatar").unwrap();
6053        assert_eq!(avatar_schema.get("type").unwrap(), "object");
6054        assert!(
6055            avatar_schema
6056                .get("properties")
6057                .unwrap()
6058                .get("content")
6059                .is_some()
6060        );
6061        assert!(
6062            avatar_schema
6063                .get("properties")
6064                .unwrap()
6065                .get("filename")
6066                .is_some()
6067        );
6068
6069        // Verify name (string) remains a simple string type
6070        let name_schema = body_properties.get("name").unwrap();
6071        assert_eq!(name_schema.get("type").unwrap(), "string");
6072        assert!(name_schema.get("properties").is_none()); // No nested properties
6073
6074        // Verify age (integer) remains an integer type
6075        let age_schema = body_properties.get("age").unwrap();
6076        assert_eq!(age_schema.get("type").unwrap(), "integer");
6077
6078        // Verify email (string with format: email) remains a string type
6079        let email_schema = body_properties.get("email").unwrap();
6080        assert_eq!(email_schema.get("type").unwrap(), "string");
6081        assert_eq!(email_schema.get("format").unwrap(), "email");
6082
6083        // Check annotations only contains avatar as file field
6084        let annotations_value = serde_json::to_value(&annotations).unwrap();
6085        let annotations_obj = annotations_value.as_object().unwrap();
6086
6087        let x_file_fields = annotations_obj
6088            .get("x-file-fields")
6089            .unwrap()
6090            .as_array()
6091            .unwrap();
6092        assert_eq!(x_file_fields.len(), 1);
6093        assert!(x_file_fields.contains(&json!("avatar")));
6094
6095        // Validate using snapshot
6096        insta::assert_json_snapshot!("test_multipart_form_data_mixed_fields", schema);
6097    }
6098
6099    #[test]
6100    fn test_multipart_format_byte_detection() {
6101        // Test that format: byte is also detected as a file field
6102        let request_body = ObjectOrReference::Object(RequestBody {
6103            description: Some("Base64 encoded file upload".to_string()),
6104            content: {
6105                let mut content = BTreeMap::new();
6106                content.insert(
6107                    "multipart/form-data".to_string(),
6108                    MediaType {
6109                        extensions: Default::default(),
6110                        schema: Some(ObjectOrReference::Object(ObjectSchema {
6111                            schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
6112                            properties: {
6113                                let mut props = BTreeMap::new();
6114                                // format: byte should be treated as a file field
6115                                props.insert(
6116                                    "data".to_string(),
6117                                    ObjectOrReference::Object(ObjectSchema {
6118                                        schema_type: Some(SchemaTypeSet::Single(
6119                                            SchemaType::String,
6120                                        )),
6121                                        format: Some("byte".to_string()),
6122                                        description: Some(
6123                                            "Base64 encoded file content".to_string(),
6124                                        ),
6125                                        ..Default::default()
6126                                    }),
6127                                );
6128                                // format: binary for comparison
6129                                props.insert(
6130                                    "attachment".to_string(),
6131                                    ObjectOrReference::Object(ObjectSchema {
6132                                        schema_type: Some(SchemaTypeSet::Single(
6133                                            SchemaType::String,
6134                                        )),
6135                                        format: Some("binary".to_string()),
6136                                        description: Some("Binary file attachment".to_string()),
6137                                        ..Default::default()
6138                                    }),
6139                                );
6140                                props
6141                            },
6142                            required: vec!["data".to_string()],
6143                            ..Default::default()
6144                        })),
6145                        examples: None,
6146                        encoding: Default::default(),
6147                    },
6148                );
6149                content
6150            },
6151            required: Some(true),
6152        });
6153
6154        let spec = create_test_spec();
6155        let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
6156            .unwrap()
6157            .unwrap();
6158
6159        let (schema, annotations, _is_required) = result;
6160        let body_properties = schema.get("properties").unwrap();
6161
6162        // Verify both byte and binary format fields are transformed
6163        let data_schema = body_properties.get("data").unwrap();
6164        assert_eq!(data_schema.get("type").unwrap(), "object");
6165        assert!(
6166            data_schema
6167                .get("properties")
6168                .unwrap()
6169                .get("content")
6170                .is_some()
6171        );
6172
6173        let attachment_schema = body_properties.get("attachment").unwrap();
6174        assert_eq!(attachment_schema.get("type").unwrap(), "object");
6175        assert!(
6176            attachment_schema
6177                .get("properties")
6178                .unwrap()
6179                .get("content")
6180                .is_some()
6181        );
6182
6183        // Check annotations contain both fields
6184        let annotations_value = serde_json::to_value(&annotations).unwrap();
6185        let annotations_obj = annotations_value.as_object().unwrap();
6186
6187        let x_file_fields = annotations_obj
6188            .get("x-file-fields")
6189            .unwrap()
6190            .as_array()
6191            .unwrap();
6192        assert_eq!(x_file_fields.len(), 2);
6193        assert!(x_file_fields.contains(&json!("data")));
6194        assert!(x_file_fields.contains(&json!("attachment")));
6195
6196        // Validate using snapshot
6197        insta::assert_json_snapshot!("test_multipart_format_byte_detection", schema);
6198    }
6199
6200    #[test]
6201    fn test_multipart_non_file_fields_unchanged() {
6202        // Test that non-file fields under multipart/form-data stay as their original types
6203        let request_body = ObjectOrReference::Object(RequestBody {
6204            description: Some("Form submission".to_string()),
6205            content: {
6206                let mut content = BTreeMap::new();
6207                content.insert(
6208                    "multipart/form-data".to_string(),
6209                    MediaType {
6210                        extensions: Default::default(),
6211                        schema: Some(ObjectOrReference::Object(ObjectSchema {
6212                            schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
6213                            properties: {
6214                                let mut props = BTreeMap::new();
6215                                // Various non-file field types
6216                                props.insert(
6217                                    "title".to_string(),
6218                                    ObjectOrReference::Object(ObjectSchema {
6219                                        schema_type: Some(SchemaTypeSet::Single(
6220                                            SchemaType::String,
6221                                        )),
6222                                        description: Some("Form title".to_string()),
6223                                        ..Default::default()
6224                                    }),
6225                                );
6226                                props.insert(
6227                                    "count".to_string(),
6228                                    ObjectOrReference::Object(ObjectSchema {
6229                                        schema_type: Some(SchemaTypeSet::Single(
6230                                            SchemaType::Integer,
6231                                        )),
6232                                        description: Some("Item count".to_string()),
6233                                        ..Default::default()
6234                                    }),
6235                                );
6236                                props.insert(
6237                                    "enabled".to_string(),
6238                                    ObjectOrReference::Object(ObjectSchema {
6239                                        schema_type: Some(SchemaTypeSet::Single(
6240                                            SchemaType::Boolean,
6241                                        )),
6242                                        description: Some("Enable flag".to_string()),
6243                                        ..Default::default()
6244                                    }),
6245                                );
6246                                props.insert(
6247                                    "price".to_string(),
6248                                    ObjectOrReference::Object(ObjectSchema {
6249                                        schema_type: Some(SchemaTypeSet::Single(
6250                                            SchemaType::Number,
6251                                        )),
6252                                        description: Some("Price value".to_string()),
6253                                        ..Default::default()
6254                                    }),
6255                                );
6256                                props.insert(
6257                                    "uuid".to_string(),
6258                                    ObjectOrReference::Object(ObjectSchema {
6259                                        schema_type: Some(SchemaTypeSet::Single(
6260                                            SchemaType::String,
6261                                        )),
6262                                        format: Some("uuid".to_string()),
6263                                        description: Some("UUID field".to_string()),
6264                                        ..Default::default()
6265                                    }),
6266                                );
6267                                props.insert(
6268                                    "date".to_string(),
6269                                    ObjectOrReference::Object(ObjectSchema {
6270                                        schema_type: Some(SchemaTypeSet::Single(
6271                                            SchemaType::String,
6272                                        )),
6273                                        format: Some("date".to_string()),
6274                                        description: Some("Date field".to_string()),
6275                                        ..Default::default()
6276                                    }),
6277                                );
6278                                props
6279                            },
6280                            required: vec!["title".to_string()],
6281                            ..Default::default()
6282                        })),
6283                        examples: None,
6284                        encoding: Default::default(),
6285                    },
6286                );
6287                content
6288            },
6289            required: Some(true),
6290        });
6291
6292        let spec = create_test_spec();
6293        let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
6294            .unwrap()
6295            .unwrap();
6296
6297        let (schema, annotations, _is_required) = result;
6298        let body_properties = schema.get("properties").unwrap();
6299
6300        // Verify string field stays as string
6301        let title_schema = body_properties.get("title").unwrap();
6302        assert_eq!(title_schema.get("type").unwrap(), "string");
6303        assert!(title_schema.get("properties").is_none());
6304
6305        // Verify integer field stays as integer
6306        let count_schema = body_properties.get("count").unwrap();
6307        assert_eq!(count_schema.get("type").unwrap(), "integer");
6308
6309        // Verify boolean field stays as boolean
6310        let enabled_schema = body_properties.get("enabled").unwrap();
6311        assert_eq!(enabled_schema.get("type").unwrap(), "boolean");
6312
6313        // Verify number field stays as number
6314        let price_schema = body_properties.get("price").unwrap();
6315        assert_eq!(price_schema.get("type").unwrap(), "number");
6316
6317        // Verify string with uuid format stays as string with format
6318        let uuid_schema = body_properties.get("uuid").unwrap();
6319        assert_eq!(uuid_schema.get("type").unwrap(), "string");
6320        assert_eq!(uuid_schema.get("format").unwrap(), "uuid");
6321
6322        // Verify string with date format stays as string with format
6323        let date_schema = body_properties.get("date").unwrap();
6324        assert_eq!(date_schema.get("type").unwrap(), "string");
6325        assert_eq!(date_schema.get("format").unwrap(), "date");
6326
6327        // Check that annotations do NOT contain x-file-fields (no file fields present)
6328        let annotations_value = serde_json::to_value(&annotations).unwrap();
6329        let annotations_obj = annotations_value.as_object().unwrap();
6330
6331        assert!(
6332            annotations_obj.get("x-file-fields").is_none(),
6333            "x-file-fields should not be present when there are no file fields"
6334        );
6335
6336        // Verify multipart/form-data content type is still set
6337        assert_eq!(
6338            annotations_obj.get("x-content-type").unwrap(),
6339            "multipart/form-data"
6340        );
6341
6342        // Validate using snapshot
6343        insta::assert_json_snapshot!("test_multipart_non_file_fields_unchanged", schema);
6344    }
6345}