rmcp_openapi/
tool_generator.rs

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