rmcp_openapi/
tool_generator.rs

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