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