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