rmcp_openapi/
tool_generator.rs

1use serde_json::{Value, json};
2use std::collections::{HashMap, HashSet};
3
4use crate::error::OpenApiError;
5use crate::server::ToolMetadata;
6use oas3::spec::{
7    ObjectOrReference, ObjectSchema, Operation, Parameter, ParameterIn, RequestBody, Schema,
8    SchemaType, SchemaTypeSet, Spec,
9};
10
11/// Tool generator for creating MCP tools from `OpenAPI` operations
12pub struct ToolGenerator;
13
14impl ToolGenerator {
15    /// Generate tool metadata from an `OpenAPI` operation
16    ///
17    /// # Errors
18    ///
19    /// Returns an error if the operation cannot be converted to tool metadata
20    pub fn generate_tool_metadata(
21        operation: &Operation,
22        method: String,
23        path: String,
24        spec: &Spec,
25    ) -> Result<ToolMetadata, OpenApiError> {
26        let name = operation.operation_id.clone().unwrap_or_else(|| {
27            format!(
28                "{}_{}",
29                method,
30                path.replace('/', "_").replace(['{', '}'], "")
31            )
32        });
33
34        // Build description from summary and description
35        let description = Self::build_description(operation, &method, &path);
36
37        // Generate parameter schema
38        let parameters = Self::generate_parameter_schema(
39            &operation.parameters,
40            &method,
41            &operation.request_body,
42            spec,
43        )?;
44
45        Ok(ToolMetadata {
46            name,
47            description,
48            parameters,
49            method,
50            path,
51        })
52    }
53
54    /// Build a comprehensive description for the tool
55    fn build_description(operation: &Operation, method: &str, path: &str) -> String {
56        match (&operation.summary, &operation.description) {
57            (Some(summary), Some(desc)) => {
58                format!(
59                    "{}\n\n{}\n\nEndpoint: {} {}",
60                    summary,
61                    desc,
62                    method.to_uppercase(),
63                    path
64                )
65            }
66            (Some(summary), None) => {
67                format!(
68                    "{}\n\nEndpoint: {} {}",
69                    summary,
70                    method.to_uppercase(),
71                    path
72                )
73            }
74            (None, Some(desc)) => {
75                format!("{}\n\nEndpoint: {} {}", desc, method.to_uppercase(), path)
76            }
77            (None, None) => {
78                format!("API endpoint: {} {}", method.to_uppercase(), path)
79            }
80        }
81    }
82
83    /// Resolve a $ref reference to get the actual schema
84    ///
85    /// # Arguments
86    /// * `ref_path` - The reference path (e.g., "#/components/schemas/Pet")
87    /// * `spec` - The OpenAPI specification
88    /// * `visited` - Set of already visited references to detect circular references
89    ///
90    /// # Returns
91    /// The resolved ObjectSchema or an error if the reference is invalid or circular
92    fn resolve_reference(
93        ref_path: &str,
94        spec: &Spec,
95        visited: &mut HashSet<String>,
96    ) -> Result<ObjectSchema, OpenApiError> {
97        // Check for circular reference
98        if visited.contains(ref_path) {
99            return Err(OpenApiError::ToolGeneration(format!(
100                "Circular reference detected: {ref_path}"
101            )));
102        }
103
104        // Add to visited set
105        visited.insert(ref_path.to_string());
106
107        // Parse the reference path
108        // Currently only supporting local references like "#/components/schemas/Pet"
109        if !ref_path.starts_with("#/components/schemas/") {
110            return Err(OpenApiError::ToolGeneration(format!(
111                "Unsupported reference format: {ref_path}. Only #/components/schemas/ references are supported"
112            )));
113        }
114
115        let schema_name = ref_path.strip_prefix("#/components/schemas/").unwrap();
116
117        // Get the schema from components
118        let components = spec.components.as_ref().ok_or_else(|| {
119            OpenApiError::ToolGeneration(format!(
120                "Reference {ref_path} points to components, but spec has no components section"
121            ))
122        })?;
123
124        let schema_ref = components.schemas.get(schema_name).ok_or_else(|| {
125            OpenApiError::ToolGeneration(format!(
126                "Schema '{schema_name}' not found in components/schemas"
127            ))
128        })?;
129
130        // Resolve the schema reference
131        let resolved_schema = match schema_ref {
132            ObjectOrReference::Object(obj_schema) => obj_schema.clone(),
133            ObjectOrReference::Ref {
134                ref_path: nested_ref,
135            } => {
136                // Recursively resolve nested references
137                Self::resolve_reference(nested_ref, spec, visited)?
138            }
139        };
140
141        // Remove from visited set before returning (for other resolution paths)
142        visited.remove(ref_path);
143
144        Ok(resolved_schema)
145    }
146
147    /// Generate JSON Schema for tool parameters
148    fn generate_parameter_schema(
149        parameters: &[ObjectOrReference<Parameter>],
150        _method: &str,
151        request_body: &Option<ObjectOrReference<RequestBody>>,
152        spec: &Spec,
153    ) -> Result<Value, OpenApiError> {
154        let mut properties = serde_json::Map::new();
155        let mut required = Vec::new();
156
157        // Group parameters by location
158        let mut path_params = Vec::new();
159        let mut query_params = Vec::new();
160        let mut header_params = Vec::new();
161        let mut cookie_params = Vec::new();
162
163        for param_ref in parameters {
164            let param = match param_ref {
165                ObjectOrReference::Object(param) => param,
166                ObjectOrReference::Ref { ref_path } => {
167                    // Try to resolve parameter reference
168                    // Note: Parameter references are rare and not supported yet in this implementation
169                    // For now, we'll continue to skip them but log a warning
170                    eprintln!("Warning: Parameter reference not resolved: {ref_path}");
171                    continue;
172                }
173            };
174
175            match &param.location {
176                ParameterIn::Query => query_params.push(param),
177                ParameterIn::Header => header_params.push(param),
178                ParameterIn::Path => path_params.push(param),
179                ParameterIn::Cookie => cookie_params.push(param),
180            }
181        }
182
183        // Process path parameters (always required)
184        for param in path_params {
185            let param_schema = Self::convert_parameter_schema(param, "path", spec)?;
186            properties.insert(param.name.clone(), param_schema);
187            required.push(param.name.clone());
188        }
189
190        // Process query parameters
191        for param in &query_params {
192            let param_schema = Self::convert_parameter_schema(param, "query", spec)?;
193            properties.insert(param.name.clone(), param_schema);
194            if param.required.unwrap_or(false) {
195                required.push(param.name.clone());
196            }
197        }
198
199        // Process header parameters (optional by default unless explicitly required)
200        for param in &header_params {
201            let mut param_schema = Self::convert_parameter_schema(param, "header", spec)?;
202
203            // Add location metadata for headers
204            if let Value::Object(ref mut obj) = param_schema {
205                obj.insert("x-location".to_string(), json!("header"));
206            }
207
208            properties.insert(format!("header_{}", param.name), param_schema);
209            if param.required.unwrap_or(false) {
210                required.push(format!("header_{}", param.name));
211            }
212        }
213
214        // Process cookie parameters (rare, but supported)
215        for param in &cookie_params {
216            let mut param_schema = Self::convert_parameter_schema(param, "cookie", spec)?;
217
218            // Add location metadata for cookies
219            if let Value::Object(ref mut obj) = param_schema {
220                obj.insert("x-location".to_string(), json!("cookie"));
221            }
222
223            properties.insert(format!("cookie_{}", param.name), param_schema);
224            if param.required.unwrap_or(false) {
225                required.push(format!("cookie_{}", param.name));
226            }
227        }
228
229        // Add request body parameter if defined in the OpenAPI spec
230        if let Some(request_body) = request_body {
231            if let Some((body_schema, is_required)) =
232                Self::convert_request_body_to_json_schema(request_body, spec)?
233            {
234                properties.insert("request_body".to_string(), body_schema);
235                if is_required {
236                    required.push("request_body".to_string());
237                }
238            }
239        }
240
241        // Add special parameters for request configuration
242        if !query_params.is_empty() || !header_params.is_empty() || !cookie_params.is_empty() {
243            // Add optional timeout parameter
244            properties.insert(
245                "timeout_seconds".to_string(),
246                json!({
247                    "type": "integer",
248                    "description": "Request timeout in seconds",
249                    "minimum": 1,
250                    "maximum": 300,
251                    "default": 30
252                }),
253            );
254        }
255
256        Ok(json!({
257            "type": "object",
258            "properties": properties,
259            "required": required,
260            "additionalProperties": false
261        }))
262    }
263
264    /// Convert `OpenAPI` parameter schema to JSON Schema for MCP tools
265    fn convert_parameter_schema(
266        param: &Parameter,
267        location: &str,
268        spec: &Spec,
269    ) -> Result<Value, OpenApiError> {
270        let mut result = serde_json::Map::new();
271
272        // Handle the parameter schema
273        if let Some(schema_ref) = &param.schema {
274            match schema_ref {
275                ObjectOrReference::Object(obj_schema) => {
276                    Self::convert_schema_to_json_schema(
277                        &Schema::Object(Box::new(ObjectOrReference::Object(obj_schema.clone()))),
278                        &mut result,
279                        spec,
280                    )?;
281                }
282                ObjectOrReference::Ref { ref_path } => {
283                    // Resolve the reference and convert to JSON schema
284                    let mut visited = HashSet::new();
285                    match Self::resolve_reference(ref_path, spec, &mut visited) {
286                        Ok(resolved_schema) => {
287                            Self::convert_schema_to_json_schema(
288                                &Schema::Object(Box::new(ObjectOrReference::Object(
289                                    resolved_schema,
290                                ))),
291                                &mut result,
292                                spec,
293                            )?;
294                        }
295                        Err(_) => {
296                            // Fallback to string for unresolvable references
297                            result.insert("type".to_string(), json!("string"));
298                        }
299                    }
300                }
301            }
302        } else {
303            // Default to string if no schema
304            result.insert("type".to_string(), json!("string"));
305        }
306
307        // Add description
308        if let Some(desc) = &param.description {
309            result.insert("description".to_string(), json!(desc));
310        } else {
311            result.insert(
312                "description".to_string(),
313                json!(format!("{} parameter", param.name)),
314            );
315        }
316
317        // Add parameter location metadata
318        result.insert("x-parameter-location".to_string(), json!(location));
319        result.insert("x-parameter-required".to_string(), json!(param.required));
320
321        Ok(Value::Object(result))
322    }
323
324    /// Converts prefixItems (tuple-like arrays) to JSON Schema draft-07 compatible format.
325    ///
326    /// This handles OpenAPI 3.1 prefixItems which define specific schemas for each array position,
327    /// converting them to draft-07 format that MCP tools can understand.
328    ///
329    /// Conversion strategy:
330    /// - If items is `false`, set minItems=maxItems=prefix_items.len() for exact length
331    /// - If all prefixItems have same type, use that type for items
332    /// - If mixed types, use oneOf with all unique types from prefixItems
333    /// - Add descriptive comment about tuple nature
334    fn convert_prefix_items_to_draft07(
335        prefix_items: &[ObjectOrReference<ObjectSchema>],
336        items: &Option<Box<Schema>>,
337        result: &mut serde_json::Map<String, Value>,
338        spec: &Spec,
339    ) -> Result<(), OpenApiError> {
340        let prefix_count = prefix_items.len();
341
342        // Extract types from prefixItems
343        let mut item_types = Vec::new();
344        for prefix_item in prefix_items {
345            match prefix_item {
346                ObjectOrReference::Object(obj_schema) => {
347                    if let Some(schema_type) = &obj_schema.schema_type {
348                        match schema_type {
349                            SchemaTypeSet::Single(SchemaType::String) => item_types.push("string"),
350                            SchemaTypeSet::Single(SchemaType::Integer) => {
351                                item_types.push("integer")
352                            }
353                            SchemaTypeSet::Single(SchemaType::Number) => item_types.push("number"),
354                            SchemaTypeSet::Single(SchemaType::Boolean) => {
355                                item_types.push("boolean")
356                            }
357                            SchemaTypeSet::Single(SchemaType::Array) => item_types.push("array"),
358                            SchemaTypeSet::Single(SchemaType::Object) => item_types.push("object"),
359                            _ => item_types.push("string"), // fallback
360                        }
361                    } else {
362                        item_types.push("string"); // fallback
363                    }
364                }
365                ObjectOrReference::Ref { ref_path } => {
366                    // Try to resolve the reference
367                    let mut visited = HashSet::new();
368                    match Self::resolve_reference(ref_path, spec, &mut visited) {
369                        Ok(resolved_schema) => {
370                            // Extract the type immediately and store it as a string
371                            if let Some(schema_type_set) = &resolved_schema.schema_type {
372                                match schema_type_set {
373                                    SchemaTypeSet::Single(SchemaType::String) => {
374                                        item_types.push("string")
375                                    }
376                                    SchemaTypeSet::Single(SchemaType::Integer) => {
377                                        item_types.push("integer")
378                                    }
379                                    SchemaTypeSet::Single(SchemaType::Number) => {
380                                        item_types.push("number")
381                                    }
382                                    SchemaTypeSet::Single(SchemaType::Boolean) => {
383                                        item_types.push("boolean")
384                                    }
385                                    SchemaTypeSet::Single(SchemaType::Array) => {
386                                        item_types.push("array")
387                                    }
388                                    SchemaTypeSet::Single(SchemaType::Object) => {
389                                        item_types.push("object")
390                                    }
391                                    _ => item_types.push("string"), // fallback
392                                }
393                            } else {
394                                item_types.push("string"); // fallback
395                            }
396                        }
397                        Err(_) => {
398                            // Fallback to string for unresolvable references
399                            item_types.push("string");
400                        }
401                    }
402                }
403            }
404        }
405
406        // Check if items is false (no additional items allowed)
407        let items_is_false =
408            matches!(items.as_ref().map(|i| i.as_ref()), Some(Schema::Boolean(b)) if !b.0);
409
410        if items_is_false {
411            // Exact array length required
412            result.insert("minItems".to_string(), json!(prefix_count));
413            result.insert("maxItems".to_string(), json!(prefix_count));
414        }
415
416        // Determine items schema based on prefixItems types
417        let unique_types: std::collections::HashSet<_> = item_types.into_iter().collect();
418
419        if unique_types.len() == 1 {
420            // All items have same type
421            let item_type = unique_types.into_iter().next().unwrap();
422            result.insert("items".to_string(), json!({"type": item_type}));
423        } else if unique_types.len() > 1 {
424            // Mixed types, use oneOf
425            let one_of: Vec<Value> = unique_types
426                .into_iter()
427                .map(|t| json!({"type": t}))
428                .collect();
429            result.insert("items".to_string(), json!({"oneOf": one_of}));
430        }
431
432        Ok(())
433    }
434
435    /// Converts the new oas3 Schema enum (which can be Boolean or Object) to draft-07 format.
436    ///
437    /// The oas3 crate now supports:
438    /// - Schema::Object(ObjectOrReference<ObjectSchema>) - regular object schemas
439    /// - Schema::Boolean(BooleanSchema) - true/false schemas for validation control
440    ///
441    /// For MCP compatibility (draft-07), we convert:
442    /// - Boolean true -> allow any items (no items constraint)
443    /// - Boolean false -> not handled here (should be handled by caller with array constraints)
444    /// - Object schemas -> recursively convert to JSON Schema
445    fn convert_items_schema_to_draft07(
446        items_schema: &Schema,
447        result: &mut serde_json::Map<String, Value>,
448        spec: &Spec,
449    ) -> Result<(), OpenApiError> {
450        match items_schema {
451            Schema::Boolean(boolean_schema) => {
452                if boolean_schema.0 {
453                    // items: true - allow any additional items (draft-07 default behavior)
454                    // Don't set items constraint, which allows any items
455                } else {
456                    // items: false - no additional items allowed
457                    // This should typically be handled in combination with prefixItems
458                    // but if we see it alone, we set a restrictive constraint
459                    result.insert("maxItems".to_string(), json!(0));
460                }
461            }
462            Schema::Object(obj_ref) => match obj_ref.as_ref() {
463                ObjectOrReference::Object(item_schema) => {
464                    let mut items_result = serde_json::Map::new();
465                    Self::convert_schema_to_json_schema(
466                        &Schema::Object(Box::new(ObjectOrReference::Object(item_schema.clone()))),
467                        &mut items_result,
468                        spec,
469                    )?;
470                    result.insert("items".to_string(), Value::Object(items_result));
471                }
472                ObjectOrReference::Ref { ref_path } => {
473                    // Try to resolve reference and convert to JSON schema
474                    let mut visited = HashSet::new();
475                    match Self::resolve_reference(ref_path, spec, &mut visited) {
476                        Ok(resolved_schema) => {
477                            let mut items_result = serde_json::Map::new();
478                            Self::convert_schema_to_json_schema(
479                                &Schema::Object(Box::new(ObjectOrReference::Object(
480                                    resolved_schema,
481                                ))),
482                                &mut items_result,
483                                spec,
484                            )?;
485                            result.insert("items".to_string(), Value::Object(items_result));
486                        }
487                        Err(_) => {
488                            // Fallback to string type for unresolvable references
489                            result.insert("items".to_string(), json!({"type": "string"}));
490                        }
491                    }
492                }
493            },
494        }
495        Ok(())
496    }
497
498    /// Convert request body from OpenAPI to JSON Schema for MCP tools
499    fn convert_request_body_to_json_schema(
500        request_body_ref: &ObjectOrReference<RequestBody>,
501        spec: &Spec,
502    ) -> Result<Option<(Value, bool)>, OpenApiError> {
503        match request_body_ref {
504            ObjectOrReference::Object(request_body) => {
505                // Extract schema from request body content
506                // Prioritize application/json content type
507                let schema_info = request_body
508                    .content
509                    .get(mime::APPLICATION_JSON.as_ref())
510                    .or_else(|| request_body.content.get("application/json"))
511                    .or_else(|| {
512                        // Fall back to first available content type
513                        request_body.content.values().next()
514                    });
515
516                if let Some(media_type) = schema_info {
517                    if let Some(schema_ref) = &media_type.schema {
518                        let mut result = serde_json::Map::new();
519
520                        // Convert the schema to JSON Schema
521                        match schema_ref {
522                            ObjectOrReference::Object(obj_schema) => {
523                                Self::convert_schema_to_json_schema(
524                                    &Schema::Object(Box::new(ObjectOrReference::Object(
525                                        obj_schema.clone(),
526                                    ))),
527                                    &mut result,
528                                    spec,
529                                )?;
530                            }
531                            ObjectOrReference::Ref { ref_path } => {
532                                // Resolve the reference and convert to JSON schema
533                                let mut visited = HashSet::new();
534                                match Self::resolve_reference(ref_path, spec, &mut visited) {
535                                    Ok(resolved_schema) => {
536                                        Self::convert_schema_to_json_schema(
537                                            &Schema::Object(Box::new(ObjectOrReference::Object(
538                                                resolved_schema,
539                                            ))),
540                                            &mut result,
541                                            spec,
542                                        )?;
543                                    }
544                                    Err(_) => {
545                                        // Fallback to generic object for unresolvable references
546                                        result.insert("type".to_string(), json!("object"));
547                                        result.insert(
548                                            "additionalProperties".to_string(),
549                                            json!(true),
550                                        );
551                                    }
552                                }
553                            }
554                        }
555
556                        // Add description if available
557                        if let Some(desc) = &request_body.description {
558                            result.insert("description".to_string(), json!(desc));
559                        } else {
560                            result.insert("description".to_string(), json!("Request body data"));
561                        }
562
563                        // Add metadata
564                        result.insert("x-location".to_string(), json!("body"));
565                        result.insert(
566                            "x-content-type".to_string(),
567                            json!(mime::APPLICATION_JSON.as_ref()),
568                        );
569
570                        let required = request_body.required.unwrap_or(false);
571                        Ok(Some((Value::Object(result), required)))
572                    } else {
573                        Ok(None)
574                    }
575                } else {
576                    Ok(None)
577                }
578            }
579            ObjectOrReference::Ref { .. } => {
580                // For references, return a generic object schema
581                let mut result = serde_json::Map::new();
582                result.insert("type".to_string(), json!("object"));
583                result.insert("additionalProperties".to_string(), json!(true));
584                result.insert("description".to_string(), json!("Request body data"));
585                result.insert("x-location".to_string(), json!("body"));
586                result.insert(
587                    "x-content-type".to_string(),
588                    json!(mime::APPLICATION_JSON.as_ref()),
589                );
590
591                Ok(Some((Value::Object(result), false)))
592            }
593        }
594    }
595
596    /// Convert `oas3::Schema` to JSON Schema properties
597    fn convert_schema_to_json_schema(
598        schema: &Schema,
599        result: &mut serde_json::Map<String, Value>,
600        spec: &Spec,
601    ) -> Result<(), OpenApiError> {
602        match schema {
603            Schema::Object(obj_schema_ref) => match obj_schema_ref.as_ref() {
604                ObjectOrReference::Object(obj_schema) => {
605                    // Handle object schema type
606                    if let Some(schema_type) = &obj_schema.schema_type {
607                        match schema_type {
608                            SchemaTypeSet::Single(SchemaType::String) => {
609                                result.insert("type".to_string(), json!("string"));
610                                if let Some(min_length) = obj_schema.min_length {
611                                    result.insert("minLength".to_string(), json!(min_length));
612                                }
613                                if let Some(max_length) = obj_schema.max_length {
614                                    result.insert("maxLength".to_string(), json!(max_length));
615                                }
616                                if let Some(pattern) = &obj_schema.pattern {
617                                    result.insert("pattern".to_string(), json!(pattern));
618                                }
619                                if let Some(format) = &obj_schema.format {
620                                    result.insert("format".to_string(), json!(format));
621                                }
622                            }
623                            SchemaTypeSet::Single(SchemaType::Number) => {
624                                result.insert("type".to_string(), json!("number"));
625                                if let Some(minimum) = &obj_schema.minimum {
626                                    result.insert("minimum".to_string(), json!(minimum));
627                                }
628                                if let Some(maximum) = &obj_schema.maximum {
629                                    result.insert("maximum".to_string(), json!(maximum));
630                                }
631                                if let Some(format) = &obj_schema.format {
632                                    result.insert("format".to_string(), json!(format));
633                                }
634                            }
635                            SchemaTypeSet::Single(SchemaType::Integer) => {
636                                result.insert("type".to_string(), json!("integer"));
637                                if let Some(minimum) = &obj_schema.minimum {
638                                    result.insert("minimum".to_string(), json!(minimum));
639                                }
640                                if let Some(maximum) = &obj_schema.maximum {
641                                    result.insert("maximum".to_string(), json!(maximum));
642                                }
643                                if let Some(format) = &obj_schema.format {
644                                    result.insert("format".to_string(), json!(format));
645                                }
646                            }
647                            SchemaTypeSet::Single(SchemaType::Boolean) => {
648                                result.insert("type".to_string(), json!("boolean"));
649                            }
650                            SchemaTypeSet::Single(SchemaType::Array) => {
651                                result.insert("type".to_string(), json!("array"));
652
653                                // Handle modern JSON Schema features (prefixItems + items:false)
654                                // and convert them to JSON Schema draft-07 compatible format for MCP tools.
655                                //
656                                // MCP uses JSON Schema draft-07 which doesn't support:
657                                // - prefixItems (introduced in draft 2020-12)
658                                // - items: false (boolean schemas introduced in draft 2019-09)
659                                //
660                                // We convert these to draft-07 equivalents:
661                                // - Use minItems/maxItems for exact array length constraints
662                                // - Convert prefixItems to regular items schema with oneOf if needed
663                                // - Document tuple nature in description
664
665                                if !obj_schema.prefix_items.is_empty() {
666                                    // Handle prefixItems (tuple-like arrays)
667                                    Self::convert_prefix_items_to_draft07(
668                                        &obj_schema.prefix_items,
669                                        &obj_schema.items,
670                                        result,
671                                        spec,
672                                    )?;
673                                } else if let Some(items) = &obj_schema.items {
674                                    // Handle regular items field (now a Schema enum)
675                                    Self::convert_items_schema_to_draft07(items, result, spec)?;
676                                } else {
677                                    // No items specified, default to accepting any items
678                                    result.insert("items".to_string(), json!({"type": "string"}));
679                                }
680                            }
681                            SchemaTypeSet::Single(SchemaType::Object) => {
682                                result.insert("type".to_string(), json!("object"));
683
684                                // Convert properties if present
685                                if !obj_schema.properties.is_empty() {
686                                    let mut properties_map = serde_json::Map::new();
687                                    for (prop_name, prop_schema) in &obj_schema.properties {
688                                        let mut prop_result = serde_json::Map::new();
689                                        match prop_schema {
690                                            ObjectOrReference::Object(prop_obj_schema) => {
691                                                Self::convert_schema_to_json_schema(
692                                                    &Schema::Object(Box::new(
693                                                        ObjectOrReference::Object(
694                                                            prop_obj_schema.clone(),
695                                                        ),
696                                                    )),
697                                                    &mut prop_result,
698                                                    spec,
699                                                )?;
700                                            }
701                                            ObjectOrReference::Ref { ref_path } => {
702                                                // Try to resolve reference and convert to JSON schema
703                                                let mut visited = HashSet::new();
704                                                match Self::resolve_reference(
705                                                    ref_path,
706                                                    spec,
707                                                    &mut visited,
708                                                ) {
709                                                    Ok(resolved_schema) => {
710                                                        Self::convert_schema_to_json_schema(
711                                                            &Schema::Object(Box::new(
712                                                                ObjectOrReference::Object(
713                                                                    resolved_schema,
714                                                                ),
715                                                            )),
716                                                            &mut prop_result,
717                                                            spec,
718                                                        )?;
719                                                    }
720                                                    Err(_) => {
721                                                        // Fallback to string for unresolvable references
722                                                        prop_result.insert(
723                                                            "type".to_string(),
724                                                            json!("string"),
725                                                        );
726                                                    }
727                                                }
728                                            }
729                                        }
730                                        properties_map
731                                            .insert(prop_name.clone(), Value::Object(prop_result));
732                                    }
733                                    result.insert(
734                                        "properties".to_string(),
735                                        Value::Object(properties_map),
736                                    );
737
738                                    // Only set additionalProperties to false if we have explicit properties
739                                    result.insert("additionalProperties".to_string(), json!(false));
740                                } else {
741                                    // No properties defined, allow additional properties
742                                    result.insert("additionalProperties".to_string(), json!(true));
743                                }
744
745                                // Add required array if present
746                                if !obj_schema.required.is_empty() {
747                                    result
748                                        .insert("required".to_string(), json!(obj_schema.required));
749                                }
750                            }
751                            _ => {
752                                // Default for other types
753                                result.insert("type".to_string(), json!("string"));
754                            }
755                        }
756                    } else {
757                        // Default to object if no type specified
758                        result.insert("type".to_string(), json!("object"));
759                    }
760                }
761                ObjectOrReference::Ref { ref_path } => {
762                    // Try to resolve reference and convert to JSON schema
763                    let mut visited = HashSet::new();
764                    match Self::resolve_reference(ref_path, spec, &mut visited) {
765                        Ok(resolved_schema) => {
766                            Self::convert_schema_to_json_schema(
767                                &Schema::Object(Box::new(ObjectOrReference::Object(
768                                    resolved_schema,
769                                ))),
770                                result,
771                                spec,
772                            )?;
773                        }
774                        Err(_) => {
775                            // Fallback to string for unresolvable references
776                            result.insert("type".to_string(), json!("string"));
777                        }
778                    }
779                }
780            },
781            Schema::Boolean(_) => {
782                // Boolean schema - allow any type
783                result.insert("type".to_string(), json!("object"));
784            }
785        }
786
787        Ok(())
788    }
789
790    /// Extract parameter values from MCP tool call arguments
791    ///
792    /// # Errors
793    ///
794    /// Returns an error if the arguments are invalid or missing required parameters
795    pub fn extract_parameters(
796        tool_metadata: &ToolMetadata,
797        arguments: &Value,
798    ) -> Result<ExtractedParameters, OpenApiError> {
799        let args = arguments
800            .as_object()
801            .ok_or_else(|| OpenApiError::Validation("Arguments must be an object".to_string()))?;
802
803        let mut path_params = HashMap::new();
804        let mut query_params = HashMap::new();
805        let mut header_params = HashMap::new();
806        let mut cookie_params = HashMap::new();
807        let mut body_params = HashMap::new();
808        let mut config = RequestConfig::default();
809
810        // Extract timeout if provided
811        if let Some(timeout) = args.get("timeout_seconds").and_then(Value::as_u64) {
812            config.timeout_seconds = u32::try_from(timeout).unwrap_or(u32::MAX);
813        }
814
815        // Process each argument
816        for (key, value) in args {
817            if key == "timeout_seconds" {
818                continue; // Already processed
819            }
820
821            // Handle special request_body parameter
822            if key == "request_body" {
823                body_params.insert("request_body".to_string(), value.clone());
824                continue;
825            }
826
827            // Determine parameter location from the tool metadata
828            let location = Self::get_parameter_location(tool_metadata, key)?;
829
830            match location.as_str() {
831                "path" => {
832                    path_params.insert(key.clone(), value.clone());
833                }
834                "query" => {
835                    query_params.insert(key.clone(), value.clone());
836                }
837                "header" => {
838                    // Remove "header_" prefix if present
839                    let header_name = if key.starts_with("header_") {
840                        key.strip_prefix("header_").unwrap_or(key).to_string()
841                    } else {
842                        key.clone()
843                    };
844                    header_params.insert(header_name, value.clone());
845                }
846                "cookie" => {
847                    // Remove "cookie_" prefix if present
848                    let cookie_name = if key.starts_with("cookie_") {
849                        key.strip_prefix("cookie_").unwrap_or(key).to_string()
850                    } else {
851                        key.clone()
852                    };
853                    cookie_params.insert(cookie_name, value.clone());
854                }
855                "body" => {
856                    // Remove "body_" prefix if present
857                    let body_name = if key.starts_with("body_") {
858                        key.strip_prefix("body_").unwrap_or(key).to_string()
859                    } else {
860                        key.clone()
861                    };
862                    body_params.insert(body_name, value.clone());
863                }
864                _ => {
865                    return Err(OpenApiError::ToolGeneration(format!(
866                        "Unknown parameter location for parameter: {key}"
867                    )));
868                }
869            }
870        }
871
872        let extracted = ExtractedParameters {
873            path: path_params,
874            query: query_params,
875            headers: header_params,
876            cookies: cookie_params,
877            body: body_params,
878            config,
879        };
880
881        // Validate parameters against tool metadata
882        Self::validate_parameters(tool_metadata, &extracted)?;
883
884        Ok(extracted)
885    }
886
887    /// Get parameter location from tool metadata
888    fn get_parameter_location(
889        tool_metadata: &ToolMetadata,
890        param_name: &str,
891    ) -> Result<String, OpenApiError> {
892        let properties = tool_metadata
893            .parameters
894            .get("properties")
895            .and_then(|p| p.as_object())
896            .ok_or_else(|| {
897                OpenApiError::ToolGeneration("Invalid tool parameters schema".to_string())
898            })?;
899
900        if let Some(param_schema) = properties.get(param_name) {
901            if let Some(location) = param_schema
902                .get("x-parameter-location")
903                .and_then(|v| v.as_str())
904            {
905                return Ok(location.to_string());
906            }
907        }
908
909        // Fallback: infer from parameter name prefix
910        if param_name.starts_with("header_") {
911            Ok("header".to_string())
912        } else if param_name.starts_with("cookie_") {
913            Ok("cookie".to_string())
914        } else if param_name.starts_with("body_") {
915            Ok("body".to_string())
916        } else {
917            // Default to query for unknown parameters
918            Ok("query".to_string())
919        }
920    }
921
922    /// Validate extracted parameters against tool metadata
923    fn validate_parameters(
924        tool_metadata: &ToolMetadata,
925        extracted: &ExtractedParameters,
926    ) -> Result<(), OpenApiError> {
927        let schema = &tool_metadata.parameters;
928
929        // Get required parameters from schema
930        let required_params = schema
931            .get("required")
932            .and_then(|r| r.as_array())
933            .map(|arr| {
934                arr.iter()
935                    .filter_map(|v| v.as_str())
936                    .collect::<std::collections::HashSet<_>>()
937            })
938            .unwrap_or_default();
939
940        let _properties = schema
941            .get("properties")
942            .and_then(|p| p.as_object())
943            .ok_or_else(|| {
944                OpenApiError::Validation("Tool schema missing properties".to_string())
945            })?;
946
947        // Check all required parameters are provided
948        for required_param in &required_params {
949            let param_found = extracted.path.contains_key(*required_param)
950                || extracted.query.contains_key(*required_param)
951                || extracted
952                    .headers
953                    .contains_key(&required_param.replace("header_", ""))
954                || extracted
955                    .cookies
956                    .contains_key(&required_param.replace("cookie_", ""))
957                || extracted
958                    .body
959                    .contains_key(&required_param.replace("body_", ""))
960                || (*required_param == "request_body"
961                    && extracted.body.contains_key("request_body"));
962
963            if !param_found {
964                return Err(OpenApiError::InvalidParameter {
965                    parameter: (*required_param).to_string(),
966                    reason: "Required parameter is missing".to_string(),
967                });
968            }
969        }
970
971        Ok(())
972    }
973}
974
975/// Extracted parameters from MCP tool call
976#[derive(Debug, Clone)]
977pub struct ExtractedParameters {
978    pub path: HashMap<String, Value>,
979    pub query: HashMap<String, Value>,
980    pub headers: HashMap<String, Value>,
981    pub cookies: HashMap<String, Value>,
982    pub body: HashMap<String, Value>,
983    pub config: RequestConfig,
984}
985
986/// Request configuration options
987#[derive(Debug, Clone)]
988pub struct RequestConfig {
989    pub timeout_seconds: u32,
990    pub content_type: String,
991}
992
993impl Default for RequestConfig {
994    fn default() -> Self {
995        Self {
996            timeout_seconds: 30,
997            content_type: mime::APPLICATION_JSON.to_string(),
998        }
999    }
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004    use super::*;
1005    use oas3::spec::{
1006        BooleanSchema, Components, MediaType, ObjectOrReference, ObjectSchema, Operation,
1007        Parameter, ParameterIn, RequestBody, SchemaType, SchemaTypeSet, Spec,
1008    };
1009    use serde_json::{Value, json};
1010    use std::collections::BTreeMap;
1011
1012    /// Create a minimal test OpenAPI spec for testing purposes
1013    fn create_test_spec() -> Spec {
1014        Spec {
1015            openapi: "3.0.0".to_string(),
1016            info: oas3::spec::Info {
1017                title: "Test API".to_string(),
1018                version: "1.0.0".to_string(),
1019                summary: None,
1020                description: Some("Test API for unit tests".to_string()),
1021                terms_of_service: None,
1022                contact: None,
1023                license: None,
1024                extensions: Default::default(),
1025            },
1026            components: Some(Components {
1027                schemas: BTreeMap::new(),
1028                responses: BTreeMap::new(),
1029                parameters: BTreeMap::new(),
1030                examples: BTreeMap::new(),
1031                request_bodies: BTreeMap::new(),
1032                headers: BTreeMap::new(),
1033                security_schemes: BTreeMap::new(),
1034                links: BTreeMap::new(),
1035                callbacks: BTreeMap::new(),
1036                path_items: BTreeMap::new(),
1037                extensions: Default::default(),
1038            }),
1039            servers: vec![],
1040            paths: None,
1041            external_docs: None,
1042            tags: vec![],
1043            security: vec![],
1044            webhooks: BTreeMap::new(),
1045            extensions: Default::default(),
1046        }
1047    }
1048
1049    fn validate_tool_against_mcp_schema(metadata: &ToolMetadata) {
1050        let schema_content = std::fs::read_to_string("schema/2025-03-26/schema.json")
1051            .expect("Failed to read MCP schema file");
1052        let full_schema: Value =
1053            serde_json::from_str(&schema_content).expect("Failed to parse MCP schema JSON");
1054
1055        // Create a schema that references the Tool definition from the full schema
1056        let tool_schema = json!({
1057            "$schema": "http://json-schema.org/draft-07/schema#",
1058            "definitions": full_schema.get("definitions"),
1059            "$ref": "#/definitions/Tool"
1060        });
1061
1062        let validator =
1063            jsonschema::validator_for(&tool_schema).expect("Failed to compile MCP Tool schema");
1064
1065        // Convert ToolMetadata to MCP Tool format
1066        let mcp_tool = json!({
1067            "name": metadata.name,
1068            "description": metadata.description,
1069            "inputSchema": metadata.parameters
1070        });
1071
1072        // Validate the generated tool against MCP schema
1073        let errors: Vec<String> = validator
1074            .iter_errors(&mcp_tool)
1075            .map(|e| e.to_string())
1076            .collect();
1077
1078        if !errors.is_empty() {
1079            panic!("Generated tool failed MCP schema validation: {errors:?}");
1080        }
1081    }
1082
1083    #[test]
1084    fn test_petstore_get_pet_by_id() {
1085        let mut operation = Operation {
1086            operation_id: Some("getPetById".to_string()),
1087            summary: Some("Find pet by ID".to_string()),
1088            description: Some("Returns a single pet".to_string()),
1089            tags: vec![],
1090            external_docs: None,
1091            parameters: vec![],
1092            request_body: None,
1093            responses: Default::default(),
1094            callbacks: Default::default(),
1095            deprecated: Some(false),
1096            security: vec![],
1097            servers: vec![],
1098            extensions: Default::default(),
1099        };
1100
1101        // Create a path parameter
1102        let param = Parameter {
1103            name: "petId".to_string(),
1104            location: ParameterIn::Path,
1105            description: Some("ID of pet to return".to_string()),
1106            required: Some(true),
1107            deprecated: Some(false),
1108            allow_empty_value: Some(false),
1109            style: None,
1110            explode: None,
1111            allow_reserved: Some(false),
1112            schema: Some(ObjectOrReference::Object(ObjectSchema {
1113                schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
1114                minimum: Some(serde_json::Number::from(1_i64)),
1115                format: Some("int64".to_string()),
1116                ..Default::default()
1117            })),
1118            example: None,
1119            examples: Default::default(),
1120            content: None,
1121            extensions: Default::default(),
1122        };
1123
1124        operation.parameters.push(ObjectOrReference::Object(param));
1125
1126        let spec = create_test_spec();
1127        let metadata = ToolGenerator::generate_tool_metadata(
1128            &operation,
1129            "get".to_string(),
1130            "/pet/{petId}".to_string(),
1131            &spec,
1132        )
1133        .unwrap();
1134
1135        assert_eq!(metadata.name, "getPetById");
1136        assert_eq!(metadata.method, "get");
1137        assert_eq!(metadata.path, "/pet/{petId}");
1138        assert!(metadata.description.contains("Find pet by ID"));
1139
1140        // Validate against MCP Tool schema
1141        validate_tool_against_mcp_schema(&metadata);
1142    }
1143
1144    #[test]
1145    fn test_convert_prefix_items_to_draft07_mixed_types() {
1146        // Test prefixItems with mixed types and items:false
1147
1148        let prefix_items = vec![
1149            ObjectOrReference::Object(ObjectSchema {
1150                schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
1151                format: Some("int32".to_string()),
1152                ..Default::default()
1153            }),
1154            ObjectOrReference::Object(ObjectSchema {
1155                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
1156                ..Default::default()
1157            }),
1158        ];
1159
1160        // items: false (no additional items allowed)
1161        let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
1162
1163        let mut result = serde_json::Map::new();
1164        let spec = create_test_spec();
1165        ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
1166            .unwrap();
1167
1168        // Should set exact array length
1169        assert_eq!(result.get("minItems"), Some(&json!(2)));
1170        assert_eq!(result.get("maxItems"), Some(&json!(2)));
1171
1172        // Should use oneOf for mixed types
1173        let items_schema = result.get("items").unwrap();
1174        assert!(items_schema.get("oneOf").is_some());
1175        let one_of = items_schema.get("oneOf").unwrap().as_array().unwrap();
1176        assert_eq!(one_of.len(), 2);
1177
1178        // Verify types are present
1179        let types: Vec<&str> = one_of
1180            .iter()
1181            .map(|v| v.get("type").unwrap().as_str().unwrap())
1182            .collect();
1183        assert!(types.contains(&"integer"));
1184        assert!(types.contains(&"string"));
1185    }
1186
1187    #[test]
1188    fn test_convert_prefix_items_to_draft07_uniform_types() {
1189        // Test prefixItems with uniform types
1190        let prefix_items = vec![
1191            ObjectOrReference::Object(ObjectSchema {
1192                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
1193                ..Default::default()
1194            }),
1195            ObjectOrReference::Object(ObjectSchema {
1196                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
1197                ..Default::default()
1198            }),
1199        ];
1200
1201        // items: false
1202        let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
1203
1204        let mut result = serde_json::Map::new();
1205        let spec = create_test_spec();
1206        ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
1207            .unwrap();
1208
1209        // Should set exact array length
1210        assert_eq!(result.get("minItems"), Some(&json!(2)));
1211        assert_eq!(result.get("maxItems"), Some(&json!(2)));
1212
1213        // Should use single type for uniform types
1214        let items_schema = result.get("items").unwrap();
1215        assert_eq!(items_schema.get("type"), Some(&json!("string")));
1216        assert!(items_schema.get("oneOf").is_none());
1217    }
1218
1219    #[test]
1220    fn test_convert_items_schema_to_draft07_boolean_true() {
1221        // Test items: true (allow any additional items)
1222        let items_schema = Schema::Boolean(BooleanSchema(true));
1223        let mut result = serde_json::Map::new();
1224        let spec = create_test_spec();
1225
1226        ToolGenerator::convert_items_schema_to_draft07(&items_schema, &mut result, &spec).unwrap();
1227
1228        // Should not add any constraints (allows any items)
1229        assert!(result.get("items").is_none());
1230        assert!(result.get("maxItems").is_none());
1231    }
1232
1233    #[test]
1234    fn test_convert_items_schema_to_draft07_boolean_false() {
1235        // Test items: false (no additional items allowed)
1236        let items_schema = Schema::Boolean(BooleanSchema(false));
1237        let mut result = serde_json::Map::new();
1238        let spec = create_test_spec();
1239
1240        ToolGenerator::convert_items_schema_to_draft07(&items_schema, &mut result, &spec).unwrap();
1241
1242        // Should set maxItems to 0
1243        assert_eq!(result.get("maxItems"), Some(&json!(0)));
1244    }
1245
1246    #[test]
1247    fn test_convert_items_schema_to_draft07_object_schema() {
1248        // Test items with object schema
1249        let item_schema = ObjectSchema {
1250            schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
1251            minimum: Some(serde_json::Number::from(0)),
1252            ..Default::default()
1253        };
1254
1255        let items_schema = Schema::Object(Box::new(ObjectOrReference::Object(item_schema)));
1256        let mut result = serde_json::Map::new();
1257        let spec = create_test_spec();
1258
1259        ToolGenerator::convert_items_schema_to_draft07(&items_schema, &mut result, &spec).unwrap();
1260
1261        // Should convert to proper items schema
1262        let items_value = result.get("items").unwrap();
1263        assert_eq!(items_value.get("type"), Some(&json!("number")));
1264        assert_eq!(items_value.get("minimum"), Some(&json!(0)));
1265    }
1266
1267    #[test]
1268    fn test_array_with_prefix_items_integration() {
1269        // Integration test: parameter with prefixItems and items:false
1270        let param = Parameter {
1271            name: "coordinates".to_string(),
1272            location: ParameterIn::Query,
1273            description: Some("X,Y coordinates as tuple".to_string()),
1274            required: Some(true),
1275            deprecated: Some(false),
1276            allow_empty_value: Some(false),
1277            style: None,
1278            explode: None,
1279            allow_reserved: Some(false),
1280            schema: Some(ObjectOrReference::Object(ObjectSchema {
1281                schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
1282                prefix_items: vec![
1283                    ObjectOrReference::Object(ObjectSchema {
1284                        schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
1285                        format: Some("double".to_string()),
1286                        ..Default::default()
1287                    }),
1288                    ObjectOrReference::Object(ObjectSchema {
1289                        schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
1290                        format: Some("double".to_string()),
1291                        ..Default::default()
1292                    }),
1293                ],
1294                items: Some(Box::new(Schema::Boolean(BooleanSchema(false)))),
1295                ..Default::default()
1296            })),
1297            example: None,
1298            examples: Default::default(),
1299            content: None,
1300            extensions: Default::default(),
1301        };
1302
1303        let spec = create_test_spec();
1304        let result = ToolGenerator::convert_parameter_schema(&param, "query", &spec).unwrap();
1305
1306        // Verify the result
1307        assert_eq!(result.get("type"), Some(&json!("array")));
1308        assert_eq!(result.get("minItems"), Some(&json!(2)));
1309        assert_eq!(result.get("maxItems"), Some(&json!(2)));
1310        assert_eq!(
1311            result.get("items").unwrap().get("type"),
1312            Some(&json!("number"))
1313        );
1314        assert_eq!(
1315            result.get("description"),
1316            Some(&json!("X,Y coordinates as tuple"))
1317        );
1318    }
1319
1320    #[test]
1321    fn test_array_with_regular_items_schema() {
1322        // Test regular array with object schema items (not boolean)
1323        let param = Parameter {
1324            name: "tags".to_string(),
1325            location: ParameterIn::Query,
1326            description: Some("List of tags".to_string()),
1327            required: Some(false),
1328            deprecated: Some(false),
1329            allow_empty_value: Some(false),
1330            style: None,
1331            explode: None,
1332            allow_reserved: Some(false),
1333            schema: Some(ObjectOrReference::Object(ObjectSchema {
1334                schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
1335                items: Some(Box::new(Schema::Object(Box::new(
1336                    ObjectOrReference::Object(ObjectSchema {
1337                        schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
1338                        min_length: Some(1),
1339                        max_length: Some(50),
1340                        ..Default::default()
1341                    }),
1342                )))),
1343                ..Default::default()
1344            })),
1345            example: None,
1346            examples: Default::default(),
1347            content: None,
1348            extensions: Default::default(),
1349        };
1350
1351        let spec = create_test_spec();
1352        let result = ToolGenerator::convert_parameter_schema(&param, "query", &spec).unwrap();
1353
1354        // Verify the result
1355        assert_eq!(result.get("type"), Some(&json!("array")));
1356        let items = result.get("items").unwrap();
1357        assert_eq!(items.get("type"), Some(&json!("string")));
1358        assert_eq!(items.get("minLength"), Some(&json!(1)));
1359        assert_eq!(items.get("maxLength"), Some(&json!(50)));
1360    }
1361
1362    #[test]
1363    fn test_request_body_object_schema() {
1364        // Test with object request body
1365        let operation = Operation {
1366            operation_id: Some("createPet".to_string()),
1367            summary: Some("Create a new pet".to_string()),
1368            description: Some("Creates a new pet in the store".to_string()),
1369            tags: vec![],
1370            external_docs: None,
1371            parameters: vec![],
1372            request_body: Some(ObjectOrReference::Object(RequestBody {
1373                description: Some("Pet object that needs to be added to the store".to_string()),
1374                content: {
1375                    let mut content = BTreeMap::new();
1376                    content.insert(
1377                        "application/json".to_string(),
1378                        MediaType {
1379                            schema: Some(ObjectOrReference::Object(ObjectSchema {
1380                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
1381                                ..Default::default()
1382                            })),
1383                            examples: None,
1384                            encoding: Default::default(),
1385                        },
1386                    );
1387                    content
1388                },
1389                required: Some(true),
1390            })),
1391            responses: Default::default(),
1392            callbacks: Default::default(),
1393            deprecated: Some(false),
1394            security: vec![],
1395            servers: vec![],
1396            extensions: Default::default(),
1397        };
1398
1399        let spec = create_test_spec();
1400        let metadata = ToolGenerator::generate_tool_metadata(
1401            &operation,
1402            "post".to_string(),
1403            "/pets".to_string(),
1404            &spec,
1405        )
1406        .unwrap();
1407
1408        // Check that request_body is in properties
1409        let properties = metadata
1410            .parameters
1411            .get("properties")
1412            .unwrap()
1413            .as_object()
1414            .unwrap();
1415        assert!(properties.contains_key("request_body"));
1416
1417        // Check that request_body is required
1418        let required = metadata
1419            .parameters
1420            .get("required")
1421            .unwrap()
1422            .as_array()
1423            .unwrap();
1424        assert!(required.contains(&json!("request_body")));
1425
1426        // Check request body schema
1427        let request_body_schema = properties.get("request_body").unwrap();
1428        assert_eq!(request_body_schema.get("type"), Some(&json!("object")));
1429        assert_eq!(
1430            request_body_schema.get("description"),
1431            Some(&json!("Pet object that needs to be added to the store"))
1432        );
1433        assert_eq!(request_body_schema.get("x-location"), Some(&json!("body")));
1434
1435        // Validate against MCP Tool schema
1436        validate_tool_against_mcp_schema(&metadata);
1437    }
1438
1439    #[test]
1440    fn test_request_body_array_schema() {
1441        // Test with array request body
1442        let operation = Operation {
1443            operation_id: Some("createPets".to_string()),
1444            summary: Some("Create multiple pets".to_string()),
1445            description: None,
1446            tags: vec![],
1447            external_docs: None,
1448            parameters: vec![],
1449            request_body: Some(ObjectOrReference::Object(RequestBody {
1450                description: Some("Array of pet objects".to_string()),
1451                content: {
1452                    let mut content = BTreeMap::new();
1453                    content.insert(
1454                        "application/json".to_string(),
1455                        MediaType {
1456                            schema: Some(ObjectOrReference::Object(ObjectSchema {
1457                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
1458                                items: Some(Box::new(Schema::Object(Box::new(
1459                                    ObjectOrReference::Object(ObjectSchema {
1460                                        schema_type: Some(SchemaTypeSet::Single(
1461                                            SchemaType::Object,
1462                                        )),
1463                                        ..Default::default()
1464                                    }),
1465                                )))),
1466                                ..Default::default()
1467                            })),
1468                            examples: None,
1469                            encoding: Default::default(),
1470                        },
1471                    );
1472                    content
1473                },
1474                required: Some(false),
1475            })),
1476            responses: Default::default(),
1477            callbacks: Default::default(),
1478            deprecated: Some(false),
1479            security: vec![],
1480            servers: vec![],
1481            extensions: Default::default(),
1482        };
1483
1484        let spec = create_test_spec();
1485        let metadata = ToolGenerator::generate_tool_metadata(
1486            &operation,
1487            "post".to_string(),
1488            "/pets/batch".to_string(),
1489            &spec,
1490        )
1491        .unwrap();
1492
1493        // Check that request_body is in properties
1494        let properties = metadata
1495            .parameters
1496            .get("properties")
1497            .unwrap()
1498            .as_object()
1499            .unwrap();
1500        assert!(properties.contains_key("request_body"));
1501
1502        // Check that request_body is NOT required (required: false)
1503        let required = metadata
1504            .parameters
1505            .get("required")
1506            .unwrap()
1507            .as_array()
1508            .unwrap();
1509        assert!(!required.contains(&json!("request_body")));
1510
1511        // Check request body schema
1512        let request_body_schema = properties.get("request_body").unwrap();
1513        assert_eq!(request_body_schema.get("type"), Some(&json!("array")));
1514        assert_eq!(
1515            request_body_schema.get("description"),
1516            Some(&json!("Array of pet objects"))
1517        );
1518
1519        // Validate against MCP Tool schema
1520        validate_tool_against_mcp_schema(&metadata);
1521    }
1522
1523    #[test]
1524    fn test_request_body_string_schema() {
1525        // Test with string request body
1526        let operation = Operation {
1527            operation_id: Some("updatePetName".to_string()),
1528            summary: Some("Update pet name".to_string()),
1529            description: None,
1530            tags: vec![],
1531            external_docs: None,
1532            parameters: vec![],
1533            request_body: Some(ObjectOrReference::Object(RequestBody {
1534                description: None,
1535                content: {
1536                    let mut content = BTreeMap::new();
1537                    content.insert(
1538                        "text/plain".to_string(),
1539                        MediaType {
1540                            schema: Some(ObjectOrReference::Object(ObjectSchema {
1541                                schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
1542                                min_length: Some(1),
1543                                max_length: Some(100),
1544                                ..Default::default()
1545                            })),
1546                            examples: None,
1547                            encoding: Default::default(),
1548                        },
1549                    );
1550                    content
1551                },
1552                required: Some(true),
1553            })),
1554            responses: Default::default(),
1555            callbacks: Default::default(),
1556            deprecated: Some(false),
1557            security: vec![],
1558            servers: vec![],
1559            extensions: Default::default(),
1560        };
1561
1562        let spec = create_test_spec();
1563        let metadata = ToolGenerator::generate_tool_metadata(
1564            &operation,
1565            "put".to_string(),
1566            "/pets/{petId}/name".to_string(),
1567            &spec,
1568        )
1569        .unwrap();
1570
1571        // Check request body schema
1572        let properties = metadata
1573            .parameters
1574            .get("properties")
1575            .unwrap()
1576            .as_object()
1577            .unwrap();
1578        let request_body_schema = properties.get("request_body").unwrap();
1579        assert_eq!(request_body_schema.get("type"), Some(&json!("string")));
1580        assert_eq!(request_body_schema.get("minLength"), Some(&json!(1)));
1581        assert_eq!(request_body_schema.get("maxLength"), Some(&json!(100)));
1582        assert_eq!(
1583            request_body_schema.get("description"),
1584            Some(&json!("Request body data"))
1585        );
1586
1587        // Validate against MCP Tool schema
1588        validate_tool_against_mcp_schema(&metadata);
1589    }
1590
1591    #[test]
1592    fn test_request_body_ref_schema() {
1593        // Test with reference request body
1594        let operation = Operation {
1595            operation_id: Some("updatePet".to_string()),
1596            summary: Some("Update existing pet".to_string()),
1597            description: None,
1598            tags: vec![],
1599            external_docs: None,
1600            parameters: vec![],
1601            request_body: Some(ObjectOrReference::Ref {
1602                ref_path: "#/components/requestBodies/PetBody".to_string(),
1603            }),
1604            responses: Default::default(),
1605            callbacks: Default::default(),
1606            deprecated: Some(false),
1607            security: vec![],
1608            servers: vec![],
1609            extensions: Default::default(),
1610        };
1611
1612        let spec = create_test_spec();
1613        let metadata = ToolGenerator::generate_tool_metadata(
1614            &operation,
1615            "put".to_string(),
1616            "/pets/{petId}".to_string(),
1617            &spec,
1618        )
1619        .unwrap();
1620
1621        // Check that request_body uses generic object schema for refs
1622        let properties = metadata
1623            .parameters
1624            .get("properties")
1625            .unwrap()
1626            .as_object()
1627            .unwrap();
1628        let request_body_schema = properties.get("request_body").unwrap();
1629        assert_eq!(request_body_schema.get("type"), Some(&json!("object")));
1630        assert_eq!(
1631            request_body_schema.get("additionalProperties"),
1632            Some(&json!(true))
1633        );
1634
1635        // Validate against MCP Tool schema
1636        validate_tool_against_mcp_schema(&metadata);
1637    }
1638
1639    #[test]
1640    fn test_no_request_body_for_get() {
1641        // Test that GET operations don't get request body by default
1642        let operation = Operation {
1643            operation_id: Some("listPets".to_string()),
1644            summary: Some("List all pets".to_string()),
1645            description: None,
1646            tags: vec![],
1647            external_docs: None,
1648            parameters: vec![],
1649            request_body: None,
1650            responses: Default::default(),
1651            callbacks: Default::default(),
1652            deprecated: Some(false),
1653            security: vec![],
1654            servers: vec![],
1655            extensions: Default::default(),
1656        };
1657
1658        let spec = create_test_spec();
1659        let metadata = ToolGenerator::generate_tool_metadata(
1660            &operation,
1661            "get".to_string(),
1662            "/pets".to_string(),
1663            &spec,
1664        )
1665        .unwrap();
1666
1667        // Check that request_body is NOT in properties
1668        let properties = metadata
1669            .parameters
1670            .get("properties")
1671            .unwrap()
1672            .as_object()
1673            .unwrap();
1674        assert!(!properties.contains_key("request_body"));
1675
1676        // Validate against MCP Tool schema
1677        validate_tool_against_mcp_schema(&metadata);
1678    }
1679
1680    #[test]
1681    fn test_request_body_simple_object_with_properties() {
1682        // Test with simple object schema with a few properties
1683        let operation = Operation {
1684            operation_id: Some("updatePetStatus".to_string()),
1685            summary: Some("Update pet status".to_string()),
1686            description: None,
1687            tags: vec![],
1688            external_docs: None,
1689            parameters: vec![],
1690            request_body: Some(ObjectOrReference::Object(RequestBody {
1691                description: Some("Pet status update".to_string()),
1692                content: {
1693                    let mut content = BTreeMap::new();
1694                    content.insert(
1695                        "application/json".to_string(),
1696                        MediaType {
1697                            schema: Some(ObjectOrReference::Object(ObjectSchema {
1698                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
1699                                properties: {
1700                                    let mut props = BTreeMap::new();
1701                                    props.insert(
1702                                        "status".to_string(),
1703                                        ObjectOrReference::Object(ObjectSchema {
1704                                            schema_type: Some(SchemaTypeSet::Single(
1705                                                SchemaType::String,
1706                                            )),
1707                                            ..Default::default()
1708                                        }),
1709                                    );
1710                                    props.insert(
1711                                        "reason".to_string(),
1712                                        ObjectOrReference::Object(ObjectSchema {
1713                                            schema_type: Some(SchemaTypeSet::Single(
1714                                                SchemaType::String,
1715                                            )),
1716                                            ..Default::default()
1717                                        }),
1718                                    );
1719                                    props
1720                                },
1721                                required: vec!["status".to_string()],
1722                                ..Default::default()
1723                            })),
1724                            examples: None,
1725                            encoding: Default::default(),
1726                        },
1727                    );
1728                    content
1729                },
1730                required: Some(false),
1731            })),
1732            responses: Default::default(),
1733            callbacks: Default::default(),
1734            deprecated: Some(false),
1735            security: vec![],
1736            servers: vec![],
1737            extensions: Default::default(),
1738        };
1739
1740        let spec = create_test_spec();
1741        let metadata = ToolGenerator::generate_tool_metadata(
1742            &operation,
1743            "patch".to_string(),
1744            "/pets/{petId}/status".to_string(),
1745            &spec,
1746        )
1747        .unwrap();
1748
1749        // Check request body schema - should have actual properties
1750        let properties = metadata
1751            .parameters
1752            .get("properties")
1753            .unwrap()
1754            .as_object()
1755            .unwrap();
1756        let request_body_schema = properties.get("request_body").unwrap();
1757
1758        // Check basic structure
1759        assert_eq!(request_body_schema.get("type"), Some(&json!("object")));
1760        assert_eq!(
1761            request_body_schema.get("description"),
1762            Some(&json!("Pet status update"))
1763        );
1764
1765        // Check extracted properties
1766        let body_props = request_body_schema
1767            .get("properties")
1768            .unwrap()
1769            .as_object()
1770            .unwrap();
1771        assert_eq!(body_props.len(), 2);
1772        assert!(body_props.contains_key("status"));
1773        assert!(body_props.contains_key("reason"));
1774
1775        // Check required array from schema
1776        assert_eq!(
1777            request_body_schema.get("required"),
1778            Some(&json!(["status"]))
1779        );
1780
1781        // Should not be in top-level required since request body itself is optional
1782        let required = metadata
1783            .parameters
1784            .get("required")
1785            .unwrap()
1786            .as_array()
1787            .unwrap();
1788        assert!(!required.contains(&json!("request_body")));
1789
1790        // Validate against MCP Tool schema
1791        validate_tool_against_mcp_schema(&metadata);
1792    }
1793
1794    #[test]
1795    fn test_request_body_with_nested_properties() {
1796        // Test with complex nested object schema
1797        let operation = Operation {
1798            operation_id: Some("createUser".to_string()),
1799            summary: Some("Create a new user".to_string()),
1800            description: None,
1801            tags: vec![],
1802            external_docs: None,
1803            parameters: vec![],
1804            request_body: Some(ObjectOrReference::Object(RequestBody {
1805                description: Some("User creation data".to_string()),
1806                content: {
1807                    let mut content = BTreeMap::new();
1808                    content.insert(
1809                        "application/json".to_string(),
1810                        MediaType {
1811                            schema: Some(ObjectOrReference::Object(ObjectSchema {
1812                                schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
1813                                properties: {
1814                                    let mut props = BTreeMap::new();
1815                                    props.insert(
1816                                        "name".to_string(),
1817                                        ObjectOrReference::Object(ObjectSchema {
1818                                            schema_type: Some(SchemaTypeSet::Single(
1819                                                SchemaType::String,
1820                                            )),
1821                                            ..Default::default()
1822                                        }),
1823                                    );
1824                                    props.insert(
1825                                        "age".to_string(),
1826                                        ObjectOrReference::Object(ObjectSchema {
1827                                            schema_type: Some(SchemaTypeSet::Single(
1828                                                SchemaType::Integer,
1829                                            )),
1830                                            minimum: Some(serde_json::Number::from(0)),
1831                                            maximum: Some(serde_json::Number::from(150)),
1832                                            ..Default::default()
1833                                        }),
1834                                    );
1835                                    props
1836                                },
1837                                required: vec!["name".to_string()],
1838                                ..Default::default()
1839                            })),
1840                            examples: None,
1841                            encoding: Default::default(),
1842                        },
1843                    );
1844                    content
1845                },
1846                required: Some(true),
1847            })),
1848            responses: Default::default(),
1849            callbacks: Default::default(),
1850            deprecated: Some(false),
1851            security: vec![],
1852            servers: vec![],
1853            extensions: Default::default(),
1854        };
1855
1856        let spec = create_test_spec();
1857        let metadata = ToolGenerator::generate_tool_metadata(
1858            &operation,
1859            "post".to_string(),
1860            "/users".to_string(),
1861            &spec,
1862        )
1863        .unwrap();
1864
1865        // Check request body schema
1866        let properties = metadata
1867            .parameters
1868            .get("properties")
1869            .unwrap()
1870            .as_object()
1871            .unwrap();
1872        let request_body_schema = properties.get("request_body").unwrap();
1873        assert_eq!(request_body_schema.get("type"), Some(&json!("object")));
1874
1875        // Check that properties were extracted
1876        assert!(request_body_schema.get("properties").is_some());
1877        let props = request_body_schema
1878            .get("properties")
1879            .unwrap()
1880            .as_object()
1881            .unwrap();
1882        assert!(props.contains_key("name"));
1883        assert!(props.contains_key("age"));
1884
1885        // Check name property
1886        let name_prop = props.get("name").unwrap();
1887        assert_eq!(name_prop.get("type"), Some(&json!("string")));
1888
1889        // Check age property
1890        let age_prop = props.get("age").unwrap();
1891        assert_eq!(age_prop.get("type"), Some(&json!("integer")));
1892        assert_eq!(age_prop.get("minimum"), Some(&json!(0)));
1893        assert_eq!(age_prop.get("maximum"), Some(&json!(150)));
1894
1895        // Check required array
1896        assert_eq!(request_body_schema.get("required"), Some(&json!(["name"])));
1897
1898        // With properties defined, additionalProperties should be false
1899        assert_eq!(
1900            request_body_schema.get("additionalProperties"),
1901            Some(&json!(false))
1902        );
1903
1904        // Validate against MCP Tool schema
1905        validate_tool_against_mcp_schema(&metadata);
1906    }
1907}