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