Skip to main content

rmcp_openapi/
tool_generator.rs

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