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