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