rmcp_openapi/
tool_generator.rs

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