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