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