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