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