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