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