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
11pub struct ToolGenerator;
13
14impl ToolGenerator {
15 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 let description = Self::build_description(operation, &method, &path);
36
37 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 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 fn resolve_reference(
93 ref_path: &str,
94 spec: &Spec,
95 visited: &mut HashSet<String>,
96 ) -> Result<ObjectSchema, OpenApiError> {
97 if visited.contains(ref_path) {
99 return Err(OpenApiError::ToolGeneration(format!(
100 "Circular reference detected: {ref_path}"
101 )));
102 }
103
104 visited.insert(ref_path.to_string());
106
107 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 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 let resolved_schema = match schema_ref {
132 ObjectOrReference::Object(obj_schema) => obj_schema.clone(),
133 ObjectOrReference::Ref {
134 ref_path: nested_ref,
135 } => {
136 Self::resolve_reference(nested_ref, spec, visited)?
138 }
139 };
140
141 visited.remove(ref_path);
143
144 Ok(resolved_schema)
145 }
146
147 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 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 eprintln!("Warning: Parameter reference not resolved: {ref_path}");
171 continue;
172 }
173 };
174
175 match ¶m.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 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 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 for param in &header_params {
201 let mut param_schema = Self::convert_parameter_schema(param, "header", spec)?;
202
203 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 for param in &cookie_params {
216 let mut param_schema = Self::convert_parameter_schema(param, "cookie", spec)?;
217
218 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 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 if !query_params.is_empty() || !header_params.is_empty() || !cookie_params.is_empty() {
243 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 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 if let Some(schema_ref) = ¶m.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 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 result.insert("type".to_string(), json!("string"));
298 }
299 }
300 }
301 }
302 } else {
303 result.insert("type".to_string(), json!("string"));
305 }
306
307 if let Some(desc) = ¶m.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 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 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 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"), }
361 } else {
362 item_types.push("string"); }
364 }
365 ObjectOrReference::Ref { ref_path } => {
366 let mut visited = HashSet::new();
368 match Self::resolve_reference(ref_path, spec, &mut visited) {
369 Ok(resolved_schema) => {
370 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"), }
393 } else {
394 item_types.push("string"); }
396 }
397 Err(_) => {
398 item_types.push("string");
400 }
401 }
402 }
403 }
404 }
405
406 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 result.insert("minItems".to_string(), json!(prefix_count));
413 result.insert("maxItems".to_string(), json!(prefix_count));
414 }
415
416 let unique_types: std::collections::HashSet<_> = item_types.into_iter().collect();
418
419 if unique_types.len() == 1 {
420 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 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 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 } else {
456 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 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 result.insert("items".to_string(), json!({"type": "string"}));
490 }
491 }
492 }
493 },
494 }
495 Ok(())
496 }
497
498 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 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 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 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 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 result.insert("type".to_string(), json!("object"));
547 result.insert(
548 "additionalProperties".to_string(),
549 json!(true),
550 );
551 }
552 }
553 }
554 }
555
556 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 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 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 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 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 if !obj_schema.prefix_items.is_empty() {
666 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 Self::convert_items_schema_to_draft07(items, result, spec)?;
676 } else {
677 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 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 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 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 result.insert("additionalProperties".to_string(), json!(false));
740 } else {
741 result.insert("additionalProperties".to_string(), json!(true));
743 }
744
745 if !obj_schema.required.is_empty() {
747 result
748 .insert("required".to_string(), json!(obj_schema.required));
749 }
750 }
751 _ => {
752 result.insert("type".to_string(), json!("string"));
754 }
755 }
756 } else {
757 result.insert("type".to_string(), json!("object"));
759 }
760 }
761 ObjectOrReference::Ref { ref_path } => {
762 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 result.insert("type".to_string(), json!("string"));
777 }
778 }
779 }
780 },
781 Schema::Boolean(_) => {
782 result.insert("type".to_string(), json!("object"));
784 }
785 }
786
787 Ok(())
788 }
789
790 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 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 for (key, value) in args {
817 if key == "timeout_seconds" {
818 continue; }
820
821 if key == "request_body" {
823 body_params.insert("request_body".to_string(), value.clone());
824 continue;
825 }
826
827 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 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 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 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 Self::validate_parameters(tool_metadata, &extracted)?;
883
884 Ok(extracted)
885 }
886
887 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 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 Ok("query".to_string())
919 }
920 }
921
922 fn validate_parameters(
924 tool_metadata: &ToolMetadata,
925 extracted: &ExtractedParameters,
926 ) -> Result<(), OpenApiError> {
927 let schema = &tool_metadata.parameters;
928
929 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 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#[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#[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 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 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 let mcp_tool = json!({
1067 "name": metadata.name,
1068 "description": metadata.description,
1069 "inputSchema": metadata.parameters
1070 });
1071
1072 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 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_tool_against_mcp_schema(&metadata);
1142 }
1143
1144 #[test]
1145 fn test_convert_prefix_items_to_draft07_mixed_types() {
1146 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 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 assert_eq!(result.get("minItems"), Some(&json!(2)));
1170 assert_eq!(result.get("maxItems"), Some(&json!(2)));
1171
1172 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 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 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 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 assert_eq!(result.get("minItems"), Some(&json!(2)));
1211 assert_eq!(result.get("maxItems"), Some(&json!(2)));
1212
1213 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 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 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 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 assert_eq!(result.get("maxItems"), Some(&json!(0)));
1244 }
1245
1246 #[test]
1247 fn test_convert_items_schema_to_draft07_object_schema() {
1248 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 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 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(¶m, "query", &spec).unwrap();
1305
1306 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 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(¶m, "query", &spec).unwrap();
1353
1354 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 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 let properties = metadata
1410 .parameters
1411 .get("properties")
1412 .unwrap()
1413 .as_object()
1414 .unwrap();
1415 assert!(properties.contains_key("request_body"));
1416
1417 let required = metadata
1419 .parameters
1420 .get("required")
1421 .unwrap()
1422 .as_array()
1423 .unwrap();
1424 assert!(required.contains(&json!("request_body")));
1425
1426 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_tool_against_mcp_schema(&metadata);
1437 }
1438
1439 #[test]
1440 fn test_request_body_array_schema() {
1441 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 let properties = metadata
1495 .parameters
1496 .get("properties")
1497 .unwrap()
1498 .as_object()
1499 .unwrap();
1500 assert!(properties.contains_key("request_body"));
1501
1502 let required = metadata
1504 .parameters
1505 .get("required")
1506 .unwrap()
1507 .as_array()
1508 .unwrap();
1509 assert!(!required.contains(&json!("request_body")));
1510
1511 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_tool_against_mcp_schema(&metadata);
1521 }
1522
1523 #[test]
1524 fn test_request_body_string_schema() {
1525 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 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_tool_against_mcp_schema(&metadata);
1589 }
1590
1591 #[test]
1592 fn test_request_body_ref_schema() {
1593 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 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_tool_against_mcp_schema(&metadata);
1637 }
1638
1639 #[test]
1640 fn test_no_request_body_for_get() {
1641 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 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_tool_against_mcp_schema(&metadata);
1678 }
1679
1680 #[test]
1681 fn test_request_body_simple_object_with_properties() {
1682 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 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 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 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 assert_eq!(
1777 request_body_schema.get("required"),
1778 Some(&json!(["status"]))
1779 );
1780
1781 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_tool_against_mcp_schema(&metadata);
1792 }
1793
1794 #[test]
1795 fn test_request_body_with_nested_properties() {
1796 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 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 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 let name_prop = props.get("name").unwrap();
1887 assert_eq!(name_prop.get("type"), Some(&json!("string")));
1888
1889 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 assert_eq!(request_body_schema.get("required"), Some(&json!(["name"])));
1897
1898 assert_eq!(
1900 request_body_schema.get("additionalProperties"),
1901 Some(&json!(false))
1902 );
1903
1904 validate_tool_against_mcp_schema(&metadata);
1906 }
1907}