1use schemars::schema_for;
2use serde::{Serialize, Serializer};
3use serde_json::{Value, json};
4use std::collections::{BTreeMap, HashMap, HashSet};
5
6use crate::error::{
7 ErrorResponse, OpenApiError, ToolCallValidationError, ValidationConstraint, ValidationError,
8};
9use crate::server::ToolMetadata;
10use oas3::spec::{
11 BooleanSchema, ObjectOrReference, ObjectSchema, Operation, Parameter, ParameterIn,
12 ParameterStyle, RequestBody, Response, Schema, SchemaType, SchemaTypeSet, Spec,
13};
14
15const X_LOCATION: &str = "x-location";
17const X_PARAMETER_LOCATION: &str = "x-parameter-location";
18const X_PARAMETER_REQUIRED: &str = "x-parameter-required";
19const X_CONTENT_TYPE: &str = "x-content-type";
20const X_ORIGINAL_NAME: &str = "x-original-name";
21const X_PARAMETER_EXPLODE: &str = "x-parameter-explode";
22
23#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum Location {
26 Parameter(ParameterIn),
28 Body,
30}
31
32impl Serialize for Location {
33 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
34 where
35 S: Serializer,
36 {
37 let str_value = match self {
38 Location::Parameter(param_in) => match param_in {
39 ParameterIn::Query => "query",
40 ParameterIn::Header => "header",
41 ParameterIn::Path => "path",
42 ParameterIn::Cookie => "cookie",
43 },
44 Location::Body => "body",
45 };
46 serializer.serialize_str(str_value)
47 }
48}
49
50#[derive(Debug, Clone, PartialEq)]
52pub enum Annotation {
53 Location(Location),
55 Required(bool),
57 ContentType(String),
59 OriginalName(String),
61 Explode(bool),
63}
64
65#[derive(Debug, Clone, Default)]
67pub struct Annotations {
68 annotations: Vec<Annotation>,
69}
70
71impl Annotations {
72 pub fn new() -> Self {
74 Self {
75 annotations: Vec::new(),
76 }
77 }
78
79 pub fn with_location(mut self, location: Location) -> Self {
81 self.annotations.push(Annotation::Location(location));
82 self
83 }
84
85 pub fn with_required(mut self, required: bool) -> Self {
87 self.annotations.push(Annotation::Required(required));
88 self
89 }
90
91 pub fn with_content_type(mut self, content_type: String) -> Self {
93 self.annotations.push(Annotation::ContentType(content_type));
94 self
95 }
96
97 pub fn with_original_name(mut self, original_name: String) -> Self {
99 self.annotations
100 .push(Annotation::OriginalName(original_name));
101 self
102 }
103
104 pub fn with_explode(mut self, explode: bool) -> Self {
106 self.annotations.push(Annotation::Explode(explode));
107 self
108 }
109}
110
111impl Serialize for Annotations {
112 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
113 where
114 S: Serializer,
115 {
116 use serde::ser::SerializeMap;
117
118 let mut map = serializer.serialize_map(Some(self.annotations.len()))?;
119
120 for annotation in &self.annotations {
121 match annotation {
122 Annotation::Location(location) => {
123 let key = match location {
125 Location::Parameter(param_in) => match param_in {
126 ParameterIn::Header | ParameterIn::Cookie => X_LOCATION,
127 _ => X_PARAMETER_LOCATION,
128 },
129 Location::Body => X_LOCATION,
130 };
131 map.serialize_entry(key, &location)?;
132
133 if let Location::Parameter(_) = location {
135 map.serialize_entry(X_PARAMETER_LOCATION, &location)?;
136 }
137 }
138 Annotation::Required(required) => {
139 map.serialize_entry(X_PARAMETER_REQUIRED, required)?;
140 }
141 Annotation::ContentType(content_type) => {
142 map.serialize_entry(X_CONTENT_TYPE, content_type)?;
143 }
144 Annotation::OriginalName(original_name) => {
145 map.serialize_entry(X_ORIGINAL_NAME, original_name)?;
146 }
147 Annotation::Explode(explode) => {
148 map.serialize_entry(X_PARAMETER_EXPLODE, explode)?;
149 }
150 }
151 }
152
153 map.end()
154 }
155}
156
157fn sanitize_property_name(name: &str) -> String {
166 let sanitized = name
168 .chars()
169 .map(|c| match c {
170 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '.' | '-' => c,
171 _ => '_',
172 })
173 .take(64)
174 .collect::<String>();
175
176 let mut collapsed = String::with_capacity(sanitized.len());
178 let mut prev_was_underscore = false;
179
180 for ch in sanitized.chars() {
181 if ch == '_' {
182 if !prev_was_underscore {
183 collapsed.push(ch);
184 }
185 prev_was_underscore = true;
186 } else {
187 collapsed.push(ch);
188 prev_was_underscore = false;
189 }
190 }
191
192 let trimmed = collapsed.trim_end_matches('_');
194
195 if trimmed.is_empty() || trimmed.chars().next().unwrap_or('0').is_numeric() {
197 format!("param_{trimmed}")
198 } else {
199 trimmed.to_string()
200 }
201}
202
203pub struct ToolGenerator;
205
206impl ToolGenerator {
207 pub fn generate_tool_metadata(
213 operation: &Operation,
214 method: String,
215 path: String,
216 spec: &Spec,
217 ) -> Result<ToolMetadata, OpenApiError> {
218 let name = operation.operation_id.clone().unwrap_or_else(|| {
219 format!(
220 "{}_{}",
221 method,
222 path.replace('/', "_").replace(['{', '}'], "")
223 )
224 });
225
226 let parameters = Self::generate_parameter_schema(
228 &operation.parameters,
229 &method,
230 &operation.request_body,
231 spec,
232 )?;
233
234 let description = Self::build_description(operation, &method, &path);
236
237 let output_schema = Self::extract_output_schema(&operation.responses, spec)?;
239
240 Ok(ToolMetadata {
241 name,
242 title: operation.summary.clone(),
243 description,
244 parameters,
245 output_schema,
246 method,
247 path,
248 })
249 }
250
251 fn build_description(operation: &Operation, method: &str, path: &str) -> String {
253 match (&operation.summary, &operation.description) {
254 (Some(summary), Some(desc)) => {
255 format!(
256 "{}\n\n{}\n\nEndpoint: {} {}",
257 summary,
258 desc,
259 method.to_uppercase(),
260 path
261 )
262 }
263 (Some(summary), None) => {
264 format!(
265 "{}\n\nEndpoint: {} {}",
266 summary,
267 method.to_uppercase(),
268 path
269 )
270 }
271 (None, Some(desc)) => {
272 format!("{}\n\nEndpoint: {} {}", desc, method.to_uppercase(), path)
273 }
274 (None, None) => {
275 format!("API endpoint: {} {}", method.to_uppercase(), path)
276 }
277 }
278 }
279
280 fn extract_output_schema(
284 responses: &Option<BTreeMap<String, ObjectOrReference<Response>>>,
285 spec: &Spec,
286 ) -> Result<Option<Value>, OpenApiError> {
287 let responses = match responses {
288 Some(r) => r,
289 None => return Ok(None),
290 };
291 let priority_codes = vec![
293 "200", "201", "202", "203", "204", "2XX", "default", ];
301
302 for status_code in priority_codes {
303 if let Some(response_or_ref) = responses.get(status_code) {
304 let response = match response_or_ref {
306 ObjectOrReference::Object(response) => response,
307 ObjectOrReference::Ref { ref_path: _ } => {
308 continue;
311 }
312 };
313
314 if status_code == "204" {
316 continue;
317 }
318
319 if !response.content.is_empty() {
321 let content = &response.content;
322 let json_media_types = vec![
324 "application/json",
325 "application/ld+json",
326 "application/vnd.api+json",
327 ];
328
329 for media_type_str in json_media_types {
330 if let Some(media_type) = content.get(media_type_str)
331 && let Some(schema_or_ref) = &media_type.schema
332 {
333 let wrapped_schema = Self::wrap_output_schema(schema_or_ref, spec)?;
335 return Ok(Some(wrapped_schema));
336 }
337 }
338
339 for media_type in content.values() {
341 if let Some(schema_or_ref) = &media_type.schema {
342 let wrapped_schema = Self::wrap_output_schema(schema_or_ref, spec)?;
344 return Ok(Some(wrapped_schema));
345 }
346 }
347 }
348 }
349 }
350
351 Ok(None)
353 }
354
355 fn convert_schema_to_json_schema(
365 schema: &Schema,
366 spec: &Spec,
367 visited: &mut HashSet<String>,
368 ) -> Result<Value, OpenApiError> {
369 match schema {
370 Schema::Object(obj_schema_or_ref) => match obj_schema_or_ref.as_ref() {
371 ObjectOrReference::Object(obj_schema) => {
372 Self::convert_object_schema_to_json_schema(obj_schema, spec, visited)
373 }
374 ObjectOrReference::Ref { ref_path } => {
375 let resolved = Self::resolve_reference(ref_path, spec, visited)?;
376 Self::convert_object_schema_to_json_schema(&resolved, spec, visited)
377 }
378 },
379 Schema::Boolean(bool_schema) => {
380 if bool_schema.0 {
382 Ok(json!({})) } else {
384 Ok(json!({"not": {}})) }
386 }
387 }
388 }
389
390 fn convert_object_schema_to_json_schema(
400 obj_schema: &ObjectSchema,
401 spec: &Spec,
402 visited: &mut HashSet<String>,
403 ) -> Result<Value, OpenApiError> {
404 let mut schema_obj = serde_json::Map::new();
405
406 if let Some(schema_type) = &obj_schema.schema_type {
408 match schema_type {
409 SchemaTypeSet::Single(single_type) => {
410 schema_obj.insert(
411 "type".to_string(),
412 json!(Self::schema_type_to_string(single_type)),
413 );
414 }
415 SchemaTypeSet::Multiple(type_set) => {
416 let types: Vec<String> =
417 type_set.iter().map(Self::schema_type_to_string).collect();
418 schema_obj.insert("type".to_string(), json!(types));
419 }
420 }
421 }
422
423 if let Some(desc) = &obj_schema.description {
425 schema_obj.insert("description".to_string(), json!(desc));
426 }
427
428 if !obj_schema.one_of.is_empty() {
430 let mut one_of_schemas = Vec::new();
431 for schema_ref in &obj_schema.one_of {
432 let schema_json = match schema_ref {
433 ObjectOrReference::Object(schema) => {
434 Self::convert_object_schema_to_json_schema(schema, spec, visited)?
435 }
436 ObjectOrReference::Ref { ref_path } => {
437 let resolved = Self::resolve_reference(ref_path, spec, visited)?;
438 Self::convert_object_schema_to_json_schema(&resolved, spec, visited)?
439 }
440 };
441 one_of_schemas.push(schema_json);
442 }
443 schema_obj.insert("oneOf".to_string(), json!(one_of_schemas));
444 return Ok(Value::Object(schema_obj));
447 }
448
449 if !obj_schema.properties.is_empty() {
451 let properties = &obj_schema.properties;
452 let mut props_map = serde_json::Map::new();
453 for (prop_name, prop_schema_or_ref) in properties {
454 let prop_schema = match prop_schema_or_ref {
455 ObjectOrReference::Object(schema) => {
456 Self::convert_schema_to_json_schema(
458 &Schema::Object(Box::new(ObjectOrReference::Object(schema.clone()))),
459 spec,
460 visited,
461 )?
462 }
463 ObjectOrReference::Ref { ref_path } => {
464 let resolved = Self::resolve_reference(ref_path, spec, visited)?;
465 Self::convert_object_schema_to_json_schema(&resolved, spec, visited)?
466 }
467 };
468
469 let sanitized_name = sanitize_property_name(prop_name);
471 if sanitized_name != *prop_name {
472 let annotations = Annotations::new().with_original_name(prop_name.clone());
474 let prop_with_annotation =
475 Self::apply_annotations_to_schema(prop_schema, annotations);
476 props_map.insert(sanitized_name, prop_with_annotation);
477 } else {
478 props_map.insert(prop_name.clone(), prop_schema);
479 }
480 }
481 schema_obj.insert("properties".to_string(), Value::Object(props_map));
482 }
483
484 if !obj_schema.required.is_empty() {
486 schema_obj.insert("required".to_string(), json!(&obj_schema.required));
487 }
488
489 if let Some(schema_type) = &obj_schema.schema_type
491 && matches!(schema_type, SchemaTypeSet::Single(SchemaType::Object))
492 {
493 match &obj_schema.additional_properties {
495 None => {
496 schema_obj.insert("additionalProperties".to_string(), json!(true));
498 }
499 Some(Schema::Boolean(BooleanSchema(value))) => {
500 schema_obj.insert("additionalProperties".to_string(), json!(value));
502 }
503 Some(Schema::Object(schema_ref)) => {
504 let mut visited = HashSet::new();
506 let additional_props_schema = Self::convert_schema_to_json_schema(
507 &Schema::Object(schema_ref.clone()),
508 spec,
509 &mut visited,
510 )?;
511 schema_obj.insert("additionalProperties".to_string(), additional_props_schema);
512 }
513 }
514 }
515
516 if let Some(schema_type) = &obj_schema.schema_type {
518 if matches!(schema_type, SchemaTypeSet::Single(SchemaType::Array)) {
519 if !obj_schema.prefix_items.is_empty() {
521 Self::convert_prefix_items_to_draft07(
523 &obj_schema.prefix_items,
524 &obj_schema.items,
525 &mut schema_obj,
526 spec,
527 )?;
528 } else if let Some(items_schema) = &obj_schema.items {
529 let items_json =
531 Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
532 schema_obj.insert("items".to_string(), items_json);
533 }
534
535 if let Some(min_items) = obj_schema.min_items {
537 schema_obj.insert("minItems".to_string(), json!(min_items));
538 }
539 if let Some(max_items) = obj_schema.max_items {
540 schema_obj.insert("maxItems".to_string(), json!(max_items));
541 }
542 } else if let Some(items_schema) = &obj_schema.items {
543 let items_json = Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
545 schema_obj.insert("items".to_string(), items_json);
546 }
547 }
548
549 if let Some(format) = &obj_schema.format {
551 schema_obj.insert("format".to_string(), json!(format));
552 }
553
554 if let Some(example) = &obj_schema.example {
555 schema_obj.insert("example".to_string(), example.clone());
556 }
557
558 if let Some(default) = &obj_schema.default {
559 schema_obj.insert("default".to_string(), default.clone());
560 }
561
562 if !obj_schema.enum_values.is_empty() {
563 schema_obj.insert("enum".to_string(), json!(&obj_schema.enum_values));
564 }
565
566 if let Some(min) = &obj_schema.minimum {
567 schema_obj.insert("minimum".to_string(), json!(min));
568 }
569
570 if let Some(max) = &obj_schema.maximum {
571 schema_obj.insert("maximum".to_string(), json!(max));
572 }
573
574 if let Some(min_length) = &obj_schema.min_length {
575 schema_obj.insert("minLength".to_string(), json!(min_length));
576 }
577
578 if let Some(max_length) = &obj_schema.max_length {
579 schema_obj.insert("maxLength".to_string(), json!(max_length));
580 }
581
582 if let Some(pattern) = &obj_schema.pattern {
583 schema_obj.insert("pattern".to_string(), json!(pattern));
584 }
585
586 Ok(Value::Object(schema_obj))
587 }
588
589 fn schema_type_to_string(schema_type: &SchemaType) -> String {
591 match schema_type {
592 SchemaType::Boolean => "boolean",
593 SchemaType::Integer => "integer",
594 SchemaType::Number => "number",
595 SchemaType::String => "string",
596 SchemaType::Array => "array",
597 SchemaType::Object => "object",
598 SchemaType::Null => "null",
599 }
600 .to_string()
601 }
602
603 fn resolve_reference(
613 ref_path: &str,
614 spec: &Spec,
615 visited: &mut HashSet<String>,
616 ) -> Result<ObjectSchema, OpenApiError> {
617 if visited.contains(ref_path) {
619 return Err(OpenApiError::ToolGeneration(format!(
620 "Circular reference detected: {ref_path}"
621 )));
622 }
623
624 visited.insert(ref_path.to_string());
626
627 if !ref_path.starts_with("#/components/schemas/") {
630 return Err(OpenApiError::ToolGeneration(format!(
631 "Unsupported reference format: {ref_path}. Only #/components/schemas/ references are supported"
632 )));
633 }
634
635 let schema_name = ref_path.strip_prefix("#/components/schemas/").unwrap();
636
637 let components = spec.components.as_ref().ok_or_else(|| {
639 OpenApiError::ToolGeneration(format!(
640 "Reference {ref_path} points to components, but spec has no components section"
641 ))
642 })?;
643
644 let schema_ref = components.schemas.get(schema_name).ok_or_else(|| {
645 OpenApiError::ToolGeneration(format!(
646 "Schema '{schema_name}' not found in components/schemas"
647 ))
648 })?;
649
650 let resolved_schema = match schema_ref {
652 ObjectOrReference::Object(obj_schema) => obj_schema.clone(),
653 ObjectOrReference::Ref {
654 ref_path: nested_ref,
655 } => {
656 Self::resolve_reference(nested_ref, spec, visited)?
658 }
659 };
660
661 visited.remove(ref_path);
663
664 Ok(resolved_schema)
665 }
666
667 fn generate_parameter_schema(
669 parameters: &[ObjectOrReference<Parameter>],
670 _method: &str,
671 request_body: &Option<ObjectOrReference<RequestBody>>,
672 spec: &Spec,
673 ) -> Result<Value, OpenApiError> {
674 let mut properties = serde_json::Map::new();
675 let mut required = Vec::new();
676
677 let mut path_params = Vec::new();
679 let mut query_params = Vec::new();
680 let mut header_params = Vec::new();
681 let mut cookie_params = Vec::new();
682
683 for param_ref in parameters {
684 let param = match param_ref {
685 ObjectOrReference::Object(param) => param,
686 ObjectOrReference::Ref { ref_path } => {
687 eprintln!("Warning: Parameter reference not resolved: {ref_path}");
691 continue;
692 }
693 };
694
695 match ¶m.location {
696 ParameterIn::Query => query_params.push(param),
697 ParameterIn::Header => header_params.push(param),
698 ParameterIn::Path => path_params.push(param),
699 ParameterIn::Cookie => cookie_params.push(param),
700 }
701 }
702
703 for param in path_params {
705 let (param_schema, mut annotations) =
706 Self::convert_parameter_schema(param, ParameterIn::Path, spec)?;
707
708 let sanitized_name = sanitize_property_name(¶m.name);
710 if sanitized_name != param.name {
711 annotations = annotations.with_original_name(param.name.clone());
712 }
713
714 let param_schema_with_annotations =
715 Self::apply_annotations_to_schema(param_schema, annotations);
716 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
717 required.push(sanitized_name);
718 }
719
720 for param in &query_params {
722 let (param_schema, mut annotations) =
723 Self::convert_parameter_schema(param, ParameterIn::Query, spec)?;
724
725 let sanitized_name = sanitize_property_name(¶m.name);
727 if sanitized_name != param.name {
728 annotations = annotations.with_original_name(param.name.clone());
729 }
730
731 let param_schema_with_annotations =
732 Self::apply_annotations_to_schema(param_schema, annotations);
733 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
734 if param.required.unwrap_or(false) {
735 required.push(sanitized_name);
736 }
737 }
738
739 for param in &header_params {
741 let (param_schema, mut annotations) =
742 Self::convert_parameter_schema(param, ParameterIn::Header, spec)?;
743
744 let prefixed_name = format!("header_{}", param.name);
746 let sanitized_name = sanitize_property_name(&prefixed_name);
747 if sanitized_name != prefixed_name {
748 annotations = annotations.with_original_name(param.name.clone());
749 }
750
751 let param_schema_with_annotations =
752 Self::apply_annotations_to_schema(param_schema, annotations);
753
754 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
755 if param.required.unwrap_or(false) {
756 required.push(sanitized_name);
757 }
758 }
759
760 for param in &cookie_params {
762 let (param_schema, mut annotations) =
763 Self::convert_parameter_schema(param, ParameterIn::Cookie, spec)?;
764
765 let prefixed_name = format!("cookie_{}", param.name);
767 let sanitized_name = sanitize_property_name(&prefixed_name);
768 if sanitized_name != prefixed_name {
769 annotations = annotations.with_original_name(param.name.clone());
770 }
771
772 let param_schema_with_annotations =
773 Self::apply_annotations_to_schema(param_schema, annotations);
774
775 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
776 if param.required.unwrap_or(false) {
777 required.push(sanitized_name);
778 }
779 }
780
781 if let Some(request_body) = request_body
783 && let Some((body_schema, annotations, is_required)) =
784 Self::convert_request_body_to_json_schema(request_body, spec)?
785 {
786 let body_schema_with_annotations =
787 Self::apply_annotations_to_schema(body_schema, annotations);
788 properties.insert("request_body".to_string(), body_schema_with_annotations);
789 if is_required {
790 required.push("request_body".to_string());
791 }
792 }
793
794 if !query_params.is_empty() || !header_params.is_empty() || !cookie_params.is_empty() {
796 properties.insert(
798 "timeout_seconds".to_string(),
799 json!({
800 "type": "integer",
801 "description": "Request timeout in seconds",
802 "minimum": 1,
803 "maximum": 300,
804 "default": 30
805 }),
806 );
807 }
808
809 Ok(json!({
810 "type": "object",
811 "properties": properties,
812 "required": required,
813 "additionalProperties": false
814 }))
815 }
816
817 fn convert_parameter_schema(
819 param: &Parameter,
820 location: ParameterIn,
821 spec: &Spec,
822 ) -> Result<(Value, Annotations), OpenApiError> {
823 let base_schema = if let Some(schema_ref) = ¶m.schema {
825 match schema_ref {
826 ObjectOrReference::Object(obj_schema) => {
827 let mut visited = HashSet::new();
828 Self::convert_schema_to_json_schema(
829 &Schema::Object(Box::new(ObjectOrReference::Object(obj_schema.clone()))),
830 spec,
831 &mut visited,
832 )?
833 }
834 ObjectOrReference::Ref { ref_path } => {
835 let mut visited = HashSet::new();
837 match Self::resolve_reference(ref_path, spec, &mut visited) {
838 Ok(resolved_schema) => Self::convert_schema_to_json_schema(
839 &Schema::Object(Box::new(ObjectOrReference::Object(resolved_schema))),
840 spec,
841 &mut visited,
842 )?,
843 Err(_) => {
844 json!({"type": "string"})
846 }
847 }
848 }
849 }
850 } else {
851 json!({"type": "string"})
853 };
854
855 let mut result = match base_schema {
857 Value::Object(obj) => obj,
858 _ => {
859 return Err(OpenApiError::ToolGeneration(format!(
861 "Internal error: schema converter returned non-object for parameter '{}'",
862 param.name
863 )));
864 }
865 };
866
867 let mut collected_examples = Vec::new();
869
870 if let Some(example) = ¶m.example {
872 collected_examples.push(example.clone());
873 } else if !param.examples.is_empty() {
874 for example_ref in param.examples.values() {
876 match example_ref {
877 ObjectOrReference::Object(example_obj) => {
878 if let Some(value) = &example_obj.value {
879 collected_examples.push(value.clone());
880 }
881 }
882 ObjectOrReference::Ref { .. } => {
883 }
885 }
886 }
887 } else if let Some(Value::String(ex_str)) = result.get("example") {
888 collected_examples.push(json!(ex_str));
890 } else if let Some(ex) = result.get("example") {
891 collected_examples.push(ex.clone());
892 }
893
894 let base_description = param
896 .description
897 .as_ref()
898 .map(|d| d.to_string())
899 .or_else(|| {
900 result
901 .get("description")
902 .and_then(|d| d.as_str())
903 .map(|d| d.to_string())
904 })
905 .unwrap_or_else(|| format!("{} parameter", param.name));
906
907 let description_with_examples = if let Some(examples_str) =
908 Self::format_examples_for_description(&collected_examples)
909 {
910 format!("{base_description}. {examples_str}")
911 } else {
912 base_description
913 };
914
915 result.insert("description".to_string(), json!(description_with_examples));
916
917 if let Some(example) = ¶m.example {
922 result.insert("example".to_string(), example.clone());
923 } else if !param.examples.is_empty() {
924 let mut examples_array = Vec::new();
927 for (example_name, example_ref) in ¶m.examples {
928 match example_ref {
929 ObjectOrReference::Object(example_obj) => {
930 if let Some(value) = &example_obj.value {
931 examples_array.push(json!({
932 "name": example_name,
933 "value": value
934 }));
935 }
936 }
937 ObjectOrReference::Ref { .. } => {
938 }
941 }
942 }
943
944 if !examples_array.is_empty() {
945 if let Some(first_example) = examples_array.first()
947 && let Some(value) = first_example.get("value")
948 {
949 result.insert("example".to_string(), value.clone());
950 }
951 result.insert("x-examples".to_string(), json!(examples_array));
953 }
954 }
955
956 let mut annotations = Annotations::new()
958 .with_location(Location::Parameter(location))
959 .with_required(param.required.unwrap_or(false));
960
961 if let Some(explode) = param.explode {
963 annotations = annotations.with_explode(explode);
964 } else {
965 let default_explode = match ¶m.style {
969 Some(ParameterStyle::Form) | None => true, _ => false,
971 };
972 annotations = annotations.with_explode(default_explode);
973 }
974
975 Ok((Value::Object(result), annotations))
976 }
977
978 fn apply_annotations_to_schema(schema: Value, annotations: Annotations) -> Value {
980 match schema {
981 Value::Object(mut obj) => {
982 if let Ok(Value::Object(ann_map)) = serde_json::to_value(&annotations) {
984 for (key, value) in ann_map {
985 obj.insert(key, value);
986 }
987 }
988 Value::Object(obj)
989 }
990 _ => schema,
991 }
992 }
993
994 fn format_examples_for_description(examples: &[Value]) -> Option<String> {
996 if examples.is_empty() {
997 return None;
998 }
999
1000 if examples.len() == 1 {
1001 let example_str =
1002 serde_json::to_string(&examples[0]).unwrap_or_else(|_| "null".to_string());
1003 Some(format!("Example: `{example_str}`"))
1004 } else {
1005 let mut result = String::from("Examples:\n");
1006 for ex in examples {
1007 let json_str = serde_json::to_string(ex).unwrap_or_else(|_| "null".to_string());
1008 result.push_str(&format!("- `{json_str}`\n"));
1009 }
1010 result.pop();
1012 Some(result)
1013 }
1014 }
1015
1016 fn convert_prefix_items_to_draft07(
1027 prefix_items: &[ObjectOrReference<ObjectSchema>],
1028 items: &Option<Box<Schema>>,
1029 result: &mut serde_json::Map<String, Value>,
1030 spec: &Spec,
1031 ) -> Result<(), OpenApiError> {
1032 let prefix_count = prefix_items.len();
1033
1034 let mut item_types = Vec::new();
1036 for prefix_item in prefix_items {
1037 match prefix_item {
1038 ObjectOrReference::Object(obj_schema) => {
1039 if let Some(schema_type) = &obj_schema.schema_type {
1040 match schema_type {
1041 SchemaTypeSet::Single(SchemaType::String) => item_types.push("string"),
1042 SchemaTypeSet::Single(SchemaType::Integer) => {
1043 item_types.push("integer")
1044 }
1045 SchemaTypeSet::Single(SchemaType::Number) => item_types.push("number"),
1046 SchemaTypeSet::Single(SchemaType::Boolean) => {
1047 item_types.push("boolean")
1048 }
1049 SchemaTypeSet::Single(SchemaType::Array) => item_types.push("array"),
1050 SchemaTypeSet::Single(SchemaType::Object) => item_types.push("object"),
1051 _ => item_types.push("string"), }
1053 } else {
1054 item_types.push("string"); }
1056 }
1057 ObjectOrReference::Ref { ref_path } => {
1058 let mut visited = HashSet::new();
1060 match Self::resolve_reference(ref_path, spec, &mut visited) {
1061 Ok(resolved_schema) => {
1062 if let Some(schema_type_set) = &resolved_schema.schema_type {
1064 match schema_type_set {
1065 SchemaTypeSet::Single(SchemaType::String) => {
1066 item_types.push("string")
1067 }
1068 SchemaTypeSet::Single(SchemaType::Integer) => {
1069 item_types.push("integer")
1070 }
1071 SchemaTypeSet::Single(SchemaType::Number) => {
1072 item_types.push("number")
1073 }
1074 SchemaTypeSet::Single(SchemaType::Boolean) => {
1075 item_types.push("boolean")
1076 }
1077 SchemaTypeSet::Single(SchemaType::Array) => {
1078 item_types.push("array")
1079 }
1080 SchemaTypeSet::Single(SchemaType::Object) => {
1081 item_types.push("object")
1082 }
1083 _ => item_types.push("string"), }
1085 } else {
1086 item_types.push("string"); }
1088 }
1089 Err(_) => {
1090 item_types.push("string");
1092 }
1093 }
1094 }
1095 }
1096 }
1097
1098 let items_is_false =
1100 matches!(items.as_ref().map(|i| i.as_ref()), Some(Schema::Boolean(b)) if !b.0);
1101
1102 if items_is_false {
1103 result.insert("minItems".to_string(), json!(prefix_count));
1105 result.insert("maxItems".to_string(), json!(prefix_count));
1106 }
1107
1108 let unique_types: std::collections::BTreeSet<_> = item_types.into_iter().collect();
1110
1111 if unique_types.len() == 1 {
1112 let item_type = unique_types.into_iter().next().unwrap();
1114 result.insert("items".to_string(), json!({"type": item_type}));
1115 } else if unique_types.len() > 1 {
1116 let one_of: Vec<Value> = unique_types
1118 .into_iter()
1119 .map(|t| json!({"type": t}))
1120 .collect();
1121 result.insert("items".to_string(), json!({"oneOf": one_of}));
1122 }
1123
1124 Ok(())
1125 }
1126
1127 fn convert_request_body_to_json_schema(
1139 request_body_ref: &ObjectOrReference<RequestBody>,
1140 spec: &Spec,
1141 ) -> Result<Option<(Value, Annotations, bool)>, OpenApiError> {
1142 match request_body_ref {
1143 ObjectOrReference::Object(request_body) => {
1144 let schema_info = request_body
1147 .content
1148 .get(mime::APPLICATION_JSON.as_ref())
1149 .or_else(|| request_body.content.get("application/json"))
1150 .or_else(|| {
1151 request_body.content.values().next()
1153 });
1154
1155 if let Some(media_type) = schema_info {
1156 if let Some(schema_ref) = &media_type.schema {
1157 let schema = Schema::Object(Box::new(schema_ref.clone()));
1159
1160 let mut visited = HashSet::new();
1162 let converted_schema =
1163 Self::convert_schema_to_json_schema(&schema, spec, &mut visited)?;
1164
1165 let mut schema_obj = match converted_schema {
1167 Value::Object(obj) => obj,
1168 _ => {
1169 let mut obj = serde_json::Map::new();
1171 obj.insert("type".to_string(), json!("object"));
1172 obj.insert("additionalProperties".to_string(), json!(true));
1173 obj
1174 }
1175 };
1176
1177 let description = request_body
1179 .description
1180 .clone()
1181 .unwrap_or_else(|| "Request body data".to_string());
1182 schema_obj.insert("description".to_string(), json!(description));
1183
1184 let annotations = Annotations::new()
1186 .with_location(Location::Body)
1187 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
1188
1189 let required = request_body.required.unwrap_or(false);
1190 Ok(Some((Value::Object(schema_obj), annotations, required)))
1191 } else {
1192 Ok(None)
1193 }
1194 } else {
1195 Ok(None)
1196 }
1197 }
1198 ObjectOrReference::Ref { .. } => {
1199 let mut result = serde_json::Map::new();
1201 result.insert("type".to_string(), json!("object"));
1202 result.insert("additionalProperties".to_string(), json!(true));
1203 result.insert("description".to_string(), json!("Request body data"));
1204
1205 let annotations = Annotations::new()
1207 .with_location(Location::Body)
1208 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
1209
1210 Ok(Some((Value::Object(result), annotations, false)))
1211 }
1212 }
1213 }
1214
1215 pub fn extract_parameters(
1221 tool_metadata: &ToolMetadata,
1222 arguments: &Value,
1223 ) -> Result<ExtractedParameters, ToolCallValidationError> {
1224 let args = arguments.as_object().ok_or_else(|| {
1225 ToolCallValidationError::RequestConstructionError {
1226 reason: "Arguments must be an object".to_string(),
1227 }
1228 })?;
1229
1230 let mut path_params = HashMap::new();
1231 let mut query_params = HashMap::new();
1232 let mut header_params = HashMap::new();
1233 let mut cookie_params = HashMap::new();
1234 let mut body_params = HashMap::new();
1235 let mut config = RequestConfig::default();
1236
1237 if let Some(timeout) = args.get("timeout_seconds").and_then(Value::as_u64) {
1239 config.timeout_seconds = u32::try_from(timeout).unwrap_or(u32::MAX);
1240 }
1241
1242 for (key, value) in args {
1244 if key == "timeout_seconds" {
1245 continue; }
1247
1248 if key == "request_body" {
1250 body_params.insert("request_body".to_string(), value.clone());
1251 continue;
1252 }
1253
1254 let location = Self::get_parameter_location(tool_metadata, key).map_err(|e| {
1256 ToolCallValidationError::RequestConstructionError {
1257 reason: e.to_string(),
1258 }
1259 })?;
1260
1261 let original_name = Self::get_original_parameter_name(tool_metadata, key);
1263
1264 match location.as_str() {
1265 "path" => {
1266 path_params.insert(original_name.unwrap_or_else(|| key.clone()), value.clone());
1267 }
1268 "query" => {
1269 let param_name = original_name.unwrap_or_else(|| key.clone());
1270 let explode = Self::get_parameter_explode(tool_metadata, key);
1271 query_params.insert(param_name, QueryParameter::new(value.clone(), explode));
1272 }
1273 "header" => {
1274 let header_name = if let Some(orig) = original_name {
1276 orig
1277 } else if key.starts_with("header_") {
1278 key.strip_prefix("header_").unwrap_or(key).to_string()
1279 } else {
1280 key.clone()
1281 };
1282 header_params.insert(header_name, value.clone());
1283 }
1284 "cookie" => {
1285 let cookie_name = if let Some(orig) = original_name {
1287 orig
1288 } else if key.starts_with("cookie_") {
1289 key.strip_prefix("cookie_").unwrap_or(key).to_string()
1290 } else {
1291 key.clone()
1292 };
1293 cookie_params.insert(cookie_name, value.clone());
1294 }
1295 "body" => {
1296 let body_name = if key.starts_with("body_") {
1298 key.strip_prefix("body_").unwrap_or(key).to_string()
1299 } else {
1300 key.clone()
1301 };
1302 body_params.insert(body_name, value.clone());
1303 }
1304 _ => {
1305 return Err(ToolCallValidationError::RequestConstructionError {
1306 reason: format!("Unknown parameter location for parameter: {key}"),
1307 });
1308 }
1309 }
1310 }
1311
1312 let extracted = ExtractedParameters {
1313 path: path_params,
1314 query: query_params,
1315 headers: header_params,
1316 cookies: cookie_params,
1317 body: body_params,
1318 config,
1319 };
1320
1321 Self::validate_parameters(tool_metadata, arguments)?;
1323
1324 Ok(extracted)
1325 }
1326
1327 fn get_original_parameter_name(
1329 tool_metadata: &ToolMetadata,
1330 param_name: &str,
1331 ) -> Option<String> {
1332 tool_metadata
1333 .parameters
1334 .get("properties")
1335 .and_then(|p| p.as_object())
1336 .and_then(|props| props.get(param_name))
1337 .and_then(|schema| schema.get(X_ORIGINAL_NAME))
1338 .and_then(|v| v.as_str())
1339 .map(|s| s.to_string())
1340 }
1341
1342 fn get_parameter_explode(tool_metadata: &ToolMetadata, param_name: &str) -> bool {
1344 tool_metadata
1345 .parameters
1346 .get("properties")
1347 .and_then(|p| p.as_object())
1348 .and_then(|props| props.get(param_name))
1349 .and_then(|schema| schema.get(X_PARAMETER_EXPLODE))
1350 .and_then(|v| v.as_bool())
1351 .unwrap_or(true) }
1353
1354 fn get_parameter_location(
1356 tool_metadata: &ToolMetadata,
1357 param_name: &str,
1358 ) -> Result<String, OpenApiError> {
1359 let properties = tool_metadata
1360 .parameters
1361 .get("properties")
1362 .and_then(|p| p.as_object())
1363 .ok_or_else(|| {
1364 OpenApiError::ToolGeneration("Invalid tool parameters schema".to_string())
1365 })?;
1366
1367 if let Some(param_schema) = properties.get(param_name)
1368 && let Some(location) = param_schema
1369 .get(X_PARAMETER_LOCATION)
1370 .and_then(|v| v.as_str())
1371 {
1372 return Ok(location.to_string());
1373 }
1374
1375 if param_name.starts_with("header_") {
1377 Ok("header".to_string())
1378 } else if param_name.starts_with("cookie_") {
1379 Ok("cookie".to_string())
1380 } else if param_name.starts_with("body_") {
1381 Ok("body".to_string())
1382 } else {
1383 Ok("query".to_string())
1385 }
1386 }
1387
1388 fn validate_parameters(
1390 tool_metadata: &ToolMetadata,
1391 arguments: &Value,
1392 ) -> Result<(), ToolCallValidationError> {
1393 let schema = &tool_metadata.parameters;
1394
1395 let required_params = schema
1397 .get("required")
1398 .and_then(|r| r.as_array())
1399 .map(|arr| {
1400 arr.iter()
1401 .filter_map(|v| v.as_str())
1402 .collect::<std::collections::HashSet<_>>()
1403 })
1404 .unwrap_or_default();
1405
1406 let properties = schema
1407 .get("properties")
1408 .and_then(|p| p.as_object())
1409 .ok_or_else(|| ToolCallValidationError::RequestConstructionError {
1410 reason: "Tool schema missing properties".to_string(),
1411 })?;
1412
1413 let args = arguments.as_object().ok_or_else(|| {
1414 ToolCallValidationError::RequestConstructionError {
1415 reason: "Arguments must be an object".to_string(),
1416 }
1417 })?;
1418
1419 let mut all_errors = Vec::new();
1421
1422 all_errors.extend(Self::check_unknown_parameters(args, properties));
1424
1425 all_errors.extend(Self::check_missing_required(
1427 args,
1428 properties,
1429 &required_params,
1430 ));
1431
1432 all_errors.extend(Self::validate_parameter_values(args, properties));
1434
1435 if !all_errors.is_empty() {
1437 return Err(ToolCallValidationError::InvalidParameters {
1438 violations: all_errors,
1439 });
1440 }
1441
1442 Ok(())
1443 }
1444
1445 fn check_unknown_parameters(
1447 args: &serde_json::Map<String, Value>,
1448 properties: &serde_json::Map<String, Value>,
1449 ) -> Vec<ValidationError> {
1450 let mut errors = Vec::new();
1451
1452 let valid_params: Vec<String> = properties.keys().map(|s| s.to_string()).collect();
1454
1455 for (arg_name, _) in args.iter() {
1457 if !properties.contains_key(arg_name) {
1458 let valid_params_refs: Vec<&str> =
1460 valid_params.iter().map(|s| s.as_str()).collect();
1461 let suggestions = crate::find_similar_strings(arg_name, &valid_params_refs);
1462
1463 errors.push(ValidationError::InvalidParameter {
1464 parameter: arg_name.clone(),
1465 suggestions,
1466 valid_parameters: valid_params.clone(),
1467 });
1468 }
1469 }
1470
1471 errors
1472 }
1473
1474 fn check_missing_required(
1476 args: &serde_json::Map<String, Value>,
1477 properties: &serde_json::Map<String, Value>,
1478 required_params: &HashSet<&str>,
1479 ) -> Vec<ValidationError> {
1480 let mut errors = Vec::new();
1481
1482 for required_param in required_params {
1483 if !args.contains_key(*required_param) {
1484 let param_schema = properties.get(*required_param);
1486
1487 let description = param_schema
1488 .and_then(|schema| schema.get("description"))
1489 .and_then(|d| d.as_str())
1490 .map(|s| s.to_string());
1491
1492 let expected_type = param_schema
1493 .and_then(Self::get_expected_type)
1494 .unwrap_or_else(|| "unknown".to_string());
1495
1496 errors.push(ValidationError::MissingRequiredParameter {
1497 parameter: (*required_param).to_string(),
1498 description,
1499 expected_type,
1500 });
1501 }
1502 }
1503
1504 errors
1505 }
1506
1507 fn validate_parameter_values(
1509 args: &serde_json::Map<String, Value>,
1510 properties: &serde_json::Map<String, Value>,
1511 ) -> Vec<ValidationError> {
1512 let mut errors = Vec::new();
1513
1514 for (param_name, param_value) in args {
1515 if let Some(param_schema) = properties.get(param_name) {
1516 let schema = json!({
1518 "type": "object",
1519 "properties": {
1520 param_name: param_schema
1521 }
1522 });
1523
1524 let compiled = match jsonschema::validator_for(&schema) {
1526 Ok(compiled) => compiled,
1527 Err(e) => {
1528 errors.push(ValidationError::ConstraintViolation {
1529 parameter: param_name.clone(),
1530 message: format!(
1531 "Failed to compile schema for parameter '{param_name}': {e}"
1532 ),
1533 field_path: None,
1534 actual_value: None,
1535 expected_type: None,
1536 constraints: vec![],
1537 });
1538 continue;
1539 }
1540 };
1541
1542 let instance = json!({ param_name: param_value });
1544
1545 let validation_errors: Vec<_> =
1547 compiled.validate(&instance).err().into_iter().collect();
1548
1549 for validation_error in validation_errors {
1550 let error_message = validation_error.to_string();
1552 let instance_path_str = validation_error.instance_path.to_string();
1553 let field_path = if instance_path_str.is_empty() || instance_path_str == "/" {
1554 Some(param_name.clone())
1555 } else {
1556 Some(instance_path_str.trim_start_matches('/').to_string())
1557 };
1558
1559 let constraints = Self::extract_constraints_from_schema(param_schema);
1561
1562 let expected_type = Self::get_expected_type(param_schema);
1564
1565 errors.push(ValidationError::ConstraintViolation {
1566 parameter: param_name.clone(),
1567 message: error_message,
1568 field_path,
1569 actual_value: Some(Box::new(param_value.clone())),
1570 expected_type,
1571 constraints,
1572 });
1573 }
1574 }
1575 }
1576
1577 errors
1578 }
1579
1580 fn extract_constraints_from_schema(schema: &Value) -> Vec<ValidationConstraint> {
1582 let mut constraints = Vec::new();
1583
1584 if let Some(min_value) = schema.get("minimum").and_then(|v| v.as_f64()) {
1586 let exclusive = schema
1587 .get("exclusiveMinimum")
1588 .and_then(|v| v.as_bool())
1589 .unwrap_or(false);
1590 constraints.push(ValidationConstraint::Minimum {
1591 value: min_value,
1592 exclusive,
1593 });
1594 }
1595
1596 if let Some(max_value) = schema.get("maximum").and_then(|v| v.as_f64()) {
1598 let exclusive = schema
1599 .get("exclusiveMaximum")
1600 .and_then(|v| v.as_bool())
1601 .unwrap_or(false);
1602 constraints.push(ValidationConstraint::Maximum {
1603 value: max_value,
1604 exclusive,
1605 });
1606 }
1607
1608 if let Some(min_len) = schema
1610 .get("minLength")
1611 .and_then(|v| v.as_u64())
1612 .map(|v| v as usize)
1613 {
1614 constraints.push(ValidationConstraint::MinLength { value: min_len });
1615 }
1616
1617 if let Some(max_len) = schema
1619 .get("maxLength")
1620 .and_then(|v| v.as_u64())
1621 .map(|v| v as usize)
1622 {
1623 constraints.push(ValidationConstraint::MaxLength { value: max_len });
1624 }
1625
1626 if let Some(pattern) = schema
1628 .get("pattern")
1629 .and_then(|v| v.as_str())
1630 .map(|s| s.to_string())
1631 {
1632 constraints.push(ValidationConstraint::Pattern { pattern });
1633 }
1634
1635 if let Some(enum_values) = schema.get("enum").and_then(|v| v.as_array()).cloned() {
1637 constraints.push(ValidationConstraint::EnumValues {
1638 values: enum_values,
1639 });
1640 }
1641
1642 if let Some(format) = schema
1644 .get("format")
1645 .and_then(|v| v.as_str())
1646 .map(|s| s.to_string())
1647 {
1648 constraints.push(ValidationConstraint::Format { format });
1649 }
1650
1651 if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
1653 constraints.push(ValidationConstraint::MultipleOf { value: multiple_of });
1654 }
1655
1656 if let Some(min_items) = schema
1658 .get("minItems")
1659 .and_then(|v| v.as_u64())
1660 .map(|v| v as usize)
1661 {
1662 constraints.push(ValidationConstraint::MinItems { value: min_items });
1663 }
1664
1665 if let Some(max_items) = schema
1667 .get("maxItems")
1668 .and_then(|v| v.as_u64())
1669 .map(|v| v as usize)
1670 {
1671 constraints.push(ValidationConstraint::MaxItems { value: max_items });
1672 }
1673
1674 if let Some(true) = schema.get("uniqueItems").and_then(|v| v.as_bool()) {
1676 constraints.push(ValidationConstraint::UniqueItems);
1677 }
1678
1679 if let Some(min_props) = schema
1681 .get("minProperties")
1682 .and_then(|v| v.as_u64())
1683 .map(|v| v as usize)
1684 {
1685 constraints.push(ValidationConstraint::MinProperties { value: min_props });
1686 }
1687
1688 if let Some(max_props) = schema
1690 .get("maxProperties")
1691 .and_then(|v| v.as_u64())
1692 .map(|v| v as usize)
1693 {
1694 constraints.push(ValidationConstraint::MaxProperties { value: max_props });
1695 }
1696
1697 if let Some(const_value) = schema.get("const").cloned() {
1699 constraints.push(ValidationConstraint::ConstValue { value: const_value });
1700 }
1701
1702 if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
1704 let properties: Vec<String> = required
1705 .iter()
1706 .filter_map(|v| v.as_str().map(|s| s.to_string()))
1707 .collect();
1708 if !properties.is_empty() {
1709 constraints.push(ValidationConstraint::Required { properties });
1710 }
1711 }
1712
1713 constraints
1714 }
1715
1716 fn get_expected_type(schema: &Value) -> Option<String> {
1718 if let Some(type_value) = schema.get("type") {
1719 if let Some(type_str) = type_value.as_str() {
1720 return Some(type_str.to_string());
1721 } else if let Some(type_array) = type_value.as_array() {
1722 let types: Vec<String> = type_array
1724 .iter()
1725 .filter_map(|v| v.as_str())
1726 .map(|s| s.to_string())
1727 .collect();
1728 if !types.is_empty() {
1729 return Some(types.join(" | "));
1730 }
1731 }
1732 }
1733 None
1734 }
1735
1736 fn wrap_output_schema(
1760 body_schema: &ObjectOrReference<ObjectSchema>,
1761 spec: &Spec,
1762 ) -> Result<Value, OpenApiError> {
1763 let mut visited = HashSet::new();
1765 let body_schema_json = match body_schema {
1766 ObjectOrReference::Object(obj_schema) => {
1767 Self::convert_object_schema_to_json_schema(obj_schema, spec, &mut visited)?
1768 }
1769 ObjectOrReference::Ref { ref_path } => {
1770 let resolved = Self::resolve_reference(ref_path, spec, &mut visited)?;
1771 Self::convert_object_schema_to_json_schema(&resolved, spec, &mut visited)?
1772 }
1773 };
1774
1775 let error_schema = create_error_response_schema();
1776
1777 Ok(json!({
1778 "type": "object",
1779 "description": "Unified response structure with success and error variants",
1780 "required": ["status", "body"],
1781 "additionalProperties": false,
1782 "properties": {
1783 "status": {
1784 "type": "integer",
1785 "description": "HTTP status code",
1786 "minimum": 100,
1787 "maximum": 599
1788 },
1789 "body": {
1790 "description": "Response body - either success data or error information",
1791 "oneOf": [
1792 body_schema_json,
1793 error_schema
1794 ]
1795 }
1796 }
1797 }))
1798 }
1799}
1800
1801fn create_error_response_schema() -> Value {
1803 let root_schema = schema_for!(ErrorResponse);
1804 let schema_json = serde_json::to_value(root_schema).expect("Valid error schema");
1805
1806 let definitions = schema_json
1808 .get("$defs")
1809 .or_else(|| schema_json.get("definitions"))
1810 .cloned()
1811 .unwrap_or_else(|| json!({}));
1812
1813 let mut result = schema_json.clone();
1815 if let Some(obj) = result.as_object_mut() {
1816 obj.remove("$schema");
1817 obj.remove("$defs");
1818 obj.remove("definitions");
1819 obj.remove("title");
1820 }
1821
1822 inline_refs(&mut result, &definitions);
1824
1825 result
1826}
1827
1828fn inline_refs(schema: &mut Value, definitions: &Value) {
1830 match schema {
1831 Value::Object(obj) => {
1832 if let Some(ref_value) = obj.get("$ref").cloned()
1834 && let Some(ref_str) = ref_value.as_str()
1835 {
1836 let def_name = ref_str
1838 .strip_prefix("#/$defs/")
1839 .or_else(|| ref_str.strip_prefix("#/definitions/"));
1840
1841 if let Some(name) = def_name
1842 && let Some(definition) = definitions.get(name)
1843 {
1844 *schema = definition.clone();
1846 inline_refs(schema, definitions);
1848 return;
1849 }
1850 }
1851
1852 for (_, value) in obj.iter_mut() {
1854 inline_refs(value, definitions);
1855 }
1856 }
1857 Value::Array(arr) => {
1858 for item in arr.iter_mut() {
1860 inline_refs(item, definitions);
1861 }
1862 }
1863 _ => {} }
1865}
1866
1867#[derive(Debug, Clone)]
1869pub struct QueryParameter {
1870 pub value: Value,
1871 pub explode: bool,
1872}
1873
1874impl QueryParameter {
1875 pub fn new(value: Value, explode: bool) -> Self {
1876 Self { value, explode }
1877 }
1878}
1879
1880#[derive(Debug, Clone)]
1882pub struct ExtractedParameters {
1883 pub path: HashMap<String, Value>,
1884 pub query: HashMap<String, QueryParameter>,
1885 pub headers: HashMap<String, Value>,
1886 pub cookies: HashMap<String, Value>,
1887 pub body: HashMap<String, Value>,
1888 pub config: RequestConfig,
1889}
1890
1891#[derive(Debug, Clone)]
1893pub struct RequestConfig {
1894 pub timeout_seconds: u32,
1895 pub content_type: String,
1896}
1897
1898impl Default for RequestConfig {
1899 fn default() -> Self {
1900 Self {
1901 timeout_seconds: 30,
1902 content_type: mime::APPLICATION_JSON.to_string(),
1903 }
1904 }
1905}
1906
1907#[cfg(test)]
1908mod tests {
1909 use super::*;
1910
1911 use insta::assert_json_snapshot;
1912 use oas3::spec::{
1913 BooleanSchema, Components, MediaType, ObjectOrReference, ObjectSchema, Operation,
1914 Parameter, ParameterIn, RequestBody, Schema, SchemaType, SchemaTypeSet, Spec,
1915 };
1916 use rmcp::model::Tool;
1917 use serde_json::{Value, json};
1918 use std::collections::BTreeMap;
1919
1920 fn create_test_spec() -> Spec {
1922 Spec {
1923 openapi: "3.0.0".to_string(),
1924 info: oas3::spec::Info {
1925 title: "Test API".to_string(),
1926 version: "1.0.0".to_string(),
1927 summary: None,
1928 description: Some("Test API for unit tests".to_string()),
1929 terms_of_service: None,
1930 contact: None,
1931 license: None,
1932 extensions: Default::default(),
1933 },
1934 components: Some(Components {
1935 schemas: BTreeMap::new(),
1936 responses: BTreeMap::new(),
1937 parameters: BTreeMap::new(),
1938 examples: BTreeMap::new(),
1939 request_bodies: BTreeMap::new(),
1940 headers: BTreeMap::new(),
1941 security_schemes: BTreeMap::new(),
1942 links: BTreeMap::new(),
1943 callbacks: BTreeMap::new(),
1944 path_items: BTreeMap::new(),
1945 extensions: Default::default(),
1946 }),
1947 servers: vec![],
1948 paths: None,
1949 external_docs: None,
1950 tags: vec![],
1951 security: vec![],
1952 webhooks: BTreeMap::new(),
1953 extensions: Default::default(),
1954 }
1955 }
1956
1957 fn validate_tool_against_mcp_schema(metadata: &ToolMetadata) {
1958 let schema_content = std::fs::read_to_string("schema/2025-06-18/schema.json")
1959 .expect("Failed to read MCP schema file");
1960 let full_schema: Value =
1961 serde_json::from_str(&schema_content).expect("Failed to parse MCP schema JSON");
1962
1963 let tool_schema = json!({
1965 "$schema": "http://json-schema.org/draft-07/schema#",
1966 "definitions": full_schema.get("definitions"),
1967 "$ref": "#/definitions/Tool"
1968 });
1969
1970 let validator =
1971 jsonschema::validator_for(&tool_schema).expect("Failed to compile MCP Tool schema");
1972
1973 let tool = Tool::from(metadata);
1975
1976 let mcp_tool_json = serde_json::to_value(&tool).expect("Failed to serialize Tool to JSON");
1978
1979 let errors: Vec<String> = validator
1981 .iter_errors(&mcp_tool_json)
1982 .map(|e| e.to_string())
1983 .collect();
1984
1985 if !errors.is_empty() {
1986 panic!("Generated tool failed MCP schema validation: {errors:?}");
1987 }
1988 }
1989
1990 #[test]
1991 fn test_error_schema_structure() {
1992 let error_schema = create_error_response_schema();
1993
1994 assert!(error_schema.get("$schema").is_none());
1996 assert!(error_schema.get("definitions").is_none());
1997
1998 assert_json_snapshot!(error_schema);
2000 }
2001
2002 #[test]
2003 fn test_petstore_get_pet_by_id() {
2004 use oas3::spec::Response;
2005
2006 let mut operation = Operation {
2007 operation_id: Some("getPetById".to_string()),
2008 summary: Some("Find pet by ID".to_string()),
2009 description: Some("Returns a single pet".to_string()),
2010 tags: vec![],
2011 external_docs: None,
2012 parameters: vec![],
2013 request_body: None,
2014 responses: Default::default(),
2015 callbacks: Default::default(),
2016 deprecated: Some(false),
2017 security: vec![],
2018 servers: vec![],
2019 extensions: Default::default(),
2020 };
2021
2022 let param = Parameter {
2024 name: "petId".to_string(),
2025 location: ParameterIn::Path,
2026 description: Some("ID of pet to return".to_string()),
2027 required: Some(true),
2028 deprecated: Some(false),
2029 allow_empty_value: Some(false),
2030 style: None,
2031 explode: None,
2032 allow_reserved: Some(false),
2033 schema: Some(ObjectOrReference::Object(ObjectSchema {
2034 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2035 minimum: Some(serde_json::Number::from(1_i64)),
2036 format: Some("int64".to_string()),
2037 ..Default::default()
2038 })),
2039 example: None,
2040 examples: Default::default(),
2041 content: None,
2042 extensions: Default::default(),
2043 };
2044
2045 operation.parameters.push(ObjectOrReference::Object(param));
2046
2047 let mut responses = BTreeMap::new();
2049 let mut content = BTreeMap::new();
2050 content.insert(
2051 "application/json".to_string(),
2052 MediaType {
2053 schema: Some(ObjectOrReference::Object(ObjectSchema {
2054 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2055 properties: {
2056 let mut props = BTreeMap::new();
2057 props.insert(
2058 "id".to_string(),
2059 ObjectOrReference::Object(ObjectSchema {
2060 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2061 format: Some("int64".to_string()),
2062 ..Default::default()
2063 }),
2064 );
2065 props.insert(
2066 "name".to_string(),
2067 ObjectOrReference::Object(ObjectSchema {
2068 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2069 ..Default::default()
2070 }),
2071 );
2072 props.insert(
2073 "status".to_string(),
2074 ObjectOrReference::Object(ObjectSchema {
2075 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2076 ..Default::default()
2077 }),
2078 );
2079 props
2080 },
2081 required: vec!["id".to_string(), "name".to_string()],
2082 ..Default::default()
2083 })),
2084 examples: None,
2085 encoding: Default::default(),
2086 },
2087 );
2088
2089 responses.insert(
2090 "200".to_string(),
2091 ObjectOrReference::Object(Response {
2092 description: Some("successful operation".to_string()),
2093 headers: Default::default(),
2094 content,
2095 links: Default::default(),
2096 extensions: Default::default(),
2097 }),
2098 );
2099 operation.responses = Some(responses);
2100
2101 let spec = create_test_spec();
2102 let metadata = ToolGenerator::generate_tool_metadata(
2103 &operation,
2104 "get".to_string(),
2105 "/pet/{petId}".to_string(),
2106 &spec,
2107 )
2108 .unwrap();
2109
2110 assert_eq!(metadata.name, "getPetById");
2111 assert_eq!(metadata.method, "get");
2112 assert_eq!(metadata.path, "/pet/{petId}");
2113 assert!(metadata.description.contains("Find pet by ID"));
2114
2115 assert!(metadata.output_schema.is_some());
2117 let output_schema = metadata.output_schema.as_ref().unwrap();
2118
2119 insta::assert_json_snapshot!("test_petstore_get_pet_by_id_output_schema", output_schema);
2121
2122 validate_tool_against_mcp_schema(&metadata);
2124 }
2125
2126 #[test]
2127 fn test_convert_prefix_items_to_draft07_mixed_types() {
2128 let prefix_items = vec![
2131 ObjectOrReference::Object(ObjectSchema {
2132 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2133 format: Some("int32".to_string()),
2134 ..Default::default()
2135 }),
2136 ObjectOrReference::Object(ObjectSchema {
2137 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2138 ..Default::default()
2139 }),
2140 ];
2141
2142 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
2144
2145 let mut result = serde_json::Map::new();
2146 let spec = create_test_spec();
2147 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
2148 .unwrap();
2149
2150 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_mixed_types", result);
2152 }
2153
2154 #[test]
2155 fn test_convert_prefix_items_to_draft07_uniform_types() {
2156 let prefix_items = vec![
2158 ObjectOrReference::Object(ObjectSchema {
2159 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2160 ..Default::default()
2161 }),
2162 ObjectOrReference::Object(ObjectSchema {
2163 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2164 ..Default::default()
2165 }),
2166 ];
2167
2168 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
2170
2171 let mut result = serde_json::Map::new();
2172 let spec = create_test_spec();
2173 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
2174 .unwrap();
2175
2176 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_uniform_types", result);
2178 }
2179
2180 #[test]
2181 fn test_array_with_prefix_items_integration() {
2182 let param = Parameter {
2184 name: "coordinates".to_string(),
2185 location: ParameterIn::Query,
2186 description: Some("X,Y coordinates as tuple".to_string()),
2187 required: Some(true),
2188 deprecated: Some(false),
2189 allow_empty_value: Some(false),
2190 style: None,
2191 explode: None,
2192 allow_reserved: Some(false),
2193 schema: Some(ObjectOrReference::Object(ObjectSchema {
2194 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
2195 prefix_items: vec![
2196 ObjectOrReference::Object(ObjectSchema {
2197 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
2198 format: Some("double".to_string()),
2199 ..Default::default()
2200 }),
2201 ObjectOrReference::Object(ObjectSchema {
2202 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
2203 format: Some("double".to_string()),
2204 ..Default::default()
2205 }),
2206 ],
2207 items: Some(Box::new(Schema::Boolean(BooleanSchema(false)))),
2208 ..Default::default()
2209 })),
2210 example: None,
2211 examples: Default::default(),
2212 content: None,
2213 extensions: Default::default(),
2214 };
2215
2216 let spec = create_test_spec();
2217 let (result, _annotations) =
2218 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec).unwrap();
2219
2220 insta::assert_json_snapshot!("test_array_with_prefix_items_integration", result);
2222 }
2223
2224 #[test]
2225 fn test_array_with_regular_items_schema() {
2226 let param = Parameter {
2228 name: "tags".to_string(),
2229 location: ParameterIn::Query,
2230 description: Some("List of tags".to_string()),
2231 required: Some(false),
2232 deprecated: Some(false),
2233 allow_empty_value: Some(false),
2234 style: None,
2235 explode: None,
2236 allow_reserved: Some(false),
2237 schema: Some(ObjectOrReference::Object(ObjectSchema {
2238 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
2239 items: Some(Box::new(Schema::Object(Box::new(
2240 ObjectOrReference::Object(ObjectSchema {
2241 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2242 min_length: Some(1),
2243 max_length: Some(50),
2244 ..Default::default()
2245 }),
2246 )))),
2247 ..Default::default()
2248 })),
2249 example: None,
2250 examples: Default::default(),
2251 content: None,
2252 extensions: Default::default(),
2253 };
2254
2255 let spec = create_test_spec();
2256 let (result, _annotations) =
2257 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec).unwrap();
2258
2259 insta::assert_json_snapshot!("test_array_with_regular_items_schema", result);
2261 }
2262
2263 #[test]
2264 fn test_request_body_object_schema() {
2265 let operation = Operation {
2267 operation_id: Some("createPet".to_string()),
2268 summary: Some("Create a new pet".to_string()),
2269 description: Some("Creates a new pet in the store".to_string()),
2270 tags: vec![],
2271 external_docs: None,
2272 parameters: vec![],
2273 request_body: Some(ObjectOrReference::Object(RequestBody {
2274 description: Some("Pet object that needs to be added to the store".to_string()),
2275 content: {
2276 let mut content = BTreeMap::new();
2277 content.insert(
2278 "application/json".to_string(),
2279 MediaType {
2280 schema: Some(ObjectOrReference::Object(ObjectSchema {
2281 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2282 ..Default::default()
2283 })),
2284 examples: None,
2285 encoding: Default::default(),
2286 },
2287 );
2288 content
2289 },
2290 required: Some(true),
2291 })),
2292 responses: Default::default(),
2293 callbacks: Default::default(),
2294 deprecated: Some(false),
2295 security: vec![],
2296 servers: vec![],
2297 extensions: Default::default(),
2298 };
2299
2300 let spec = create_test_spec();
2301 let metadata = ToolGenerator::generate_tool_metadata(
2302 &operation,
2303 "post".to_string(),
2304 "/pets".to_string(),
2305 &spec,
2306 )
2307 .unwrap();
2308
2309 let properties = metadata
2311 .parameters
2312 .get("properties")
2313 .unwrap()
2314 .as_object()
2315 .unwrap();
2316 assert!(properties.contains_key("request_body"));
2317
2318 let required = metadata
2320 .parameters
2321 .get("required")
2322 .unwrap()
2323 .as_array()
2324 .unwrap();
2325 assert!(required.contains(&json!("request_body")));
2326
2327 let request_body_schema = properties.get("request_body").unwrap();
2329 insta::assert_json_snapshot!("test_request_body_object_schema", request_body_schema);
2330
2331 validate_tool_against_mcp_schema(&metadata);
2333 }
2334
2335 #[test]
2336 fn test_request_body_array_schema() {
2337 let operation = Operation {
2339 operation_id: Some("createPets".to_string()),
2340 summary: Some("Create multiple pets".to_string()),
2341 description: None,
2342 tags: vec![],
2343 external_docs: None,
2344 parameters: vec![],
2345 request_body: Some(ObjectOrReference::Object(RequestBody {
2346 description: Some("Array of pet objects".to_string()),
2347 content: {
2348 let mut content = BTreeMap::new();
2349 content.insert(
2350 "application/json".to_string(),
2351 MediaType {
2352 schema: Some(ObjectOrReference::Object(ObjectSchema {
2353 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
2354 items: Some(Box::new(Schema::Object(Box::new(
2355 ObjectOrReference::Object(ObjectSchema {
2356 schema_type: Some(SchemaTypeSet::Single(
2357 SchemaType::Object,
2358 )),
2359 ..Default::default()
2360 }),
2361 )))),
2362 ..Default::default()
2363 })),
2364 examples: None,
2365 encoding: Default::default(),
2366 },
2367 );
2368 content
2369 },
2370 required: Some(false),
2371 })),
2372 responses: Default::default(),
2373 callbacks: Default::default(),
2374 deprecated: Some(false),
2375 security: vec![],
2376 servers: vec![],
2377 extensions: Default::default(),
2378 };
2379
2380 let spec = create_test_spec();
2381 let metadata = ToolGenerator::generate_tool_metadata(
2382 &operation,
2383 "post".to_string(),
2384 "/pets/batch".to_string(),
2385 &spec,
2386 )
2387 .unwrap();
2388
2389 let properties = metadata
2391 .parameters
2392 .get("properties")
2393 .unwrap()
2394 .as_object()
2395 .unwrap();
2396 assert!(properties.contains_key("request_body"));
2397
2398 let required = metadata
2400 .parameters
2401 .get("required")
2402 .unwrap()
2403 .as_array()
2404 .unwrap();
2405 assert!(!required.contains(&json!("request_body")));
2406
2407 let request_body_schema = properties.get("request_body").unwrap();
2409 insta::assert_json_snapshot!("test_request_body_array_schema", request_body_schema);
2410
2411 validate_tool_against_mcp_schema(&metadata);
2413 }
2414
2415 #[test]
2416 fn test_request_body_string_schema() {
2417 let operation = Operation {
2419 operation_id: Some("updatePetName".to_string()),
2420 summary: Some("Update pet name".to_string()),
2421 description: None,
2422 tags: vec![],
2423 external_docs: None,
2424 parameters: vec![],
2425 request_body: Some(ObjectOrReference::Object(RequestBody {
2426 description: None,
2427 content: {
2428 let mut content = BTreeMap::new();
2429 content.insert(
2430 "text/plain".to_string(),
2431 MediaType {
2432 schema: Some(ObjectOrReference::Object(ObjectSchema {
2433 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2434 min_length: Some(1),
2435 max_length: Some(100),
2436 ..Default::default()
2437 })),
2438 examples: None,
2439 encoding: Default::default(),
2440 },
2441 );
2442 content
2443 },
2444 required: Some(true),
2445 })),
2446 responses: Default::default(),
2447 callbacks: Default::default(),
2448 deprecated: Some(false),
2449 security: vec![],
2450 servers: vec![],
2451 extensions: Default::default(),
2452 };
2453
2454 let spec = create_test_spec();
2455 let metadata = ToolGenerator::generate_tool_metadata(
2456 &operation,
2457 "put".to_string(),
2458 "/pets/{petId}/name".to_string(),
2459 &spec,
2460 )
2461 .unwrap();
2462
2463 let properties = metadata
2465 .parameters
2466 .get("properties")
2467 .unwrap()
2468 .as_object()
2469 .unwrap();
2470 let request_body_schema = properties.get("request_body").unwrap();
2471 insta::assert_json_snapshot!("test_request_body_string_schema", request_body_schema);
2472
2473 validate_tool_against_mcp_schema(&metadata);
2475 }
2476
2477 #[test]
2478 fn test_request_body_ref_schema() {
2479 let operation = Operation {
2481 operation_id: Some("updatePet".to_string()),
2482 summary: Some("Update existing pet".to_string()),
2483 description: None,
2484 tags: vec![],
2485 external_docs: None,
2486 parameters: vec![],
2487 request_body: Some(ObjectOrReference::Ref {
2488 ref_path: "#/components/requestBodies/PetBody".to_string(),
2489 }),
2490 responses: Default::default(),
2491 callbacks: Default::default(),
2492 deprecated: Some(false),
2493 security: vec![],
2494 servers: vec![],
2495 extensions: Default::default(),
2496 };
2497
2498 let spec = create_test_spec();
2499 let metadata = ToolGenerator::generate_tool_metadata(
2500 &operation,
2501 "put".to_string(),
2502 "/pets/{petId}".to_string(),
2503 &spec,
2504 )
2505 .unwrap();
2506
2507 let properties = metadata
2509 .parameters
2510 .get("properties")
2511 .unwrap()
2512 .as_object()
2513 .unwrap();
2514 let request_body_schema = properties.get("request_body").unwrap();
2515 insta::assert_json_snapshot!("test_request_body_ref_schema", request_body_schema);
2516
2517 validate_tool_against_mcp_schema(&metadata);
2519 }
2520
2521 #[test]
2522 fn test_no_request_body_for_get() {
2523 let operation = Operation {
2525 operation_id: Some("listPets".to_string()),
2526 summary: Some("List all pets".to_string()),
2527 description: None,
2528 tags: vec![],
2529 external_docs: None,
2530 parameters: vec![],
2531 request_body: None,
2532 responses: Default::default(),
2533 callbacks: Default::default(),
2534 deprecated: Some(false),
2535 security: vec![],
2536 servers: vec![],
2537 extensions: Default::default(),
2538 };
2539
2540 let spec = create_test_spec();
2541 let metadata = ToolGenerator::generate_tool_metadata(
2542 &operation,
2543 "get".to_string(),
2544 "/pets".to_string(),
2545 &spec,
2546 )
2547 .unwrap();
2548
2549 let properties = metadata
2551 .parameters
2552 .get("properties")
2553 .unwrap()
2554 .as_object()
2555 .unwrap();
2556 assert!(!properties.contains_key("request_body"));
2557
2558 validate_tool_against_mcp_schema(&metadata);
2560 }
2561
2562 #[test]
2563 fn test_request_body_simple_object_with_properties() {
2564 let operation = Operation {
2566 operation_id: Some("updatePetStatus".to_string()),
2567 summary: Some("Update pet status".to_string()),
2568 description: None,
2569 tags: vec![],
2570 external_docs: None,
2571 parameters: vec![],
2572 request_body: Some(ObjectOrReference::Object(RequestBody {
2573 description: Some("Pet status update".to_string()),
2574 content: {
2575 let mut content = BTreeMap::new();
2576 content.insert(
2577 "application/json".to_string(),
2578 MediaType {
2579 schema: Some(ObjectOrReference::Object(ObjectSchema {
2580 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2581 properties: {
2582 let mut props = BTreeMap::new();
2583 props.insert(
2584 "status".to_string(),
2585 ObjectOrReference::Object(ObjectSchema {
2586 schema_type: Some(SchemaTypeSet::Single(
2587 SchemaType::String,
2588 )),
2589 ..Default::default()
2590 }),
2591 );
2592 props.insert(
2593 "reason".to_string(),
2594 ObjectOrReference::Object(ObjectSchema {
2595 schema_type: Some(SchemaTypeSet::Single(
2596 SchemaType::String,
2597 )),
2598 ..Default::default()
2599 }),
2600 );
2601 props
2602 },
2603 required: vec!["status".to_string()],
2604 ..Default::default()
2605 })),
2606 examples: None,
2607 encoding: Default::default(),
2608 },
2609 );
2610 content
2611 },
2612 required: Some(false),
2613 })),
2614 responses: Default::default(),
2615 callbacks: Default::default(),
2616 deprecated: Some(false),
2617 security: vec![],
2618 servers: vec![],
2619 extensions: Default::default(),
2620 };
2621
2622 let spec = create_test_spec();
2623 let metadata = ToolGenerator::generate_tool_metadata(
2624 &operation,
2625 "patch".to_string(),
2626 "/pets/{petId}/status".to_string(),
2627 &spec,
2628 )
2629 .unwrap();
2630
2631 let properties = metadata
2633 .parameters
2634 .get("properties")
2635 .unwrap()
2636 .as_object()
2637 .unwrap();
2638 let request_body_schema = properties.get("request_body").unwrap();
2639 insta::assert_json_snapshot!(
2640 "test_request_body_simple_object_with_properties",
2641 request_body_schema
2642 );
2643
2644 let required = metadata
2646 .parameters
2647 .get("required")
2648 .unwrap()
2649 .as_array()
2650 .unwrap();
2651 assert!(!required.contains(&json!("request_body")));
2652
2653 validate_tool_against_mcp_schema(&metadata);
2655 }
2656
2657 #[test]
2658 fn test_request_body_with_nested_properties() {
2659 let operation = Operation {
2661 operation_id: Some("createUser".to_string()),
2662 summary: Some("Create a new user".to_string()),
2663 description: None,
2664 tags: vec![],
2665 external_docs: None,
2666 parameters: vec![],
2667 request_body: Some(ObjectOrReference::Object(RequestBody {
2668 description: Some("User creation data".to_string()),
2669 content: {
2670 let mut content = BTreeMap::new();
2671 content.insert(
2672 "application/json".to_string(),
2673 MediaType {
2674 schema: Some(ObjectOrReference::Object(ObjectSchema {
2675 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2676 properties: {
2677 let mut props = BTreeMap::new();
2678 props.insert(
2679 "name".to_string(),
2680 ObjectOrReference::Object(ObjectSchema {
2681 schema_type: Some(SchemaTypeSet::Single(
2682 SchemaType::String,
2683 )),
2684 ..Default::default()
2685 }),
2686 );
2687 props.insert(
2688 "age".to_string(),
2689 ObjectOrReference::Object(ObjectSchema {
2690 schema_type: Some(SchemaTypeSet::Single(
2691 SchemaType::Integer,
2692 )),
2693 minimum: Some(serde_json::Number::from(0)),
2694 maximum: Some(serde_json::Number::from(150)),
2695 ..Default::default()
2696 }),
2697 );
2698 props
2699 },
2700 required: vec!["name".to_string()],
2701 ..Default::default()
2702 })),
2703 examples: None,
2704 encoding: Default::default(),
2705 },
2706 );
2707 content
2708 },
2709 required: Some(true),
2710 })),
2711 responses: Default::default(),
2712 callbacks: Default::default(),
2713 deprecated: Some(false),
2714 security: vec![],
2715 servers: vec![],
2716 extensions: Default::default(),
2717 };
2718
2719 let spec = create_test_spec();
2720 let metadata = ToolGenerator::generate_tool_metadata(
2721 &operation,
2722 "post".to_string(),
2723 "/users".to_string(),
2724 &spec,
2725 )
2726 .unwrap();
2727
2728 let properties = metadata
2730 .parameters
2731 .get("properties")
2732 .unwrap()
2733 .as_object()
2734 .unwrap();
2735 let request_body_schema = properties.get("request_body").unwrap();
2736 insta::assert_json_snapshot!(
2737 "test_request_body_with_nested_properties",
2738 request_body_schema
2739 );
2740
2741 validate_tool_against_mcp_schema(&metadata);
2743 }
2744
2745 #[test]
2746 fn test_operation_without_responses_has_no_output_schema() {
2747 let operation = Operation {
2748 operation_id: Some("testOperation".to_string()),
2749 summary: Some("Test operation".to_string()),
2750 description: None,
2751 tags: vec![],
2752 external_docs: None,
2753 parameters: vec![],
2754 request_body: None,
2755 responses: None,
2756 callbacks: Default::default(),
2757 deprecated: Some(false),
2758 security: vec![],
2759 servers: vec![],
2760 extensions: Default::default(),
2761 };
2762
2763 let spec = create_test_spec();
2764 let metadata = ToolGenerator::generate_tool_metadata(
2765 &operation,
2766 "get".to_string(),
2767 "/test".to_string(),
2768 &spec,
2769 )
2770 .unwrap();
2771
2772 assert!(metadata.output_schema.is_none());
2774
2775 validate_tool_against_mcp_schema(&metadata);
2777 }
2778
2779 #[test]
2780 fn test_extract_output_schema_with_200_response() {
2781 use oas3::spec::Response;
2782
2783 let mut responses = BTreeMap::new();
2785 let mut content = BTreeMap::new();
2786 content.insert(
2787 "application/json".to_string(),
2788 MediaType {
2789 schema: Some(ObjectOrReference::Object(ObjectSchema {
2790 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2791 properties: {
2792 let mut props = BTreeMap::new();
2793 props.insert(
2794 "id".to_string(),
2795 ObjectOrReference::Object(ObjectSchema {
2796 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2797 ..Default::default()
2798 }),
2799 );
2800 props.insert(
2801 "name".to_string(),
2802 ObjectOrReference::Object(ObjectSchema {
2803 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2804 ..Default::default()
2805 }),
2806 );
2807 props
2808 },
2809 required: vec!["id".to_string(), "name".to_string()],
2810 ..Default::default()
2811 })),
2812 examples: None,
2813 encoding: Default::default(),
2814 },
2815 );
2816
2817 responses.insert(
2818 "200".to_string(),
2819 ObjectOrReference::Object(Response {
2820 description: Some("Successful response".to_string()),
2821 headers: Default::default(),
2822 content,
2823 links: Default::default(),
2824 extensions: Default::default(),
2825 }),
2826 );
2827
2828 let spec = create_test_spec();
2829 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
2830
2831 insta::assert_json_snapshot!(result);
2833 }
2834
2835 #[test]
2836 fn test_extract_output_schema_with_201_response() {
2837 use oas3::spec::Response;
2838
2839 let mut responses = BTreeMap::new();
2841 let mut content = BTreeMap::new();
2842 content.insert(
2843 "application/json".to_string(),
2844 MediaType {
2845 schema: Some(ObjectOrReference::Object(ObjectSchema {
2846 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2847 properties: {
2848 let mut props = BTreeMap::new();
2849 props.insert(
2850 "created".to_string(),
2851 ObjectOrReference::Object(ObjectSchema {
2852 schema_type: Some(SchemaTypeSet::Single(SchemaType::Boolean)),
2853 ..Default::default()
2854 }),
2855 );
2856 props
2857 },
2858 ..Default::default()
2859 })),
2860 examples: None,
2861 encoding: Default::default(),
2862 },
2863 );
2864
2865 responses.insert(
2866 "201".to_string(),
2867 ObjectOrReference::Object(Response {
2868 description: Some("Created".to_string()),
2869 headers: Default::default(),
2870 content,
2871 links: Default::default(),
2872 extensions: Default::default(),
2873 }),
2874 );
2875
2876 let spec = create_test_spec();
2877 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
2878
2879 insta::assert_json_snapshot!(result);
2881 }
2882
2883 #[test]
2884 fn test_extract_output_schema_with_2xx_response() {
2885 use oas3::spec::Response;
2886
2887 let mut responses = BTreeMap::new();
2889 let mut content = BTreeMap::new();
2890 content.insert(
2891 "application/json".to_string(),
2892 MediaType {
2893 schema: Some(ObjectOrReference::Object(ObjectSchema {
2894 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
2895 items: Some(Box::new(Schema::Object(Box::new(
2896 ObjectOrReference::Object(ObjectSchema {
2897 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2898 ..Default::default()
2899 }),
2900 )))),
2901 ..Default::default()
2902 })),
2903 examples: None,
2904 encoding: Default::default(),
2905 },
2906 );
2907
2908 responses.insert(
2909 "2XX".to_string(),
2910 ObjectOrReference::Object(Response {
2911 description: Some("Success".to_string()),
2912 headers: Default::default(),
2913 content,
2914 links: Default::default(),
2915 extensions: Default::default(),
2916 }),
2917 );
2918
2919 let spec = create_test_spec();
2920 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
2921
2922 insta::assert_json_snapshot!(result);
2924 }
2925
2926 #[test]
2927 fn test_extract_output_schema_no_responses() {
2928 let spec = create_test_spec();
2929 let result = ToolGenerator::extract_output_schema(&None, &spec).unwrap();
2930
2931 insta::assert_json_snapshot!(result);
2933 }
2934
2935 #[test]
2936 fn test_extract_output_schema_only_error_responses() {
2937 use oas3::spec::Response;
2938
2939 let mut responses = BTreeMap::new();
2941 responses.insert(
2942 "404".to_string(),
2943 ObjectOrReference::Object(Response {
2944 description: Some("Not found".to_string()),
2945 headers: Default::default(),
2946 content: Default::default(),
2947 links: Default::default(),
2948 extensions: Default::default(),
2949 }),
2950 );
2951 responses.insert(
2952 "500".to_string(),
2953 ObjectOrReference::Object(Response {
2954 description: Some("Server error".to_string()),
2955 headers: Default::default(),
2956 content: Default::default(),
2957 links: Default::default(),
2958 extensions: Default::default(),
2959 }),
2960 );
2961
2962 let spec = create_test_spec();
2963 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
2964
2965 insta::assert_json_snapshot!(result);
2967 }
2968
2969 #[test]
2970 fn test_extract_output_schema_with_ref() {
2971 use oas3::spec::Response;
2972
2973 let mut spec = create_test_spec();
2975 let mut schemas = BTreeMap::new();
2976 schemas.insert(
2977 "Pet".to_string(),
2978 ObjectOrReference::Object(ObjectSchema {
2979 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2980 properties: {
2981 let mut props = BTreeMap::new();
2982 props.insert(
2983 "name".to_string(),
2984 ObjectOrReference::Object(ObjectSchema {
2985 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2986 ..Default::default()
2987 }),
2988 );
2989 props
2990 },
2991 ..Default::default()
2992 }),
2993 );
2994 spec.components.as_mut().unwrap().schemas = schemas;
2995
2996 let mut responses = BTreeMap::new();
2998 let mut content = BTreeMap::new();
2999 content.insert(
3000 "application/json".to_string(),
3001 MediaType {
3002 schema: Some(ObjectOrReference::Ref {
3003 ref_path: "#/components/schemas/Pet".to_string(),
3004 }),
3005 examples: None,
3006 encoding: Default::default(),
3007 },
3008 );
3009
3010 responses.insert(
3011 "200".to_string(),
3012 ObjectOrReference::Object(Response {
3013 description: Some("Success".to_string()),
3014 headers: Default::default(),
3015 content,
3016 links: Default::default(),
3017 extensions: Default::default(),
3018 }),
3019 );
3020
3021 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
3022
3023 insta::assert_json_snapshot!(result);
3025 }
3026
3027 #[test]
3028 fn test_generate_tool_metadata_includes_output_schema() {
3029 use oas3::spec::Response;
3030
3031 let mut operation = Operation {
3032 operation_id: Some("getPet".to_string()),
3033 summary: Some("Get a pet".to_string()),
3034 description: None,
3035 tags: vec![],
3036 external_docs: None,
3037 parameters: vec![],
3038 request_body: None,
3039 responses: Default::default(),
3040 callbacks: Default::default(),
3041 deprecated: Some(false),
3042 security: vec![],
3043 servers: vec![],
3044 extensions: Default::default(),
3045 };
3046
3047 let mut responses = BTreeMap::new();
3049 let mut content = BTreeMap::new();
3050 content.insert(
3051 "application/json".to_string(),
3052 MediaType {
3053 schema: Some(ObjectOrReference::Object(ObjectSchema {
3054 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3055 properties: {
3056 let mut props = BTreeMap::new();
3057 props.insert(
3058 "id".to_string(),
3059 ObjectOrReference::Object(ObjectSchema {
3060 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3061 ..Default::default()
3062 }),
3063 );
3064 props
3065 },
3066 ..Default::default()
3067 })),
3068 examples: None,
3069 encoding: Default::default(),
3070 },
3071 );
3072
3073 responses.insert(
3074 "200".to_string(),
3075 ObjectOrReference::Object(Response {
3076 description: Some("Success".to_string()),
3077 headers: Default::default(),
3078 content,
3079 links: Default::default(),
3080 extensions: Default::default(),
3081 }),
3082 );
3083 operation.responses = Some(responses);
3084
3085 let spec = create_test_spec();
3086 let metadata = ToolGenerator::generate_tool_metadata(
3087 &operation,
3088 "get".to_string(),
3089 "/pets/{id}".to_string(),
3090 &spec,
3091 )
3092 .unwrap();
3093
3094 assert!(metadata.output_schema.is_some());
3096 let output_schema = metadata.output_schema.as_ref().unwrap();
3097
3098 insta::assert_json_snapshot!(
3100 "test_generate_tool_metadata_includes_output_schema",
3101 output_schema
3102 );
3103
3104 validate_tool_against_mcp_schema(&metadata);
3106 }
3107
3108 #[test]
3109 fn test_sanitize_property_name() {
3110 assert_eq!(sanitize_property_name("user name"), "user_name");
3112 assert_eq!(
3113 sanitize_property_name("first name last name"),
3114 "first_name_last_name"
3115 );
3116
3117 assert_eq!(sanitize_property_name("user(admin)"), "user_admin");
3119 assert_eq!(sanitize_property_name("user[admin]"), "user_admin");
3120 assert_eq!(sanitize_property_name("price($)"), "price");
3121 assert_eq!(sanitize_property_name("email@address"), "email_address");
3122 assert_eq!(sanitize_property_name("item#1"), "item_1");
3123 assert_eq!(sanitize_property_name("a/b/c"), "a_b_c");
3124
3125 assert_eq!(sanitize_property_name("user_name"), "user_name");
3127 assert_eq!(sanitize_property_name("userName123"), "userName123");
3128 assert_eq!(sanitize_property_name("user.name"), "user.name");
3129 assert_eq!(sanitize_property_name("user-name"), "user-name");
3130
3131 assert_eq!(sanitize_property_name("123name"), "param_123name");
3133 assert_eq!(sanitize_property_name("1st_place"), "param_1st_place");
3134
3135 assert_eq!(sanitize_property_name(""), "param_");
3137
3138 let long_name = "a".repeat(100);
3140 assert_eq!(sanitize_property_name(&long_name).len(), 64);
3141
3142 assert_eq!(sanitize_property_name("!@#$%^&*()"), "param_");
3145 }
3146
3147 #[test]
3148 fn test_sanitize_property_name_trailing_underscores() {
3149 assert_eq!(sanitize_property_name("page[size]"), "page_size");
3151 assert_eq!(sanitize_property_name("user[id]"), "user_id");
3152 assert_eq!(sanitize_property_name("field[]"), "field");
3153
3154 assert_eq!(sanitize_property_name("field___"), "field");
3156 assert_eq!(sanitize_property_name("test[[["), "test");
3157 }
3158
3159 #[test]
3160 fn test_sanitize_property_name_consecutive_underscores() {
3161 assert_eq!(sanitize_property_name("user__name"), "user_name");
3163 assert_eq!(sanitize_property_name("first___last"), "first_last");
3164 assert_eq!(sanitize_property_name("a____b____c"), "a_b_c");
3165
3166 assert_eq!(sanitize_property_name("user[[name]]"), "user_name");
3168 assert_eq!(sanitize_property_name("field@#$value"), "field_value");
3169 }
3170
3171 #[test]
3172 fn test_sanitize_property_name_edge_cases() {
3173 assert_eq!(sanitize_property_name("_private"), "_private");
3175 assert_eq!(sanitize_property_name("__dunder"), "_dunder");
3176
3177 assert_eq!(sanitize_property_name("[[["), "param_");
3179 assert_eq!(sanitize_property_name("@@@"), "param_");
3180
3181 assert_eq!(sanitize_property_name(""), "param_");
3183
3184 assert_eq!(sanitize_property_name("_field[size]"), "_field_size");
3186 assert_eq!(sanitize_property_name("__test__"), "_test");
3187 }
3188
3189 #[test]
3190 fn test_sanitize_property_name_complex_cases() {
3191 assert_eq!(sanitize_property_name("page[size]"), "page_size");
3193 assert_eq!(sanitize_property_name("filter[status]"), "filter_status");
3194 assert_eq!(
3195 sanitize_property_name("sort[-created_at]"),
3196 "sort_-created_at"
3197 );
3198 assert_eq!(
3199 sanitize_property_name("include[author.posts]"),
3200 "include_author.posts"
3201 );
3202
3203 let long_name = "very_long_field_name_with_special[characters]_that_needs_truncation_____";
3205 let expected = "very_long_field_name_with_special_characters_that_needs_truncat";
3206 assert_eq!(sanitize_property_name(long_name), expected);
3207 }
3208
3209 #[test]
3210 fn test_property_sanitization_with_annotations() {
3211 let spec = create_test_spec();
3212 let mut visited = HashSet::new();
3213
3214 let obj_schema = ObjectSchema {
3216 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3217 properties: {
3218 let mut props = BTreeMap::new();
3219 props.insert(
3221 "user name".to_string(),
3222 ObjectOrReference::Object(ObjectSchema {
3223 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3224 ..Default::default()
3225 }),
3226 );
3227 props.insert(
3229 "price($)".to_string(),
3230 ObjectOrReference::Object(ObjectSchema {
3231 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
3232 ..Default::default()
3233 }),
3234 );
3235 props.insert(
3237 "validName".to_string(),
3238 ObjectOrReference::Object(ObjectSchema {
3239 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3240 ..Default::default()
3241 }),
3242 );
3243 props
3244 },
3245 ..Default::default()
3246 };
3247
3248 let result =
3249 ToolGenerator::convert_object_schema_to_json_schema(&obj_schema, &spec, &mut visited)
3250 .unwrap();
3251
3252 insta::assert_json_snapshot!("test_property_sanitization_with_annotations", result);
3254 }
3255
3256 #[test]
3257 fn test_parameter_sanitization_and_extraction() {
3258 let spec = create_test_spec();
3259
3260 let operation = Operation {
3262 operation_id: Some("testOp".to_string()),
3263 parameters: vec![
3264 ObjectOrReference::Object(Parameter {
3266 name: "user(id)".to_string(),
3267 location: ParameterIn::Path,
3268 description: Some("User ID".to_string()),
3269 required: Some(true),
3270 deprecated: Some(false),
3271 allow_empty_value: Some(false),
3272 style: None,
3273 explode: None,
3274 allow_reserved: Some(false),
3275 schema: Some(ObjectOrReference::Object(ObjectSchema {
3276 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3277 ..Default::default()
3278 })),
3279 example: None,
3280 examples: Default::default(),
3281 content: None,
3282 extensions: Default::default(),
3283 }),
3284 ObjectOrReference::Object(Parameter {
3286 name: "page size".to_string(),
3287 location: ParameterIn::Query,
3288 description: Some("Page size".to_string()),
3289 required: Some(false),
3290 deprecated: Some(false),
3291 allow_empty_value: Some(false),
3292 style: None,
3293 explode: None,
3294 allow_reserved: Some(false),
3295 schema: Some(ObjectOrReference::Object(ObjectSchema {
3296 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3297 ..Default::default()
3298 })),
3299 example: None,
3300 examples: Default::default(),
3301 content: None,
3302 extensions: Default::default(),
3303 }),
3304 ObjectOrReference::Object(Parameter {
3306 name: "auth-token!".to_string(),
3307 location: ParameterIn::Header,
3308 description: Some("Auth token".to_string()),
3309 required: Some(false),
3310 deprecated: Some(false),
3311 allow_empty_value: Some(false),
3312 style: None,
3313 explode: None,
3314 allow_reserved: Some(false),
3315 schema: Some(ObjectOrReference::Object(ObjectSchema {
3316 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3317 ..Default::default()
3318 })),
3319 example: None,
3320 examples: Default::default(),
3321 content: None,
3322 extensions: Default::default(),
3323 }),
3324 ],
3325 ..Default::default()
3326 };
3327
3328 let tool_metadata = ToolGenerator::generate_tool_metadata(
3329 &operation,
3330 "get".to_string(),
3331 "/users/{user(id)}".to_string(),
3332 &spec,
3333 )
3334 .unwrap();
3335
3336 let properties = tool_metadata
3338 .parameters
3339 .get("properties")
3340 .unwrap()
3341 .as_object()
3342 .unwrap();
3343
3344 assert!(properties.contains_key("user_id"));
3345 assert!(properties.contains_key("page_size"));
3346 assert!(properties.contains_key("header_auth-token"));
3347
3348 let required = tool_metadata
3350 .parameters
3351 .get("required")
3352 .unwrap()
3353 .as_array()
3354 .unwrap();
3355 assert!(required.contains(&json!("user_id")));
3356
3357 let arguments = json!({
3359 "user_id": "123",
3360 "page_size": 10,
3361 "header_auth-token": "secret"
3362 });
3363
3364 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
3365
3366 assert_eq!(extracted.path.get("user(id)"), Some(&json!("123")));
3368
3369 assert_eq!(
3371 extracted.query.get("page size").map(|q| &q.value),
3372 Some(&json!(10))
3373 );
3374
3375 assert_eq!(extracted.headers.get("auth-token!"), Some(&json!("secret")));
3377 }
3378
3379 #[test]
3380 fn test_check_unknown_parameters() {
3381 let mut properties = serde_json::Map::new();
3383 properties.insert("page_size".to_string(), json!({"type": "integer"}));
3384 properties.insert("user_id".to_string(), json!({"type": "string"}));
3385
3386 let mut args = serde_json::Map::new();
3387 args.insert("page_sixe".to_string(), json!(10)); let result = ToolGenerator::check_unknown_parameters(&args, &properties);
3390 assert!(!result.is_empty());
3391 assert_eq!(result.len(), 1);
3392
3393 match &result[0] {
3394 ValidationError::InvalidParameter {
3395 parameter,
3396 suggestions,
3397 valid_parameters,
3398 } => {
3399 assert_eq!(parameter, "page_sixe");
3400 assert_eq!(suggestions, &vec!["page_size".to_string()]);
3401 assert_eq!(
3402 valid_parameters,
3403 &vec!["page_size".to_string(), "user_id".to_string()]
3404 );
3405 }
3406 _ => panic!("Expected InvalidParameter variant"),
3407 }
3408 }
3409
3410 #[test]
3411 fn test_check_unknown_parameters_no_suggestions() {
3412 let mut properties = serde_json::Map::new();
3414 properties.insert("limit".to_string(), json!({"type": "integer"}));
3415 properties.insert("offset".to_string(), json!({"type": "integer"}));
3416
3417 let mut args = serde_json::Map::new();
3418 args.insert("xyz123".to_string(), json!("value"));
3419
3420 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
3421 assert!(!result.is_empty());
3422 assert_eq!(result.len(), 1);
3423
3424 match &result[0] {
3425 ValidationError::InvalidParameter {
3426 parameter,
3427 suggestions,
3428 valid_parameters,
3429 } => {
3430 assert_eq!(parameter, "xyz123");
3431 assert!(suggestions.is_empty());
3432 assert!(valid_parameters.contains(&"limit".to_string()));
3433 assert!(valid_parameters.contains(&"offset".to_string()));
3434 }
3435 _ => panic!("Expected InvalidParameter variant"),
3436 }
3437 }
3438
3439 #[test]
3440 fn test_check_unknown_parameters_multiple_suggestions() {
3441 let mut properties = serde_json::Map::new();
3443 properties.insert("user_id".to_string(), json!({"type": "string"}));
3444 properties.insert("user_iid".to_string(), json!({"type": "string"}));
3445 properties.insert("user_name".to_string(), json!({"type": "string"}));
3446
3447 let mut args = serde_json::Map::new();
3448 args.insert("usr_id".to_string(), json!("123"));
3449
3450 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
3451 assert!(!result.is_empty());
3452 assert_eq!(result.len(), 1);
3453
3454 match &result[0] {
3455 ValidationError::InvalidParameter {
3456 parameter,
3457 suggestions,
3458 valid_parameters,
3459 } => {
3460 assert_eq!(parameter, "usr_id");
3461 assert!(!suggestions.is_empty());
3462 assert!(suggestions.contains(&"user_id".to_string()));
3463 assert_eq!(valid_parameters.len(), 3);
3464 }
3465 _ => panic!("Expected InvalidParameter variant"),
3466 }
3467 }
3468
3469 #[test]
3470 fn test_check_unknown_parameters_valid() {
3471 let mut properties = serde_json::Map::new();
3473 properties.insert("name".to_string(), json!({"type": "string"}));
3474 properties.insert("email".to_string(), json!({"type": "string"}));
3475
3476 let mut args = serde_json::Map::new();
3477 args.insert("name".to_string(), json!("John"));
3478 args.insert("email".to_string(), json!("john@example.com"));
3479
3480 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
3481 assert!(result.is_empty());
3482 }
3483
3484 #[test]
3485 fn test_check_unknown_parameters_empty() {
3486 let properties = serde_json::Map::new();
3488
3489 let mut args = serde_json::Map::new();
3490 args.insert("any_param".to_string(), json!("value"));
3491
3492 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
3493 assert!(!result.is_empty());
3494 assert_eq!(result.len(), 1);
3495
3496 match &result[0] {
3497 ValidationError::InvalidParameter {
3498 parameter,
3499 suggestions,
3500 valid_parameters,
3501 } => {
3502 assert_eq!(parameter, "any_param");
3503 assert!(suggestions.is_empty());
3504 assert!(valid_parameters.is_empty());
3505 }
3506 _ => panic!("Expected InvalidParameter variant"),
3507 }
3508 }
3509
3510 #[test]
3511 fn test_check_unknown_parameters_gltf_pagination() {
3512 let mut properties = serde_json::Map::new();
3514 properties.insert(
3515 "page_number".to_string(),
3516 json!({
3517 "type": "integer",
3518 "x-original-name": "page[number]"
3519 }),
3520 );
3521 properties.insert(
3522 "page_size".to_string(),
3523 json!({
3524 "type": "integer",
3525 "x-original-name": "page[size]"
3526 }),
3527 );
3528
3529 let mut args = serde_json::Map::new();
3531 args.insert("page".to_string(), json!(1));
3532 args.insert("per_page".to_string(), json!(10));
3533
3534 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
3535 assert_eq!(result.len(), 2, "Should have 2 unknown parameters");
3536
3537 let page_error = result
3539 .iter()
3540 .find(|e| {
3541 if let ValidationError::InvalidParameter { parameter, .. } = e {
3542 parameter == "page"
3543 } else {
3544 false
3545 }
3546 })
3547 .expect("Should have error for 'page'");
3548
3549 let per_page_error = result
3550 .iter()
3551 .find(|e| {
3552 if let ValidationError::InvalidParameter { parameter, .. } = e {
3553 parameter == "per_page"
3554 } else {
3555 false
3556 }
3557 })
3558 .expect("Should have error for 'per_page'");
3559
3560 match page_error {
3562 ValidationError::InvalidParameter {
3563 suggestions,
3564 valid_parameters,
3565 ..
3566 } => {
3567 assert!(
3568 suggestions.contains(&"page_number".to_string()),
3569 "Should suggest 'page_number' for 'page'"
3570 );
3571 assert_eq!(valid_parameters.len(), 2);
3572 assert!(valid_parameters.contains(&"page_number".to_string()));
3573 assert!(valid_parameters.contains(&"page_size".to_string()));
3574 }
3575 _ => panic!("Expected InvalidParameter"),
3576 }
3577
3578 match per_page_error {
3580 ValidationError::InvalidParameter {
3581 parameter,
3582 suggestions,
3583 valid_parameters,
3584 ..
3585 } => {
3586 assert_eq!(parameter, "per_page");
3587 assert_eq!(valid_parameters.len(), 2);
3588 if !suggestions.is_empty() {
3591 assert!(suggestions.contains(&"page_size".to_string()));
3592 }
3593 }
3594 _ => panic!("Expected InvalidParameter"),
3595 }
3596 }
3597
3598 #[test]
3599 fn test_validate_parameters_with_invalid_params() {
3600 let tool_metadata = ToolMetadata {
3602 name: "listItems".to_string(),
3603 title: None,
3604 description: "List items".to_string(),
3605 parameters: json!({
3606 "type": "object",
3607 "properties": {
3608 "page_number": {
3609 "type": "integer",
3610 "x-original-name": "page[number]"
3611 },
3612 "page_size": {
3613 "type": "integer",
3614 "x-original-name": "page[size]"
3615 }
3616 },
3617 "required": []
3618 }),
3619 output_schema: None,
3620 method: "GET".to_string(),
3621 path: "/items".to_string(),
3622 };
3623
3624 let arguments = json!({
3626 "page": 1,
3627 "per_page": 10
3628 });
3629
3630 let result = ToolGenerator::validate_parameters(&tool_metadata, &arguments);
3631 assert!(
3632 result.is_err(),
3633 "Should fail validation with unknown parameters"
3634 );
3635
3636 let error = result.unwrap_err();
3637 match error {
3638 ToolCallValidationError::InvalidParameters { violations } => {
3639 assert_eq!(violations.len(), 2, "Should have 2 validation errors");
3640
3641 let has_page_error = violations.iter().any(|v| {
3643 if let ValidationError::InvalidParameter { parameter, .. } = v {
3644 parameter == "page"
3645 } else {
3646 false
3647 }
3648 });
3649
3650 let has_per_page_error = violations.iter().any(|v| {
3651 if let ValidationError::InvalidParameter { parameter, .. } = v {
3652 parameter == "per_page"
3653 } else {
3654 false
3655 }
3656 });
3657
3658 assert!(has_page_error, "Should have error for 'page' parameter");
3659 assert!(
3660 has_per_page_error,
3661 "Should have error for 'per_page' parameter"
3662 );
3663 }
3664 _ => panic!("Expected InvalidParameters"),
3665 }
3666 }
3667
3668 #[test]
3669 fn test_cookie_parameter_sanitization() {
3670 let spec = create_test_spec();
3671
3672 let operation = Operation {
3673 operation_id: Some("testCookie".to_string()),
3674 parameters: vec![ObjectOrReference::Object(Parameter {
3675 name: "session[id]".to_string(),
3676 location: ParameterIn::Cookie,
3677 description: Some("Session ID".to_string()),
3678 required: Some(false),
3679 deprecated: Some(false),
3680 allow_empty_value: Some(false),
3681 style: None,
3682 explode: None,
3683 allow_reserved: Some(false),
3684 schema: Some(ObjectOrReference::Object(ObjectSchema {
3685 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3686 ..Default::default()
3687 })),
3688 example: None,
3689 examples: Default::default(),
3690 content: None,
3691 extensions: Default::default(),
3692 })],
3693 ..Default::default()
3694 };
3695
3696 let tool_metadata = ToolGenerator::generate_tool_metadata(
3697 &operation,
3698 "get".to_string(),
3699 "/data".to_string(),
3700 &spec,
3701 )
3702 .unwrap();
3703
3704 let properties = tool_metadata
3705 .parameters
3706 .get("properties")
3707 .unwrap()
3708 .as_object()
3709 .unwrap();
3710
3711 assert!(properties.contains_key("cookie_session_id"));
3713
3714 let arguments = json!({
3716 "cookie_session_id": "abc123"
3717 });
3718
3719 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
3720
3721 assert_eq!(extracted.cookies.get("session[id]"), Some(&json!("abc123")));
3723 }
3724
3725 #[test]
3726 fn test_parameter_description_with_examples() {
3727 let spec = create_test_spec();
3728
3729 let param_with_example = Parameter {
3731 name: "status".to_string(),
3732 location: ParameterIn::Query,
3733 description: Some("Filter by status".to_string()),
3734 required: Some(false),
3735 deprecated: Some(false),
3736 allow_empty_value: Some(false),
3737 style: None,
3738 explode: None,
3739 allow_reserved: Some(false),
3740 schema: Some(ObjectOrReference::Object(ObjectSchema {
3741 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3742 ..Default::default()
3743 })),
3744 example: Some(json!("active")),
3745 examples: Default::default(),
3746 content: None,
3747 extensions: Default::default(),
3748 };
3749
3750 let (schema, _) =
3751 ToolGenerator::convert_parameter_schema(¶m_with_example, ParameterIn::Query, &spec)
3752 .unwrap();
3753 let description = schema.get("description").unwrap().as_str().unwrap();
3754 assert_eq!(description, "Filter by status. Example: `\"active\"`");
3755
3756 let mut examples_map = std::collections::BTreeMap::new();
3758 examples_map.insert(
3759 "example1".to_string(),
3760 ObjectOrReference::Object(oas3::spec::Example {
3761 value: Some(json!("pending")),
3762 ..Default::default()
3763 }),
3764 );
3765 examples_map.insert(
3766 "example2".to_string(),
3767 ObjectOrReference::Object(oas3::spec::Example {
3768 value: Some(json!("completed")),
3769 ..Default::default()
3770 }),
3771 );
3772
3773 let param_with_examples = Parameter {
3774 name: "status".to_string(),
3775 location: ParameterIn::Query,
3776 description: Some("Filter by status".to_string()),
3777 required: Some(false),
3778 deprecated: Some(false),
3779 allow_empty_value: Some(false),
3780 style: None,
3781 explode: None,
3782 allow_reserved: Some(false),
3783 schema: Some(ObjectOrReference::Object(ObjectSchema {
3784 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3785 ..Default::default()
3786 })),
3787 example: None,
3788 examples: examples_map,
3789 content: None,
3790 extensions: Default::default(),
3791 };
3792
3793 let (schema, _) = ToolGenerator::convert_parameter_schema(
3794 ¶m_with_examples,
3795 ParameterIn::Query,
3796 &spec,
3797 )
3798 .unwrap();
3799 let description = schema.get("description").unwrap().as_str().unwrap();
3800 assert!(description.starts_with("Filter by status. Examples:\n"));
3801 assert!(description.contains("`\"pending\"`"));
3802 assert!(description.contains("`\"completed\"`"));
3803
3804 let param_no_desc = Parameter {
3806 name: "limit".to_string(),
3807 location: ParameterIn::Query,
3808 description: None,
3809 required: Some(false),
3810 deprecated: Some(false),
3811 allow_empty_value: Some(false),
3812 style: None,
3813 explode: None,
3814 allow_reserved: Some(false),
3815 schema: Some(ObjectOrReference::Object(ObjectSchema {
3816 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3817 ..Default::default()
3818 })),
3819 example: Some(json!(100)),
3820 examples: Default::default(),
3821 content: None,
3822 extensions: Default::default(),
3823 };
3824
3825 let (schema, _) =
3826 ToolGenerator::convert_parameter_schema(¶m_no_desc, ParameterIn::Query, &spec)
3827 .unwrap();
3828 let description = schema.get("description").unwrap().as_str().unwrap();
3829 assert_eq!(description, "limit parameter. Example: `100`");
3830 }
3831
3832 #[test]
3833 fn test_format_examples_for_description() {
3834 let examples = vec![json!("active")];
3836 let result = ToolGenerator::format_examples_for_description(&examples);
3837 assert_eq!(result, Some("Example: `\"active\"`".to_string()));
3838
3839 let examples = vec![json!(42)];
3841 let result = ToolGenerator::format_examples_for_description(&examples);
3842 assert_eq!(result, Some("Example: `42`".to_string()));
3843
3844 let examples = vec![json!(true)];
3846 let result = ToolGenerator::format_examples_for_description(&examples);
3847 assert_eq!(result, Some("Example: `true`".to_string()));
3848
3849 let examples = vec![json!("active"), json!("pending"), json!("completed")];
3851 let result = ToolGenerator::format_examples_for_description(&examples);
3852 assert_eq!(
3853 result,
3854 Some("Examples:\n- `\"active\"`\n- `\"pending\"`\n- `\"completed\"`".to_string())
3855 );
3856
3857 let examples = vec![json!(["a", "b", "c"])];
3859 let result = ToolGenerator::format_examples_for_description(&examples);
3860 assert_eq!(result, Some("Example: `[\"a\",\"b\",\"c\"]`".to_string()));
3861
3862 let examples = vec![json!({"key": "value"})];
3864 let result = ToolGenerator::format_examples_for_description(&examples);
3865 assert_eq!(result, Some("Example: `{\"key\":\"value\"}`".to_string()));
3866
3867 let examples = vec![];
3869 let result = ToolGenerator::format_examples_for_description(&examples);
3870 assert_eq!(result, None);
3871
3872 let examples = vec![json!(null)];
3874 let result = ToolGenerator::format_examples_for_description(&examples);
3875 assert_eq!(result, Some("Example: `null`".to_string()));
3876
3877 let examples = vec![json!("text"), json!(123), json!(true)];
3879 let result = ToolGenerator::format_examples_for_description(&examples);
3880 assert_eq!(
3881 result,
3882 Some("Examples:\n- `\"text\"`\n- `123`\n- `true`".to_string())
3883 );
3884
3885 let examples = vec![json!(["a", "b", "c", "d", "e", "f"])];
3887 let result = ToolGenerator::format_examples_for_description(&examples);
3888 assert_eq!(
3889 result,
3890 Some("Example: `[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\"]`".to_string())
3891 );
3892
3893 let examples = vec![json!([1, 2])];
3895 let result = ToolGenerator::format_examples_for_description(&examples);
3896 assert_eq!(result, Some("Example: `[1,2]`".to_string()));
3897
3898 let examples = vec![json!({"user": {"name": "John", "age": 30}})];
3900 let result = ToolGenerator::format_examples_for_description(&examples);
3901 assert_eq!(
3902 result,
3903 Some("Example: `{\"user\":{\"age\":30,\"name\":\"John\"}}`".to_string())
3904 );
3905
3906 let examples = vec![json!("a"), json!("b"), json!("c"), json!("d"), json!("e")];
3908 let result = ToolGenerator::format_examples_for_description(&examples);
3909 assert_eq!(
3910 result,
3911 Some("Examples:\n- `\"a\"`\n- `\"b\"`\n- `\"c\"`\n- `\"d\"`\n- `\"e\"`".to_string())
3912 );
3913
3914 let examples = vec![json!(3.5)];
3916 let result = ToolGenerator::format_examples_for_description(&examples);
3917 assert_eq!(result, Some("Example: `3.5`".to_string()));
3918
3919 let examples = vec![json!(-42)];
3921 let result = ToolGenerator::format_examples_for_description(&examples);
3922 assert_eq!(result, Some("Example: `-42`".to_string()));
3923
3924 let examples = vec![json!(false)];
3926 let result = ToolGenerator::format_examples_for_description(&examples);
3927 assert_eq!(result, Some("Example: `false`".to_string()));
3928
3929 let examples = vec![json!("hello \"world\"")];
3931 let result = ToolGenerator::format_examples_for_description(&examples);
3932 assert_eq!(result, Some(r#"Example: `"hello \"world\""`"#.to_string()));
3934
3935 let examples = vec![json!("")];
3937 let result = ToolGenerator::format_examples_for_description(&examples);
3938 assert_eq!(result, Some("Example: `\"\"`".to_string()));
3939
3940 let examples = vec![json!([])];
3942 let result = ToolGenerator::format_examples_for_description(&examples);
3943 assert_eq!(result, Some("Example: `[]`".to_string()));
3944
3945 let examples = vec![json!({})];
3947 let result = ToolGenerator::format_examples_for_description(&examples);
3948 assert_eq!(result, Some("Example: `{}`".to_string()));
3949 }
3950}