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 if let Some(schema_or_ref) = &media_type.schema {
332 let wrapped_schema = Self::wrap_output_schema(schema_or_ref, spec)?;
334 return Ok(Some(wrapped_schema));
335 }
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 if matches!(schema_type, SchemaTypeSet::Single(SchemaType::Object)) {
492 match &obj_schema.additional_properties {
494 None => {
495 schema_obj.insert("additionalProperties".to_string(), json!(true));
497 }
498 Some(Schema::Boolean(BooleanSchema(value))) => {
499 schema_obj.insert("additionalProperties".to_string(), json!(value));
501 }
502 Some(Schema::Object(schema_ref)) => {
503 let mut visited = HashSet::new();
505 let additional_props_schema = Self::convert_schema_to_json_schema(
506 &Schema::Object(schema_ref.clone()),
507 spec,
508 &mut visited,
509 )?;
510 schema_obj
511 .insert("additionalProperties".to_string(), additional_props_schema);
512 }
513 }
514 }
515 }
516
517 if let Some(schema_type) = &obj_schema.schema_type {
519 if matches!(schema_type, SchemaTypeSet::Single(SchemaType::Array)) {
520 if !obj_schema.prefix_items.is_empty() {
522 Self::convert_prefix_items_to_draft07(
524 &obj_schema.prefix_items,
525 &obj_schema.items,
526 &mut schema_obj,
527 spec,
528 )?;
529 } else if let Some(items_schema) = &obj_schema.items {
530 let items_json =
532 Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
533 schema_obj.insert("items".to_string(), items_json);
534 }
535
536 if let Some(min_items) = obj_schema.min_items {
538 schema_obj.insert("minItems".to_string(), json!(min_items));
539 }
540 if let Some(max_items) = obj_schema.max_items {
541 schema_obj.insert("maxItems".to_string(), json!(max_items));
542 }
543 } else if let Some(items_schema) = &obj_schema.items {
544 let items_json = Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
546 schema_obj.insert("items".to_string(), items_json);
547 }
548 }
549
550 if let Some(format) = &obj_schema.format {
552 schema_obj.insert("format".to_string(), json!(format));
553 }
554
555 if let Some(example) = &obj_schema.example {
556 schema_obj.insert("example".to_string(), example.clone());
557 }
558
559 if let Some(default) = &obj_schema.default {
560 schema_obj.insert("default".to_string(), default.clone());
561 }
562
563 if !obj_schema.enum_values.is_empty() {
564 schema_obj.insert("enum".to_string(), json!(&obj_schema.enum_values));
565 }
566
567 if let Some(min) = &obj_schema.minimum {
568 schema_obj.insert("minimum".to_string(), json!(min));
569 }
570
571 if let Some(max) = &obj_schema.maximum {
572 schema_obj.insert("maximum".to_string(), json!(max));
573 }
574
575 if let Some(min_length) = &obj_schema.min_length {
576 schema_obj.insert("minLength".to_string(), json!(min_length));
577 }
578
579 if let Some(max_length) = &obj_schema.max_length {
580 schema_obj.insert("maxLength".to_string(), json!(max_length));
581 }
582
583 if let Some(pattern) = &obj_schema.pattern {
584 schema_obj.insert("pattern".to_string(), json!(pattern));
585 }
586
587 Ok(Value::Object(schema_obj))
588 }
589
590 fn schema_type_to_string(schema_type: &SchemaType) -> String {
592 match schema_type {
593 SchemaType::Boolean => "boolean",
594 SchemaType::Integer => "integer",
595 SchemaType::Number => "number",
596 SchemaType::String => "string",
597 SchemaType::Array => "array",
598 SchemaType::Object => "object",
599 SchemaType::Null => "null",
600 }
601 .to_string()
602 }
603
604 fn resolve_reference(
614 ref_path: &str,
615 spec: &Spec,
616 visited: &mut HashSet<String>,
617 ) -> Result<ObjectSchema, OpenApiError> {
618 if visited.contains(ref_path) {
620 return Err(OpenApiError::ToolGeneration(format!(
621 "Circular reference detected: {ref_path}"
622 )));
623 }
624
625 visited.insert(ref_path.to_string());
627
628 if !ref_path.starts_with("#/components/schemas/") {
631 return Err(OpenApiError::ToolGeneration(format!(
632 "Unsupported reference format: {ref_path}. Only #/components/schemas/ references are supported"
633 )));
634 }
635
636 let schema_name = ref_path.strip_prefix("#/components/schemas/").unwrap();
637
638 let components = spec.components.as_ref().ok_or_else(|| {
640 OpenApiError::ToolGeneration(format!(
641 "Reference {ref_path} points to components, but spec has no components section"
642 ))
643 })?;
644
645 let schema_ref = components.schemas.get(schema_name).ok_or_else(|| {
646 OpenApiError::ToolGeneration(format!(
647 "Schema '{schema_name}' not found in components/schemas"
648 ))
649 })?;
650
651 let resolved_schema = match schema_ref {
653 ObjectOrReference::Object(obj_schema) => obj_schema.clone(),
654 ObjectOrReference::Ref {
655 ref_path: nested_ref,
656 } => {
657 Self::resolve_reference(nested_ref, spec, visited)?
659 }
660 };
661
662 visited.remove(ref_path);
664
665 Ok(resolved_schema)
666 }
667
668 fn generate_parameter_schema(
670 parameters: &[ObjectOrReference<Parameter>],
671 _method: &str,
672 request_body: &Option<ObjectOrReference<RequestBody>>,
673 spec: &Spec,
674 ) -> Result<Value, OpenApiError> {
675 let mut properties = serde_json::Map::new();
676 let mut required = Vec::new();
677
678 let mut path_params = Vec::new();
680 let mut query_params = Vec::new();
681 let mut header_params = Vec::new();
682 let mut cookie_params = Vec::new();
683
684 for param_ref in parameters {
685 let param = match param_ref {
686 ObjectOrReference::Object(param) => param,
687 ObjectOrReference::Ref { ref_path } => {
688 eprintln!("Warning: Parameter reference not resolved: {ref_path}");
692 continue;
693 }
694 };
695
696 match ¶m.location {
697 ParameterIn::Query => query_params.push(param),
698 ParameterIn::Header => header_params.push(param),
699 ParameterIn::Path => path_params.push(param),
700 ParameterIn::Cookie => cookie_params.push(param),
701 }
702 }
703
704 for param in path_params {
706 let (param_schema, mut annotations) =
707 Self::convert_parameter_schema(param, ParameterIn::Path, spec)?;
708
709 let sanitized_name = sanitize_property_name(¶m.name);
711 if sanitized_name != param.name {
712 annotations = annotations.with_original_name(param.name.clone());
713 }
714
715 let param_schema_with_annotations =
716 Self::apply_annotations_to_schema(param_schema, annotations);
717 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
718 required.push(sanitized_name);
719 }
720
721 for param in &query_params {
723 let (param_schema, mut annotations) =
724 Self::convert_parameter_schema(param, ParameterIn::Query, spec)?;
725
726 let sanitized_name = sanitize_property_name(¶m.name);
728 if sanitized_name != param.name {
729 annotations = annotations.with_original_name(param.name.clone());
730 }
731
732 let param_schema_with_annotations =
733 Self::apply_annotations_to_schema(param_schema, annotations);
734 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
735 if param.required.unwrap_or(false) {
736 required.push(sanitized_name);
737 }
738 }
739
740 for param in &header_params {
742 let (param_schema, mut annotations) =
743 Self::convert_parameter_schema(param, ParameterIn::Header, spec)?;
744
745 let prefixed_name = format!("header_{}", param.name);
747 let sanitized_name = sanitize_property_name(&prefixed_name);
748 if sanitized_name != prefixed_name {
749 annotations = annotations.with_original_name(param.name.clone());
750 }
751
752 let param_schema_with_annotations =
753 Self::apply_annotations_to_schema(param_schema, annotations);
754
755 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
756 if param.required.unwrap_or(false) {
757 required.push(sanitized_name);
758 }
759 }
760
761 for param in &cookie_params {
763 let (param_schema, mut annotations) =
764 Self::convert_parameter_schema(param, ParameterIn::Cookie, spec)?;
765
766 let prefixed_name = format!("cookie_{}", param.name);
768 let sanitized_name = sanitize_property_name(&prefixed_name);
769 if sanitized_name != prefixed_name {
770 annotations = annotations.with_original_name(param.name.clone());
771 }
772
773 let param_schema_with_annotations =
774 Self::apply_annotations_to_schema(param_schema, annotations);
775
776 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
777 if param.required.unwrap_or(false) {
778 required.push(sanitized_name);
779 }
780 }
781
782 if let Some(request_body) = request_body {
784 if let Some((body_schema, annotations, is_required)) =
785 Self::convert_request_body_to_json_schema(request_body, spec)?
786 {
787 let body_schema_with_annotations =
788 Self::apply_annotations_to_schema(body_schema, annotations);
789 properties.insert("request_body".to_string(), body_schema_with_annotations);
790 if is_required {
791 required.push("request_body".to_string());
792 }
793 }
794 }
795
796 if !query_params.is_empty() || !header_params.is_empty() || !cookie_params.is_empty() {
798 properties.insert(
800 "timeout_seconds".to_string(),
801 json!({
802 "type": "integer",
803 "description": "Request timeout in seconds",
804 "minimum": 1,
805 "maximum": 300,
806 "default": 30
807 }),
808 );
809 }
810
811 Ok(json!({
812 "type": "object",
813 "properties": properties,
814 "required": required,
815 "additionalProperties": false
816 }))
817 }
818
819 fn convert_parameter_schema(
821 param: &Parameter,
822 location: ParameterIn,
823 spec: &Spec,
824 ) -> Result<(Value, Annotations), OpenApiError> {
825 let base_schema = if let Some(schema_ref) = ¶m.schema {
827 match schema_ref {
828 ObjectOrReference::Object(obj_schema) => {
829 let mut visited = HashSet::new();
830 Self::convert_schema_to_json_schema(
831 &Schema::Object(Box::new(ObjectOrReference::Object(obj_schema.clone()))),
832 spec,
833 &mut visited,
834 )?
835 }
836 ObjectOrReference::Ref { ref_path } => {
837 let mut visited = HashSet::new();
839 match Self::resolve_reference(ref_path, spec, &mut visited) {
840 Ok(resolved_schema) => Self::convert_schema_to_json_schema(
841 &Schema::Object(Box::new(ObjectOrReference::Object(resolved_schema))),
842 spec,
843 &mut visited,
844 )?,
845 Err(_) => {
846 json!({"type": "string"})
848 }
849 }
850 }
851 }
852 } else {
853 json!({"type": "string"})
855 };
856
857 let mut result = match base_schema {
859 Value::Object(obj) => obj,
860 _ => {
861 return Err(OpenApiError::ToolGeneration(format!(
863 "Internal error: schema converter returned non-object for parameter '{}'",
864 param.name
865 )));
866 }
867 };
868
869 let mut collected_examples = Vec::new();
871
872 if let Some(example) = ¶m.example {
874 collected_examples.push(example.clone());
875 } else if !param.examples.is_empty() {
876 for example_ref in param.examples.values() {
878 match example_ref {
879 ObjectOrReference::Object(example_obj) => {
880 if let Some(value) = &example_obj.value {
881 collected_examples.push(value.clone());
882 }
883 }
884 ObjectOrReference::Ref { .. } => {
885 }
887 }
888 }
889 } else if let Some(Value::String(ex_str)) = result.get("example") {
890 collected_examples.push(json!(ex_str));
892 } else if let Some(ex) = result.get("example") {
893 collected_examples.push(ex.clone());
894 }
895
896 let base_description = param
898 .description
899 .as_ref()
900 .map(|d| d.to_string())
901 .or_else(|| {
902 result
903 .get("description")
904 .and_then(|d| d.as_str())
905 .map(|d| d.to_string())
906 })
907 .unwrap_or_else(|| format!("{} parameter", param.name));
908
909 let description_with_examples = if let Some(examples_str) =
910 Self::format_examples_for_description(&collected_examples)
911 {
912 format!("{base_description}. {examples_str}")
913 } else {
914 base_description
915 };
916
917 result.insert("description".to_string(), json!(description_with_examples));
918
919 if let Some(example) = ¶m.example {
924 result.insert("example".to_string(), example.clone());
925 } else if !param.examples.is_empty() {
926 let mut examples_array = Vec::new();
929 for (example_name, example_ref) in ¶m.examples {
930 match example_ref {
931 ObjectOrReference::Object(example_obj) => {
932 if let Some(value) = &example_obj.value {
933 examples_array.push(json!({
934 "name": example_name,
935 "value": value
936 }));
937 }
938 }
939 ObjectOrReference::Ref { .. } => {
940 }
943 }
944 }
945
946 if !examples_array.is_empty() {
947 if let Some(first_example) = examples_array.first() {
949 if let Some(value) = first_example.get("value") {
950 result.insert("example".to_string(), value.clone());
951 }
952 }
953 result.insert("x-examples".to_string(), json!(examples_array));
955 }
956 }
957
958 let mut annotations = Annotations::new()
960 .with_location(Location::Parameter(location))
961 .with_required(param.required.unwrap_or(false));
962
963 if let Some(explode) = param.explode {
965 annotations = annotations.with_explode(explode);
966 } else {
967 let default_explode = match ¶m.style {
971 Some(ParameterStyle::Form) | None => true, _ => false,
973 };
974 annotations = annotations.with_explode(default_explode);
975 }
976
977 Ok((Value::Object(result), annotations))
978 }
979
980 fn apply_annotations_to_schema(schema: Value, annotations: Annotations) -> Value {
982 match schema {
983 Value::Object(mut obj) => {
984 if let Ok(Value::Object(ann_map)) = serde_json::to_value(&annotations) {
986 for (key, value) in ann_map {
987 obj.insert(key, value);
988 }
989 }
990 Value::Object(obj)
991 }
992 _ => schema,
993 }
994 }
995
996 fn format_examples_for_description(examples: &[Value]) -> Option<String> {
998 if examples.is_empty() {
999 return None;
1000 }
1001
1002 if examples.len() == 1 {
1003 let example_str =
1004 serde_json::to_string(&examples[0]).unwrap_or_else(|_| "null".to_string());
1005 Some(format!("Example: `{example_str}`"))
1006 } else {
1007 let mut result = String::from("Examples:\n");
1008 for ex in examples {
1009 let json_str = serde_json::to_string(ex).unwrap_or_else(|_| "null".to_string());
1010 result.push_str(&format!("- `{json_str}`\n"));
1011 }
1012 result.pop();
1014 Some(result)
1015 }
1016 }
1017
1018 fn convert_prefix_items_to_draft07(
1029 prefix_items: &[ObjectOrReference<ObjectSchema>],
1030 items: &Option<Box<Schema>>,
1031 result: &mut serde_json::Map<String, Value>,
1032 spec: &Spec,
1033 ) -> Result<(), OpenApiError> {
1034 let prefix_count = prefix_items.len();
1035
1036 let mut item_types = Vec::new();
1038 for prefix_item in prefix_items {
1039 match prefix_item {
1040 ObjectOrReference::Object(obj_schema) => {
1041 if let Some(schema_type) = &obj_schema.schema_type {
1042 match schema_type {
1043 SchemaTypeSet::Single(SchemaType::String) => item_types.push("string"),
1044 SchemaTypeSet::Single(SchemaType::Integer) => {
1045 item_types.push("integer")
1046 }
1047 SchemaTypeSet::Single(SchemaType::Number) => item_types.push("number"),
1048 SchemaTypeSet::Single(SchemaType::Boolean) => {
1049 item_types.push("boolean")
1050 }
1051 SchemaTypeSet::Single(SchemaType::Array) => item_types.push("array"),
1052 SchemaTypeSet::Single(SchemaType::Object) => item_types.push("object"),
1053 _ => item_types.push("string"), }
1055 } else {
1056 item_types.push("string"); }
1058 }
1059 ObjectOrReference::Ref { ref_path } => {
1060 let mut visited = HashSet::new();
1062 match Self::resolve_reference(ref_path, spec, &mut visited) {
1063 Ok(resolved_schema) => {
1064 if let Some(schema_type_set) = &resolved_schema.schema_type {
1066 match schema_type_set {
1067 SchemaTypeSet::Single(SchemaType::String) => {
1068 item_types.push("string")
1069 }
1070 SchemaTypeSet::Single(SchemaType::Integer) => {
1071 item_types.push("integer")
1072 }
1073 SchemaTypeSet::Single(SchemaType::Number) => {
1074 item_types.push("number")
1075 }
1076 SchemaTypeSet::Single(SchemaType::Boolean) => {
1077 item_types.push("boolean")
1078 }
1079 SchemaTypeSet::Single(SchemaType::Array) => {
1080 item_types.push("array")
1081 }
1082 SchemaTypeSet::Single(SchemaType::Object) => {
1083 item_types.push("object")
1084 }
1085 _ => item_types.push("string"), }
1087 } else {
1088 item_types.push("string"); }
1090 }
1091 Err(_) => {
1092 item_types.push("string");
1094 }
1095 }
1096 }
1097 }
1098 }
1099
1100 let items_is_false =
1102 matches!(items.as_ref().map(|i| i.as_ref()), Some(Schema::Boolean(b)) if !b.0);
1103
1104 if items_is_false {
1105 result.insert("minItems".to_string(), json!(prefix_count));
1107 result.insert("maxItems".to_string(), json!(prefix_count));
1108 }
1109
1110 let unique_types: std::collections::BTreeSet<_> = item_types.into_iter().collect();
1112
1113 if unique_types.len() == 1 {
1114 let item_type = unique_types.into_iter().next().unwrap();
1116 result.insert("items".to_string(), json!({"type": item_type}));
1117 } else if unique_types.len() > 1 {
1118 let one_of: Vec<Value> = unique_types
1120 .into_iter()
1121 .map(|t| json!({"type": t}))
1122 .collect();
1123 result.insert("items".to_string(), json!({"oneOf": one_of}));
1124 }
1125
1126 Ok(())
1127 }
1128
1129 fn convert_request_body_to_json_schema(
1141 request_body_ref: &ObjectOrReference<RequestBody>,
1142 spec: &Spec,
1143 ) -> Result<Option<(Value, Annotations, bool)>, OpenApiError> {
1144 match request_body_ref {
1145 ObjectOrReference::Object(request_body) => {
1146 let schema_info = request_body
1149 .content
1150 .get(mime::APPLICATION_JSON.as_ref())
1151 .or_else(|| request_body.content.get("application/json"))
1152 .or_else(|| {
1153 request_body.content.values().next()
1155 });
1156
1157 if let Some(media_type) = schema_info {
1158 if let Some(schema_ref) = &media_type.schema {
1159 let schema = Schema::Object(Box::new(schema_ref.clone()));
1161
1162 let mut visited = HashSet::new();
1164 let converted_schema =
1165 Self::convert_schema_to_json_schema(&schema, spec, &mut visited)?;
1166
1167 let mut schema_obj = match converted_schema {
1169 Value::Object(obj) => obj,
1170 _ => {
1171 let mut obj = serde_json::Map::new();
1173 obj.insert("type".to_string(), json!("object"));
1174 obj.insert("additionalProperties".to_string(), json!(true));
1175 obj
1176 }
1177 };
1178
1179 let description = request_body
1181 .description
1182 .clone()
1183 .unwrap_or_else(|| "Request body data".to_string());
1184 schema_obj.insert("description".to_string(), json!(description));
1185
1186 let annotations = Annotations::new()
1188 .with_location(Location::Body)
1189 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
1190
1191 let required = request_body.required.unwrap_or(false);
1192 Ok(Some((Value::Object(schema_obj), annotations, required)))
1193 } else {
1194 Ok(None)
1195 }
1196 } else {
1197 Ok(None)
1198 }
1199 }
1200 ObjectOrReference::Ref { .. } => {
1201 let mut result = serde_json::Map::new();
1203 result.insert("type".to_string(), json!("object"));
1204 result.insert("additionalProperties".to_string(), json!(true));
1205 result.insert("description".to_string(), json!("Request body data"));
1206
1207 let annotations = Annotations::new()
1209 .with_location(Location::Body)
1210 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
1211
1212 Ok(Some((Value::Object(result), annotations, false)))
1213 }
1214 }
1215 }
1216
1217 pub fn extract_parameters(
1223 tool_metadata: &ToolMetadata,
1224 arguments: &Value,
1225 ) -> Result<ExtractedParameters, ToolCallValidationError> {
1226 let args = arguments.as_object().ok_or_else(|| {
1227 ToolCallValidationError::RequestConstructionError {
1228 reason: "Arguments must be an object".to_string(),
1229 }
1230 })?;
1231
1232 let mut path_params = HashMap::new();
1233 let mut query_params = HashMap::new();
1234 let mut header_params = HashMap::new();
1235 let mut cookie_params = HashMap::new();
1236 let mut body_params = HashMap::new();
1237 let mut config = RequestConfig::default();
1238
1239 if let Some(timeout) = args.get("timeout_seconds").and_then(Value::as_u64) {
1241 config.timeout_seconds = u32::try_from(timeout).unwrap_or(u32::MAX);
1242 }
1243
1244 for (key, value) in args {
1246 if key == "timeout_seconds" {
1247 continue; }
1249
1250 if key == "request_body" {
1252 body_params.insert("request_body".to_string(), value.clone());
1253 continue;
1254 }
1255
1256 let location = Self::get_parameter_location(tool_metadata, key).map_err(|e| {
1258 ToolCallValidationError::RequestConstructionError {
1259 reason: e.to_string(),
1260 }
1261 })?;
1262
1263 let original_name = Self::get_original_parameter_name(tool_metadata, key);
1265
1266 match location.as_str() {
1267 "path" => {
1268 path_params.insert(original_name.unwrap_or_else(|| key.clone()), value.clone());
1269 }
1270 "query" => {
1271 let param_name = original_name.unwrap_or_else(|| key.clone());
1272 let explode = Self::get_parameter_explode(tool_metadata, key);
1273 query_params.insert(param_name, QueryParameter::new(value.clone(), explode));
1274 }
1275 "header" => {
1276 let header_name = if let Some(orig) = original_name {
1278 orig
1279 } else if key.starts_with("header_") {
1280 key.strip_prefix("header_").unwrap_or(key).to_string()
1281 } else {
1282 key.clone()
1283 };
1284 header_params.insert(header_name, value.clone());
1285 }
1286 "cookie" => {
1287 let cookie_name = if let Some(orig) = original_name {
1289 orig
1290 } else if key.starts_with("cookie_") {
1291 key.strip_prefix("cookie_").unwrap_or(key).to_string()
1292 } else {
1293 key.clone()
1294 };
1295 cookie_params.insert(cookie_name, value.clone());
1296 }
1297 "body" => {
1298 let body_name = if key.starts_with("body_") {
1300 key.strip_prefix("body_").unwrap_or(key).to_string()
1301 } else {
1302 key.clone()
1303 };
1304 body_params.insert(body_name, value.clone());
1305 }
1306 _ => {
1307 return Err(ToolCallValidationError::RequestConstructionError {
1308 reason: format!("Unknown parameter location for parameter: {key}"),
1309 });
1310 }
1311 }
1312 }
1313
1314 let extracted = ExtractedParameters {
1315 path: path_params,
1316 query: query_params,
1317 headers: header_params,
1318 cookies: cookie_params,
1319 body: body_params,
1320 config,
1321 };
1322
1323 Self::validate_parameters(tool_metadata, arguments)?;
1325
1326 Ok(extracted)
1327 }
1328
1329 fn get_original_parameter_name(
1331 tool_metadata: &ToolMetadata,
1332 param_name: &str,
1333 ) -> Option<String> {
1334 tool_metadata
1335 .parameters
1336 .get("properties")
1337 .and_then(|p| p.as_object())
1338 .and_then(|props| props.get(param_name))
1339 .and_then(|schema| schema.get(X_ORIGINAL_NAME))
1340 .and_then(|v| v.as_str())
1341 .map(|s| s.to_string())
1342 }
1343
1344 fn get_parameter_explode(tool_metadata: &ToolMetadata, param_name: &str) -> bool {
1346 tool_metadata
1347 .parameters
1348 .get("properties")
1349 .and_then(|p| p.as_object())
1350 .and_then(|props| props.get(param_name))
1351 .and_then(|schema| schema.get(X_PARAMETER_EXPLODE))
1352 .and_then(|v| v.as_bool())
1353 .unwrap_or(true) }
1355
1356 fn get_parameter_location(
1358 tool_metadata: &ToolMetadata,
1359 param_name: &str,
1360 ) -> Result<String, OpenApiError> {
1361 let properties = tool_metadata
1362 .parameters
1363 .get("properties")
1364 .and_then(|p| p.as_object())
1365 .ok_or_else(|| {
1366 OpenApiError::ToolGeneration("Invalid tool parameters schema".to_string())
1367 })?;
1368
1369 if let Some(param_schema) = properties.get(param_name) {
1370 if let Some(location) = param_schema
1371 .get(X_PARAMETER_LOCATION)
1372 .and_then(|v| v.as_str())
1373 {
1374 return Ok(location.to_string());
1375 }
1376 }
1377
1378 if param_name.starts_with("header_") {
1380 Ok("header".to_string())
1381 } else if param_name.starts_with("cookie_") {
1382 Ok("cookie".to_string())
1383 } else if param_name.starts_with("body_") {
1384 Ok("body".to_string())
1385 } else {
1386 Ok("query".to_string())
1388 }
1389 }
1390
1391 fn validate_parameters(
1393 tool_metadata: &ToolMetadata,
1394 arguments: &Value,
1395 ) -> Result<(), ToolCallValidationError> {
1396 let schema = &tool_metadata.parameters;
1397
1398 let required_params = schema
1400 .get("required")
1401 .and_then(|r| r.as_array())
1402 .map(|arr| {
1403 arr.iter()
1404 .filter_map(|v| v.as_str())
1405 .collect::<std::collections::HashSet<_>>()
1406 })
1407 .unwrap_or_default();
1408
1409 let properties = schema
1410 .get("properties")
1411 .and_then(|p| p.as_object())
1412 .ok_or_else(|| ToolCallValidationError::RequestConstructionError {
1413 reason: "Tool schema missing properties".to_string(),
1414 })?;
1415
1416 let args = arguments.as_object().ok_or_else(|| {
1417 ToolCallValidationError::RequestConstructionError {
1418 reason: "Arguments must be an object".to_string(),
1419 }
1420 })?;
1421
1422 let mut all_errors = Vec::new();
1424
1425 all_errors.extend(Self::check_unknown_parameters(args, properties));
1427
1428 all_errors.extend(Self::check_missing_required(
1430 args,
1431 properties,
1432 &required_params,
1433 ));
1434
1435 all_errors.extend(Self::validate_parameter_values(args, properties));
1437
1438 if !all_errors.is_empty() {
1440 return Err(ToolCallValidationError::InvalidParameters {
1441 violations: all_errors,
1442 });
1443 }
1444
1445 Ok(())
1446 }
1447
1448 fn check_unknown_parameters(
1450 args: &serde_json::Map<String, Value>,
1451 properties: &serde_json::Map<String, Value>,
1452 ) -> Vec<ValidationError> {
1453 let mut errors = Vec::new();
1454
1455 let valid_params: Vec<String> = properties.keys().map(|s| s.to_string()).collect();
1457
1458 for (arg_name, _) in args.iter() {
1460 if !properties.contains_key(arg_name) {
1461 let valid_params_refs: Vec<&str> =
1463 valid_params.iter().map(|s| s.as_str()).collect();
1464 let suggestions = crate::find_similar_strings(arg_name, &valid_params_refs);
1465
1466 errors.push(ValidationError::InvalidParameter {
1467 parameter: arg_name.clone(),
1468 suggestions,
1469 valid_parameters: valid_params.clone(),
1470 });
1471 }
1472 }
1473
1474 errors
1475 }
1476
1477 fn check_missing_required(
1479 args: &serde_json::Map<String, Value>,
1480 properties: &serde_json::Map<String, Value>,
1481 required_params: &HashSet<&str>,
1482 ) -> Vec<ValidationError> {
1483 let mut errors = Vec::new();
1484
1485 for required_param in required_params {
1486 if !args.contains_key(*required_param) {
1487 let param_schema = properties.get(*required_param);
1489
1490 let description = param_schema
1491 .and_then(|schema| schema.get("description"))
1492 .and_then(|d| d.as_str())
1493 .map(|s| s.to_string());
1494
1495 let expected_type = param_schema
1496 .and_then(Self::get_expected_type)
1497 .unwrap_or_else(|| "unknown".to_string());
1498
1499 errors.push(ValidationError::MissingRequiredParameter {
1500 parameter: (*required_param).to_string(),
1501 description,
1502 expected_type,
1503 });
1504 }
1505 }
1506
1507 errors
1508 }
1509
1510 fn validate_parameter_values(
1512 args: &serde_json::Map<String, Value>,
1513 properties: &serde_json::Map<String, Value>,
1514 ) -> Vec<ValidationError> {
1515 let mut errors = Vec::new();
1516
1517 for (param_name, param_value) in args {
1518 if let Some(param_schema) = properties.get(param_name) {
1519 let schema = json!({
1521 "type": "object",
1522 "properties": {
1523 param_name: param_schema
1524 }
1525 });
1526
1527 let compiled = match jsonschema::validator_for(&schema) {
1529 Ok(compiled) => compiled,
1530 Err(e) => {
1531 errors.push(ValidationError::ConstraintViolation {
1532 parameter: param_name.clone(),
1533 message: format!(
1534 "Failed to compile schema for parameter '{param_name}': {e}"
1535 ),
1536 field_path: None,
1537 actual_value: None,
1538 expected_type: None,
1539 constraints: vec![],
1540 });
1541 continue;
1542 }
1543 };
1544
1545 let instance = json!({ param_name: param_value });
1547
1548 let validation_errors: Vec<_> =
1550 compiled.validate(&instance).err().into_iter().collect();
1551
1552 for validation_error in validation_errors {
1553 let error_message = validation_error.to_string();
1555 let instance_path_str = validation_error.instance_path.to_string();
1556 let field_path = if instance_path_str.is_empty() || instance_path_str == "/" {
1557 Some(param_name.clone())
1558 } else {
1559 Some(instance_path_str.trim_start_matches('/').to_string())
1560 };
1561
1562 let constraints = Self::extract_constraints_from_schema(param_schema);
1564
1565 let expected_type = Self::get_expected_type(param_schema);
1567
1568 errors.push(ValidationError::ConstraintViolation {
1569 parameter: param_name.clone(),
1570 message: error_message,
1571 field_path,
1572 actual_value: Some(Box::new(param_value.clone())),
1573 expected_type,
1574 constraints,
1575 });
1576 }
1577 }
1578 }
1579
1580 errors
1581 }
1582
1583 fn extract_constraints_from_schema(schema: &Value) -> Vec<ValidationConstraint> {
1585 let mut constraints = Vec::new();
1586
1587 if let Some(min_value) = schema.get("minimum").and_then(|v| v.as_f64()) {
1589 let exclusive = schema
1590 .get("exclusiveMinimum")
1591 .and_then(|v| v.as_bool())
1592 .unwrap_or(false);
1593 constraints.push(ValidationConstraint::Minimum {
1594 value: min_value,
1595 exclusive,
1596 });
1597 }
1598
1599 if let Some(max_value) = schema.get("maximum").and_then(|v| v.as_f64()) {
1601 let exclusive = schema
1602 .get("exclusiveMaximum")
1603 .and_then(|v| v.as_bool())
1604 .unwrap_or(false);
1605 constraints.push(ValidationConstraint::Maximum {
1606 value: max_value,
1607 exclusive,
1608 });
1609 }
1610
1611 if let Some(min_len) = schema
1613 .get("minLength")
1614 .and_then(|v| v.as_u64())
1615 .map(|v| v as usize)
1616 {
1617 constraints.push(ValidationConstraint::MinLength { value: min_len });
1618 }
1619
1620 if let Some(max_len) = schema
1622 .get("maxLength")
1623 .and_then(|v| v.as_u64())
1624 .map(|v| v as usize)
1625 {
1626 constraints.push(ValidationConstraint::MaxLength { value: max_len });
1627 }
1628
1629 if let Some(pattern) = schema
1631 .get("pattern")
1632 .and_then(|v| v.as_str())
1633 .map(|s| s.to_string())
1634 {
1635 constraints.push(ValidationConstraint::Pattern { pattern });
1636 }
1637
1638 if let Some(enum_values) = schema.get("enum").and_then(|v| v.as_array()).cloned() {
1640 constraints.push(ValidationConstraint::EnumValues {
1641 values: enum_values,
1642 });
1643 }
1644
1645 if let Some(format) = schema
1647 .get("format")
1648 .and_then(|v| v.as_str())
1649 .map(|s| s.to_string())
1650 {
1651 constraints.push(ValidationConstraint::Format { format });
1652 }
1653
1654 if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
1656 constraints.push(ValidationConstraint::MultipleOf { value: multiple_of });
1657 }
1658
1659 if let Some(min_items) = schema
1661 .get("minItems")
1662 .and_then(|v| v.as_u64())
1663 .map(|v| v as usize)
1664 {
1665 constraints.push(ValidationConstraint::MinItems { value: min_items });
1666 }
1667
1668 if let Some(max_items) = schema
1670 .get("maxItems")
1671 .and_then(|v| v.as_u64())
1672 .map(|v| v as usize)
1673 {
1674 constraints.push(ValidationConstraint::MaxItems { value: max_items });
1675 }
1676
1677 if let Some(true) = schema.get("uniqueItems").and_then(|v| v.as_bool()) {
1679 constraints.push(ValidationConstraint::UniqueItems);
1680 }
1681
1682 if let Some(min_props) = schema
1684 .get("minProperties")
1685 .and_then(|v| v.as_u64())
1686 .map(|v| v as usize)
1687 {
1688 constraints.push(ValidationConstraint::MinProperties { value: min_props });
1689 }
1690
1691 if let Some(max_props) = schema
1693 .get("maxProperties")
1694 .and_then(|v| v.as_u64())
1695 .map(|v| v as usize)
1696 {
1697 constraints.push(ValidationConstraint::MaxProperties { value: max_props });
1698 }
1699
1700 if let Some(const_value) = schema.get("const").cloned() {
1702 constraints.push(ValidationConstraint::ConstValue { value: const_value });
1703 }
1704
1705 if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
1707 let properties: Vec<String> = required
1708 .iter()
1709 .filter_map(|v| v.as_str().map(|s| s.to_string()))
1710 .collect();
1711 if !properties.is_empty() {
1712 constraints.push(ValidationConstraint::Required { properties });
1713 }
1714 }
1715
1716 constraints
1717 }
1718
1719 fn get_expected_type(schema: &Value) -> Option<String> {
1721 if let Some(type_value) = schema.get("type") {
1722 if let Some(type_str) = type_value.as_str() {
1723 return Some(type_str.to_string());
1724 } else if let Some(type_array) = type_value.as_array() {
1725 let types: Vec<String> = type_array
1727 .iter()
1728 .filter_map(|v| v.as_str())
1729 .map(|s| s.to_string())
1730 .collect();
1731 if !types.is_empty() {
1732 return Some(types.join(" | "));
1733 }
1734 }
1735 }
1736 None
1737 }
1738
1739 fn wrap_output_schema(
1763 body_schema: &ObjectOrReference<ObjectSchema>,
1764 spec: &Spec,
1765 ) -> Result<Value, OpenApiError> {
1766 let mut visited = HashSet::new();
1768 let body_schema_json = match body_schema {
1769 ObjectOrReference::Object(obj_schema) => {
1770 Self::convert_object_schema_to_json_schema(obj_schema, spec, &mut visited)?
1771 }
1772 ObjectOrReference::Ref { ref_path } => {
1773 let resolved = Self::resolve_reference(ref_path, spec, &mut visited)?;
1774 Self::convert_object_schema_to_json_schema(&resolved, spec, &mut visited)?
1775 }
1776 };
1777
1778 let error_schema = create_error_response_schema();
1779
1780 Ok(json!({
1781 "type": "object",
1782 "description": "Unified response structure with success and error variants",
1783 "required": ["status", "body"],
1784 "additionalProperties": false,
1785 "properties": {
1786 "status": {
1787 "type": "integer",
1788 "description": "HTTP status code",
1789 "minimum": 100,
1790 "maximum": 599
1791 },
1792 "body": {
1793 "description": "Response body - either success data or error information",
1794 "oneOf": [
1795 body_schema_json,
1796 error_schema
1797 ]
1798 }
1799 }
1800 }))
1801 }
1802}
1803
1804fn create_error_response_schema() -> Value {
1806 let root_schema = schema_for!(ErrorResponse);
1807 let schema_json = serde_json::to_value(root_schema).expect("Valid error schema");
1808
1809 let definitions = schema_json
1811 .get("$defs")
1812 .or_else(|| schema_json.get("definitions"))
1813 .cloned()
1814 .unwrap_or_else(|| json!({}));
1815
1816 let mut result = schema_json.clone();
1818 if let Some(obj) = result.as_object_mut() {
1819 obj.remove("$schema");
1820 obj.remove("$defs");
1821 obj.remove("definitions");
1822 obj.remove("title");
1823 }
1824
1825 inline_refs(&mut result, &definitions);
1827
1828 result
1829}
1830
1831fn inline_refs(schema: &mut Value, definitions: &Value) {
1833 match schema {
1834 Value::Object(obj) => {
1835 if let Some(ref_value) = obj.get("$ref").cloned() {
1837 if let Some(ref_str) = ref_value.as_str() {
1838 let def_name = ref_str
1840 .strip_prefix("#/$defs/")
1841 .or_else(|| ref_str.strip_prefix("#/definitions/"));
1842
1843 if let Some(name) = def_name {
1844 if let Some(definition) = definitions.get(name) {
1845 *schema = definition.clone();
1847 inline_refs(schema, definitions);
1849 return;
1850 }
1851 }
1852 }
1853 }
1854
1855 for (_, value) in obj.iter_mut() {
1857 inline_refs(value, definitions);
1858 }
1859 }
1860 Value::Array(arr) => {
1861 for item in arr.iter_mut() {
1863 inline_refs(item, definitions);
1864 }
1865 }
1866 _ => {} }
1868}
1869
1870#[derive(Debug, Clone)]
1872pub struct QueryParameter {
1873 pub value: Value,
1874 pub explode: bool,
1875}
1876
1877impl QueryParameter {
1878 pub fn new(value: Value, explode: bool) -> Self {
1879 Self { value, explode }
1880 }
1881}
1882
1883#[derive(Debug, Clone)]
1885pub struct ExtractedParameters {
1886 pub path: HashMap<String, Value>,
1887 pub query: HashMap<String, QueryParameter>,
1888 pub headers: HashMap<String, Value>,
1889 pub cookies: HashMap<String, Value>,
1890 pub body: HashMap<String, Value>,
1891 pub config: RequestConfig,
1892}
1893
1894#[derive(Debug, Clone)]
1896pub struct RequestConfig {
1897 pub timeout_seconds: u32,
1898 pub content_type: String,
1899}
1900
1901impl Default for RequestConfig {
1902 fn default() -> Self {
1903 Self {
1904 timeout_seconds: 30,
1905 content_type: mime::APPLICATION_JSON.to_string(),
1906 }
1907 }
1908}
1909
1910#[cfg(test)]
1911mod tests {
1912 use super::*;
1913
1914 use insta::assert_json_snapshot;
1915 use oas3::spec::{
1916 BooleanSchema, Components, MediaType, ObjectOrReference, ObjectSchema, Operation,
1917 Parameter, ParameterIn, RequestBody, Schema, SchemaType, SchemaTypeSet, Spec,
1918 };
1919 use rmcp::model::Tool;
1920 use serde_json::{Value, json};
1921 use std::collections::BTreeMap;
1922
1923 fn create_test_spec() -> Spec {
1925 Spec {
1926 openapi: "3.0.0".to_string(),
1927 info: oas3::spec::Info {
1928 title: "Test API".to_string(),
1929 version: "1.0.0".to_string(),
1930 summary: None,
1931 description: Some("Test API for unit tests".to_string()),
1932 terms_of_service: None,
1933 contact: None,
1934 license: None,
1935 extensions: Default::default(),
1936 },
1937 components: Some(Components {
1938 schemas: BTreeMap::new(),
1939 responses: BTreeMap::new(),
1940 parameters: BTreeMap::new(),
1941 examples: BTreeMap::new(),
1942 request_bodies: BTreeMap::new(),
1943 headers: BTreeMap::new(),
1944 security_schemes: BTreeMap::new(),
1945 links: BTreeMap::new(),
1946 callbacks: BTreeMap::new(),
1947 path_items: BTreeMap::new(),
1948 extensions: Default::default(),
1949 }),
1950 servers: vec![],
1951 paths: None,
1952 external_docs: None,
1953 tags: vec![],
1954 security: vec![],
1955 webhooks: BTreeMap::new(),
1956 extensions: Default::default(),
1957 }
1958 }
1959
1960 fn validate_tool_against_mcp_schema(metadata: &ToolMetadata) {
1961 let schema_content = std::fs::read_to_string("schema/2025-06-18/schema.json")
1962 .expect("Failed to read MCP schema file");
1963 let full_schema: Value =
1964 serde_json::from_str(&schema_content).expect("Failed to parse MCP schema JSON");
1965
1966 let tool_schema = json!({
1968 "$schema": "http://json-schema.org/draft-07/schema#",
1969 "definitions": full_schema.get("definitions"),
1970 "$ref": "#/definitions/Tool"
1971 });
1972
1973 let validator =
1974 jsonschema::validator_for(&tool_schema).expect("Failed to compile MCP Tool schema");
1975
1976 let tool = Tool::from(metadata);
1978
1979 let mcp_tool_json = serde_json::to_value(&tool).expect("Failed to serialize Tool to JSON");
1981
1982 let errors: Vec<String> = validator
1984 .iter_errors(&mcp_tool_json)
1985 .map(|e| e.to_string())
1986 .collect();
1987
1988 if !errors.is_empty() {
1989 panic!("Generated tool failed MCP schema validation: {errors:?}");
1990 }
1991 }
1992
1993 #[test]
1994 fn test_error_schema_structure() {
1995 let error_schema = create_error_response_schema();
1996
1997 assert!(error_schema.get("$schema").is_none());
1999 assert!(error_schema.get("definitions").is_none());
2000
2001 assert_json_snapshot!(error_schema);
2003 }
2004
2005 #[test]
2006 fn test_petstore_get_pet_by_id() {
2007 use oas3::spec::Response;
2008
2009 let mut operation = Operation {
2010 operation_id: Some("getPetById".to_string()),
2011 summary: Some("Find pet by ID".to_string()),
2012 description: Some("Returns a single pet".to_string()),
2013 tags: vec![],
2014 external_docs: None,
2015 parameters: vec![],
2016 request_body: None,
2017 responses: Default::default(),
2018 callbacks: Default::default(),
2019 deprecated: Some(false),
2020 security: vec![],
2021 servers: vec![],
2022 extensions: Default::default(),
2023 };
2024
2025 let param = Parameter {
2027 name: "petId".to_string(),
2028 location: ParameterIn::Path,
2029 description: Some("ID of pet to return".to_string()),
2030 required: Some(true),
2031 deprecated: Some(false),
2032 allow_empty_value: Some(false),
2033 style: None,
2034 explode: None,
2035 allow_reserved: Some(false),
2036 schema: Some(ObjectOrReference::Object(ObjectSchema {
2037 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2038 minimum: Some(serde_json::Number::from(1_i64)),
2039 format: Some("int64".to_string()),
2040 ..Default::default()
2041 })),
2042 example: None,
2043 examples: Default::default(),
2044 content: None,
2045 extensions: Default::default(),
2046 };
2047
2048 operation.parameters.push(ObjectOrReference::Object(param));
2049
2050 let mut responses = BTreeMap::new();
2052 let mut content = BTreeMap::new();
2053 content.insert(
2054 "application/json".to_string(),
2055 MediaType {
2056 schema: Some(ObjectOrReference::Object(ObjectSchema {
2057 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2058 properties: {
2059 let mut props = BTreeMap::new();
2060 props.insert(
2061 "id".to_string(),
2062 ObjectOrReference::Object(ObjectSchema {
2063 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2064 format: Some("int64".to_string()),
2065 ..Default::default()
2066 }),
2067 );
2068 props.insert(
2069 "name".to_string(),
2070 ObjectOrReference::Object(ObjectSchema {
2071 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2072 ..Default::default()
2073 }),
2074 );
2075 props.insert(
2076 "status".to_string(),
2077 ObjectOrReference::Object(ObjectSchema {
2078 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2079 ..Default::default()
2080 }),
2081 );
2082 props
2083 },
2084 required: vec!["id".to_string(), "name".to_string()],
2085 ..Default::default()
2086 })),
2087 examples: None,
2088 encoding: Default::default(),
2089 },
2090 );
2091
2092 responses.insert(
2093 "200".to_string(),
2094 ObjectOrReference::Object(Response {
2095 description: Some("successful operation".to_string()),
2096 headers: Default::default(),
2097 content,
2098 links: Default::default(),
2099 extensions: Default::default(),
2100 }),
2101 );
2102 operation.responses = Some(responses);
2103
2104 let spec = create_test_spec();
2105 let metadata = ToolGenerator::generate_tool_metadata(
2106 &operation,
2107 "get".to_string(),
2108 "/pet/{petId}".to_string(),
2109 &spec,
2110 )
2111 .unwrap();
2112
2113 assert_eq!(metadata.name, "getPetById");
2114 assert_eq!(metadata.method, "get");
2115 assert_eq!(metadata.path, "/pet/{petId}");
2116 assert!(metadata.description.contains("Find pet by ID"));
2117
2118 assert!(metadata.output_schema.is_some());
2120 let output_schema = metadata.output_schema.as_ref().unwrap();
2121
2122 insta::assert_json_snapshot!("test_petstore_get_pet_by_id_output_schema", output_schema);
2124
2125 validate_tool_against_mcp_schema(&metadata);
2127 }
2128
2129 #[test]
2130 fn test_convert_prefix_items_to_draft07_mixed_types() {
2131 let prefix_items = vec![
2134 ObjectOrReference::Object(ObjectSchema {
2135 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2136 format: Some("int32".to_string()),
2137 ..Default::default()
2138 }),
2139 ObjectOrReference::Object(ObjectSchema {
2140 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2141 ..Default::default()
2142 }),
2143 ];
2144
2145 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
2147
2148 let mut result = serde_json::Map::new();
2149 let spec = create_test_spec();
2150 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
2151 .unwrap();
2152
2153 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_mixed_types", result);
2155 }
2156
2157 #[test]
2158 fn test_convert_prefix_items_to_draft07_uniform_types() {
2159 let prefix_items = vec![
2161 ObjectOrReference::Object(ObjectSchema {
2162 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2163 ..Default::default()
2164 }),
2165 ObjectOrReference::Object(ObjectSchema {
2166 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2167 ..Default::default()
2168 }),
2169 ];
2170
2171 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
2173
2174 let mut result = serde_json::Map::new();
2175 let spec = create_test_spec();
2176 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
2177 .unwrap();
2178
2179 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_uniform_types", result);
2181 }
2182
2183 #[test]
2184 fn test_array_with_prefix_items_integration() {
2185 let param = Parameter {
2187 name: "coordinates".to_string(),
2188 location: ParameterIn::Query,
2189 description: Some("X,Y coordinates as tuple".to_string()),
2190 required: Some(true),
2191 deprecated: Some(false),
2192 allow_empty_value: Some(false),
2193 style: None,
2194 explode: None,
2195 allow_reserved: Some(false),
2196 schema: Some(ObjectOrReference::Object(ObjectSchema {
2197 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
2198 prefix_items: vec![
2199 ObjectOrReference::Object(ObjectSchema {
2200 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
2201 format: Some("double".to_string()),
2202 ..Default::default()
2203 }),
2204 ObjectOrReference::Object(ObjectSchema {
2205 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
2206 format: Some("double".to_string()),
2207 ..Default::default()
2208 }),
2209 ],
2210 items: Some(Box::new(Schema::Boolean(BooleanSchema(false)))),
2211 ..Default::default()
2212 })),
2213 example: None,
2214 examples: Default::default(),
2215 content: None,
2216 extensions: Default::default(),
2217 };
2218
2219 let spec = create_test_spec();
2220 let (result, _annotations) =
2221 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec).unwrap();
2222
2223 insta::assert_json_snapshot!("test_array_with_prefix_items_integration", result);
2225 }
2226
2227 #[test]
2228 fn test_array_with_regular_items_schema() {
2229 let param = Parameter {
2231 name: "tags".to_string(),
2232 location: ParameterIn::Query,
2233 description: Some("List of tags".to_string()),
2234 required: Some(false),
2235 deprecated: Some(false),
2236 allow_empty_value: Some(false),
2237 style: None,
2238 explode: None,
2239 allow_reserved: Some(false),
2240 schema: Some(ObjectOrReference::Object(ObjectSchema {
2241 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
2242 items: Some(Box::new(Schema::Object(Box::new(
2243 ObjectOrReference::Object(ObjectSchema {
2244 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2245 min_length: Some(1),
2246 max_length: Some(50),
2247 ..Default::default()
2248 }),
2249 )))),
2250 ..Default::default()
2251 })),
2252 example: None,
2253 examples: Default::default(),
2254 content: None,
2255 extensions: Default::default(),
2256 };
2257
2258 let spec = create_test_spec();
2259 let (result, _annotations) =
2260 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec).unwrap();
2261
2262 insta::assert_json_snapshot!("test_array_with_regular_items_schema", result);
2264 }
2265
2266 #[test]
2267 fn test_request_body_object_schema() {
2268 let operation = Operation {
2270 operation_id: Some("createPet".to_string()),
2271 summary: Some("Create a new pet".to_string()),
2272 description: Some("Creates a new pet in the store".to_string()),
2273 tags: vec![],
2274 external_docs: None,
2275 parameters: vec![],
2276 request_body: Some(ObjectOrReference::Object(RequestBody {
2277 description: Some("Pet object that needs to be added to the store".to_string()),
2278 content: {
2279 let mut content = BTreeMap::new();
2280 content.insert(
2281 "application/json".to_string(),
2282 MediaType {
2283 schema: Some(ObjectOrReference::Object(ObjectSchema {
2284 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2285 ..Default::default()
2286 })),
2287 examples: None,
2288 encoding: Default::default(),
2289 },
2290 );
2291 content
2292 },
2293 required: Some(true),
2294 })),
2295 responses: Default::default(),
2296 callbacks: Default::default(),
2297 deprecated: Some(false),
2298 security: vec![],
2299 servers: vec![],
2300 extensions: Default::default(),
2301 };
2302
2303 let spec = create_test_spec();
2304 let metadata = ToolGenerator::generate_tool_metadata(
2305 &operation,
2306 "post".to_string(),
2307 "/pets".to_string(),
2308 &spec,
2309 )
2310 .unwrap();
2311
2312 let properties = metadata
2314 .parameters
2315 .get("properties")
2316 .unwrap()
2317 .as_object()
2318 .unwrap();
2319 assert!(properties.contains_key("request_body"));
2320
2321 let required = metadata
2323 .parameters
2324 .get("required")
2325 .unwrap()
2326 .as_array()
2327 .unwrap();
2328 assert!(required.contains(&json!("request_body")));
2329
2330 let request_body_schema = properties.get("request_body").unwrap();
2332 insta::assert_json_snapshot!("test_request_body_object_schema", request_body_schema);
2333
2334 validate_tool_against_mcp_schema(&metadata);
2336 }
2337
2338 #[test]
2339 fn test_request_body_array_schema() {
2340 let operation = Operation {
2342 operation_id: Some("createPets".to_string()),
2343 summary: Some("Create multiple pets".to_string()),
2344 description: None,
2345 tags: vec![],
2346 external_docs: None,
2347 parameters: vec![],
2348 request_body: Some(ObjectOrReference::Object(RequestBody {
2349 description: Some("Array of pet objects".to_string()),
2350 content: {
2351 let mut content = BTreeMap::new();
2352 content.insert(
2353 "application/json".to_string(),
2354 MediaType {
2355 schema: Some(ObjectOrReference::Object(ObjectSchema {
2356 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
2357 items: Some(Box::new(Schema::Object(Box::new(
2358 ObjectOrReference::Object(ObjectSchema {
2359 schema_type: Some(SchemaTypeSet::Single(
2360 SchemaType::Object,
2361 )),
2362 ..Default::default()
2363 }),
2364 )))),
2365 ..Default::default()
2366 })),
2367 examples: None,
2368 encoding: Default::default(),
2369 },
2370 );
2371 content
2372 },
2373 required: Some(false),
2374 })),
2375 responses: Default::default(),
2376 callbacks: Default::default(),
2377 deprecated: Some(false),
2378 security: vec![],
2379 servers: vec![],
2380 extensions: Default::default(),
2381 };
2382
2383 let spec = create_test_spec();
2384 let metadata = ToolGenerator::generate_tool_metadata(
2385 &operation,
2386 "post".to_string(),
2387 "/pets/batch".to_string(),
2388 &spec,
2389 )
2390 .unwrap();
2391
2392 let properties = metadata
2394 .parameters
2395 .get("properties")
2396 .unwrap()
2397 .as_object()
2398 .unwrap();
2399 assert!(properties.contains_key("request_body"));
2400
2401 let required = metadata
2403 .parameters
2404 .get("required")
2405 .unwrap()
2406 .as_array()
2407 .unwrap();
2408 assert!(!required.contains(&json!("request_body")));
2409
2410 let request_body_schema = properties.get("request_body").unwrap();
2412 insta::assert_json_snapshot!("test_request_body_array_schema", request_body_schema);
2413
2414 validate_tool_against_mcp_schema(&metadata);
2416 }
2417
2418 #[test]
2419 fn test_request_body_string_schema() {
2420 let operation = Operation {
2422 operation_id: Some("updatePetName".to_string()),
2423 summary: Some("Update pet name".to_string()),
2424 description: None,
2425 tags: vec![],
2426 external_docs: None,
2427 parameters: vec![],
2428 request_body: Some(ObjectOrReference::Object(RequestBody {
2429 description: None,
2430 content: {
2431 let mut content = BTreeMap::new();
2432 content.insert(
2433 "text/plain".to_string(),
2434 MediaType {
2435 schema: Some(ObjectOrReference::Object(ObjectSchema {
2436 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2437 min_length: Some(1),
2438 max_length: Some(100),
2439 ..Default::default()
2440 })),
2441 examples: None,
2442 encoding: Default::default(),
2443 },
2444 );
2445 content
2446 },
2447 required: Some(true),
2448 })),
2449 responses: Default::default(),
2450 callbacks: Default::default(),
2451 deprecated: Some(false),
2452 security: vec![],
2453 servers: vec![],
2454 extensions: Default::default(),
2455 };
2456
2457 let spec = create_test_spec();
2458 let metadata = ToolGenerator::generate_tool_metadata(
2459 &operation,
2460 "put".to_string(),
2461 "/pets/{petId}/name".to_string(),
2462 &spec,
2463 )
2464 .unwrap();
2465
2466 let properties = metadata
2468 .parameters
2469 .get("properties")
2470 .unwrap()
2471 .as_object()
2472 .unwrap();
2473 let request_body_schema = properties.get("request_body").unwrap();
2474 insta::assert_json_snapshot!("test_request_body_string_schema", request_body_schema);
2475
2476 validate_tool_against_mcp_schema(&metadata);
2478 }
2479
2480 #[test]
2481 fn test_request_body_ref_schema() {
2482 let operation = Operation {
2484 operation_id: Some("updatePet".to_string()),
2485 summary: Some("Update existing pet".to_string()),
2486 description: None,
2487 tags: vec![],
2488 external_docs: None,
2489 parameters: vec![],
2490 request_body: Some(ObjectOrReference::Ref {
2491 ref_path: "#/components/requestBodies/PetBody".to_string(),
2492 }),
2493 responses: Default::default(),
2494 callbacks: Default::default(),
2495 deprecated: Some(false),
2496 security: vec![],
2497 servers: vec![],
2498 extensions: Default::default(),
2499 };
2500
2501 let spec = create_test_spec();
2502 let metadata = ToolGenerator::generate_tool_metadata(
2503 &operation,
2504 "put".to_string(),
2505 "/pets/{petId}".to_string(),
2506 &spec,
2507 )
2508 .unwrap();
2509
2510 let properties = metadata
2512 .parameters
2513 .get("properties")
2514 .unwrap()
2515 .as_object()
2516 .unwrap();
2517 let request_body_schema = properties.get("request_body").unwrap();
2518 insta::assert_json_snapshot!("test_request_body_ref_schema", request_body_schema);
2519
2520 validate_tool_against_mcp_schema(&metadata);
2522 }
2523
2524 #[test]
2525 fn test_no_request_body_for_get() {
2526 let operation = Operation {
2528 operation_id: Some("listPets".to_string()),
2529 summary: Some("List all pets".to_string()),
2530 description: None,
2531 tags: vec![],
2532 external_docs: None,
2533 parameters: vec![],
2534 request_body: None,
2535 responses: Default::default(),
2536 callbacks: Default::default(),
2537 deprecated: Some(false),
2538 security: vec![],
2539 servers: vec![],
2540 extensions: Default::default(),
2541 };
2542
2543 let spec = create_test_spec();
2544 let metadata = ToolGenerator::generate_tool_metadata(
2545 &operation,
2546 "get".to_string(),
2547 "/pets".to_string(),
2548 &spec,
2549 )
2550 .unwrap();
2551
2552 let properties = metadata
2554 .parameters
2555 .get("properties")
2556 .unwrap()
2557 .as_object()
2558 .unwrap();
2559 assert!(!properties.contains_key("request_body"));
2560
2561 validate_tool_against_mcp_schema(&metadata);
2563 }
2564
2565 #[test]
2566 fn test_request_body_simple_object_with_properties() {
2567 let operation = Operation {
2569 operation_id: Some("updatePetStatus".to_string()),
2570 summary: Some("Update pet status".to_string()),
2571 description: None,
2572 tags: vec![],
2573 external_docs: None,
2574 parameters: vec![],
2575 request_body: Some(ObjectOrReference::Object(RequestBody {
2576 description: Some("Pet status update".to_string()),
2577 content: {
2578 let mut content = BTreeMap::new();
2579 content.insert(
2580 "application/json".to_string(),
2581 MediaType {
2582 schema: Some(ObjectOrReference::Object(ObjectSchema {
2583 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2584 properties: {
2585 let mut props = BTreeMap::new();
2586 props.insert(
2587 "status".to_string(),
2588 ObjectOrReference::Object(ObjectSchema {
2589 schema_type: Some(SchemaTypeSet::Single(
2590 SchemaType::String,
2591 )),
2592 ..Default::default()
2593 }),
2594 );
2595 props.insert(
2596 "reason".to_string(),
2597 ObjectOrReference::Object(ObjectSchema {
2598 schema_type: Some(SchemaTypeSet::Single(
2599 SchemaType::String,
2600 )),
2601 ..Default::default()
2602 }),
2603 );
2604 props
2605 },
2606 required: vec!["status".to_string()],
2607 ..Default::default()
2608 })),
2609 examples: None,
2610 encoding: Default::default(),
2611 },
2612 );
2613 content
2614 },
2615 required: Some(false),
2616 })),
2617 responses: Default::default(),
2618 callbacks: Default::default(),
2619 deprecated: Some(false),
2620 security: vec![],
2621 servers: vec![],
2622 extensions: Default::default(),
2623 };
2624
2625 let spec = create_test_spec();
2626 let metadata = ToolGenerator::generate_tool_metadata(
2627 &operation,
2628 "patch".to_string(),
2629 "/pets/{petId}/status".to_string(),
2630 &spec,
2631 )
2632 .unwrap();
2633
2634 let properties = metadata
2636 .parameters
2637 .get("properties")
2638 .unwrap()
2639 .as_object()
2640 .unwrap();
2641 let request_body_schema = properties.get("request_body").unwrap();
2642 insta::assert_json_snapshot!(
2643 "test_request_body_simple_object_with_properties",
2644 request_body_schema
2645 );
2646
2647 let required = metadata
2649 .parameters
2650 .get("required")
2651 .unwrap()
2652 .as_array()
2653 .unwrap();
2654 assert!(!required.contains(&json!("request_body")));
2655
2656 validate_tool_against_mcp_schema(&metadata);
2658 }
2659
2660 #[test]
2661 fn test_request_body_with_nested_properties() {
2662 let operation = Operation {
2664 operation_id: Some("createUser".to_string()),
2665 summary: Some("Create a new user".to_string()),
2666 description: None,
2667 tags: vec![],
2668 external_docs: None,
2669 parameters: vec![],
2670 request_body: Some(ObjectOrReference::Object(RequestBody {
2671 description: Some("User creation data".to_string()),
2672 content: {
2673 let mut content = BTreeMap::new();
2674 content.insert(
2675 "application/json".to_string(),
2676 MediaType {
2677 schema: Some(ObjectOrReference::Object(ObjectSchema {
2678 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2679 properties: {
2680 let mut props = BTreeMap::new();
2681 props.insert(
2682 "name".to_string(),
2683 ObjectOrReference::Object(ObjectSchema {
2684 schema_type: Some(SchemaTypeSet::Single(
2685 SchemaType::String,
2686 )),
2687 ..Default::default()
2688 }),
2689 );
2690 props.insert(
2691 "age".to_string(),
2692 ObjectOrReference::Object(ObjectSchema {
2693 schema_type: Some(SchemaTypeSet::Single(
2694 SchemaType::Integer,
2695 )),
2696 minimum: Some(serde_json::Number::from(0)),
2697 maximum: Some(serde_json::Number::from(150)),
2698 ..Default::default()
2699 }),
2700 );
2701 props
2702 },
2703 required: vec!["name".to_string()],
2704 ..Default::default()
2705 })),
2706 examples: None,
2707 encoding: Default::default(),
2708 },
2709 );
2710 content
2711 },
2712 required: Some(true),
2713 })),
2714 responses: Default::default(),
2715 callbacks: Default::default(),
2716 deprecated: Some(false),
2717 security: vec![],
2718 servers: vec![],
2719 extensions: Default::default(),
2720 };
2721
2722 let spec = create_test_spec();
2723 let metadata = ToolGenerator::generate_tool_metadata(
2724 &operation,
2725 "post".to_string(),
2726 "/users".to_string(),
2727 &spec,
2728 )
2729 .unwrap();
2730
2731 let properties = metadata
2733 .parameters
2734 .get("properties")
2735 .unwrap()
2736 .as_object()
2737 .unwrap();
2738 let request_body_schema = properties.get("request_body").unwrap();
2739 insta::assert_json_snapshot!(
2740 "test_request_body_with_nested_properties",
2741 request_body_schema
2742 );
2743
2744 validate_tool_against_mcp_schema(&metadata);
2746 }
2747
2748 #[test]
2749 fn test_operation_without_responses_has_no_output_schema() {
2750 let operation = Operation {
2751 operation_id: Some("testOperation".to_string()),
2752 summary: Some("Test operation".to_string()),
2753 description: None,
2754 tags: vec![],
2755 external_docs: None,
2756 parameters: vec![],
2757 request_body: None,
2758 responses: None,
2759 callbacks: Default::default(),
2760 deprecated: Some(false),
2761 security: vec![],
2762 servers: vec![],
2763 extensions: Default::default(),
2764 };
2765
2766 let spec = create_test_spec();
2767 let metadata = ToolGenerator::generate_tool_metadata(
2768 &operation,
2769 "get".to_string(),
2770 "/test".to_string(),
2771 &spec,
2772 )
2773 .unwrap();
2774
2775 assert!(metadata.output_schema.is_none());
2777
2778 validate_tool_against_mcp_schema(&metadata);
2780 }
2781
2782 #[test]
2783 fn test_extract_output_schema_with_200_response() {
2784 use oas3::spec::Response;
2785
2786 let mut responses = BTreeMap::new();
2788 let mut content = BTreeMap::new();
2789 content.insert(
2790 "application/json".to_string(),
2791 MediaType {
2792 schema: Some(ObjectOrReference::Object(ObjectSchema {
2793 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2794 properties: {
2795 let mut props = BTreeMap::new();
2796 props.insert(
2797 "id".to_string(),
2798 ObjectOrReference::Object(ObjectSchema {
2799 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2800 ..Default::default()
2801 }),
2802 );
2803 props.insert(
2804 "name".to_string(),
2805 ObjectOrReference::Object(ObjectSchema {
2806 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2807 ..Default::default()
2808 }),
2809 );
2810 props
2811 },
2812 required: vec!["id".to_string(), "name".to_string()],
2813 ..Default::default()
2814 })),
2815 examples: None,
2816 encoding: Default::default(),
2817 },
2818 );
2819
2820 responses.insert(
2821 "200".to_string(),
2822 ObjectOrReference::Object(Response {
2823 description: Some("Successful response".to_string()),
2824 headers: Default::default(),
2825 content,
2826 links: Default::default(),
2827 extensions: Default::default(),
2828 }),
2829 );
2830
2831 let spec = create_test_spec();
2832 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
2833
2834 insta::assert_json_snapshot!(result);
2836 }
2837
2838 #[test]
2839 fn test_extract_output_schema_with_201_response() {
2840 use oas3::spec::Response;
2841
2842 let mut responses = BTreeMap::new();
2844 let mut content = BTreeMap::new();
2845 content.insert(
2846 "application/json".to_string(),
2847 MediaType {
2848 schema: Some(ObjectOrReference::Object(ObjectSchema {
2849 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2850 properties: {
2851 let mut props = BTreeMap::new();
2852 props.insert(
2853 "created".to_string(),
2854 ObjectOrReference::Object(ObjectSchema {
2855 schema_type: Some(SchemaTypeSet::Single(SchemaType::Boolean)),
2856 ..Default::default()
2857 }),
2858 );
2859 props
2860 },
2861 ..Default::default()
2862 })),
2863 examples: None,
2864 encoding: Default::default(),
2865 },
2866 );
2867
2868 responses.insert(
2869 "201".to_string(),
2870 ObjectOrReference::Object(Response {
2871 description: Some("Created".to_string()),
2872 headers: Default::default(),
2873 content,
2874 links: Default::default(),
2875 extensions: Default::default(),
2876 }),
2877 );
2878
2879 let spec = create_test_spec();
2880 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
2881
2882 insta::assert_json_snapshot!(result);
2884 }
2885
2886 #[test]
2887 fn test_extract_output_schema_with_2xx_response() {
2888 use oas3::spec::Response;
2889
2890 let mut responses = BTreeMap::new();
2892 let mut content = BTreeMap::new();
2893 content.insert(
2894 "application/json".to_string(),
2895 MediaType {
2896 schema: Some(ObjectOrReference::Object(ObjectSchema {
2897 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
2898 items: Some(Box::new(Schema::Object(Box::new(
2899 ObjectOrReference::Object(ObjectSchema {
2900 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2901 ..Default::default()
2902 }),
2903 )))),
2904 ..Default::default()
2905 })),
2906 examples: None,
2907 encoding: Default::default(),
2908 },
2909 );
2910
2911 responses.insert(
2912 "2XX".to_string(),
2913 ObjectOrReference::Object(Response {
2914 description: Some("Success".to_string()),
2915 headers: Default::default(),
2916 content,
2917 links: Default::default(),
2918 extensions: Default::default(),
2919 }),
2920 );
2921
2922 let spec = create_test_spec();
2923 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
2924
2925 insta::assert_json_snapshot!(result);
2927 }
2928
2929 #[test]
2930 fn test_extract_output_schema_no_responses() {
2931 let spec = create_test_spec();
2932 let result = ToolGenerator::extract_output_schema(&None, &spec).unwrap();
2933
2934 insta::assert_json_snapshot!(result);
2936 }
2937
2938 #[test]
2939 fn test_extract_output_schema_only_error_responses() {
2940 use oas3::spec::Response;
2941
2942 let mut responses = BTreeMap::new();
2944 responses.insert(
2945 "404".to_string(),
2946 ObjectOrReference::Object(Response {
2947 description: Some("Not found".to_string()),
2948 headers: Default::default(),
2949 content: Default::default(),
2950 links: Default::default(),
2951 extensions: Default::default(),
2952 }),
2953 );
2954 responses.insert(
2955 "500".to_string(),
2956 ObjectOrReference::Object(Response {
2957 description: Some("Server error".to_string()),
2958 headers: Default::default(),
2959 content: Default::default(),
2960 links: Default::default(),
2961 extensions: Default::default(),
2962 }),
2963 );
2964
2965 let spec = create_test_spec();
2966 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
2967
2968 insta::assert_json_snapshot!(result);
2970 }
2971
2972 #[test]
2973 fn test_extract_output_schema_with_ref() {
2974 use oas3::spec::Response;
2975
2976 let mut spec = create_test_spec();
2978 let mut schemas = BTreeMap::new();
2979 schemas.insert(
2980 "Pet".to_string(),
2981 ObjectOrReference::Object(ObjectSchema {
2982 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2983 properties: {
2984 let mut props = BTreeMap::new();
2985 props.insert(
2986 "name".to_string(),
2987 ObjectOrReference::Object(ObjectSchema {
2988 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2989 ..Default::default()
2990 }),
2991 );
2992 props
2993 },
2994 ..Default::default()
2995 }),
2996 );
2997 spec.components.as_mut().unwrap().schemas = schemas;
2998
2999 let mut responses = BTreeMap::new();
3001 let mut content = BTreeMap::new();
3002 content.insert(
3003 "application/json".to_string(),
3004 MediaType {
3005 schema: Some(ObjectOrReference::Ref {
3006 ref_path: "#/components/schemas/Pet".to_string(),
3007 }),
3008 examples: None,
3009 encoding: Default::default(),
3010 },
3011 );
3012
3013 responses.insert(
3014 "200".to_string(),
3015 ObjectOrReference::Object(Response {
3016 description: Some("Success".to_string()),
3017 headers: Default::default(),
3018 content,
3019 links: Default::default(),
3020 extensions: Default::default(),
3021 }),
3022 );
3023
3024 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
3025
3026 insta::assert_json_snapshot!(result);
3028 }
3029
3030 #[test]
3031 fn test_generate_tool_metadata_includes_output_schema() {
3032 use oas3::spec::Response;
3033
3034 let mut operation = Operation {
3035 operation_id: Some("getPet".to_string()),
3036 summary: Some("Get a pet".to_string()),
3037 description: None,
3038 tags: vec![],
3039 external_docs: None,
3040 parameters: vec![],
3041 request_body: None,
3042 responses: Default::default(),
3043 callbacks: Default::default(),
3044 deprecated: Some(false),
3045 security: vec![],
3046 servers: vec![],
3047 extensions: Default::default(),
3048 };
3049
3050 let mut responses = BTreeMap::new();
3052 let mut content = BTreeMap::new();
3053 content.insert(
3054 "application/json".to_string(),
3055 MediaType {
3056 schema: Some(ObjectOrReference::Object(ObjectSchema {
3057 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3058 properties: {
3059 let mut props = BTreeMap::new();
3060 props.insert(
3061 "id".to_string(),
3062 ObjectOrReference::Object(ObjectSchema {
3063 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3064 ..Default::default()
3065 }),
3066 );
3067 props
3068 },
3069 ..Default::default()
3070 })),
3071 examples: None,
3072 encoding: Default::default(),
3073 },
3074 );
3075
3076 responses.insert(
3077 "200".to_string(),
3078 ObjectOrReference::Object(Response {
3079 description: Some("Success".to_string()),
3080 headers: Default::default(),
3081 content,
3082 links: Default::default(),
3083 extensions: Default::default(),
3084 }),
3085 );
3086 operation.responses = Some(responses);
3087
3088 let spec = create_test_spec();
3089 let metadata = ToolGenerator::generate_tool_metadata(
3090 &operation,
3091 "get".to_string(),
3092 "/pets/{id}".to_string(),
3093 &spec,
3094 )
3095 .unwrap();
3096
3097 assert!(metadata.output_schema.is_some());
3099 let output_schema = metadata.output_schema.as_ref().unwrap();
3100
3101 insta::assert_json_snapshot!(
3103 "test_generate_tool_metadata_includes_output_schema",
3104 output_schema
3105 );
3106
3107 validate_tool_against_mcp_schema(&metadata);
3109 }
3110
3111 #[test]
3112 fn test_sanitize_property_name() {
3113 assert_eq!(sanitize_property_name("user name"), "user_name");
3115 assert_eq!(
3116 sanitize_property_name("first name last name"),
3117 "first_name_last_name"
3118 );
3119
3120 assert_eq!(sanitize_property_name("user(admin)"), "user_admin");
3122 assert_eq!(sanitize_property_name("user[admin]"), "user_admin");
3123 assert_eq!(sanitize_property_name("price($)"), "price");
3124 assert_eq!(sanitize_property_name("email@address"), "email_address");
3125 assert_eq!(sanitize_property_name("item#1"), "item_1");
3126 assert_eq!(sanitize_property_name("a/b/c"), "a_b_c");
3127
3128 assert_eq!(sanitize_property_name("user_name"), "user_name");
3130 assert_eq!(sanitize_property_name("userName123"), "userName123");
3131 assert_eq!(sanitize_property_name("user.name"), "user.name");
3132 assert_eq!(sanitize_property_name("user-name"), "user-name");
3133
3134 assert_eq!(sanitize_property_name("123name"), "param_123name");
3136 assert_eq!(sanitize_property_name("1st_place"), "param_1st_place");
3137
3138 assert_eq!(sanitize_property_name(""), "param_");
3140
3141 let long_name = "a".repeat(100);
3143 assert_eq!(sanitize_property_name(&long_name).len(), 64);
3144
3145 assert_eq!(sanitize_property_name("!@#$%^&*()"), "param_");
3148 }
3149
3150 #[test]
3151 fn test_sanitize_property_name_trailing_underscores() {
3152 assert_eq!(sanitize_property_name("page[size]"), "page_size");
3154 assert_eq!(sanitize_property_name("user[id]"), "user_id");
3155 assert_eq!(sanitize_property_name("field[]"), "field");
3156
3157 assert_eq!(sanitize_property_name("field___"), "field");
3159 assert_eq!(sanitize_property_name("test[[["), "test");
3160 }
3161
3162 #[test]
3163 fn test_sanitize_property_name_consecutive_underscores() {
3164 assert_eq!(sanitize_property_name("user__name"), "user_name");
3166 assert_eq!(sanitize_property_name("first___last"), "first_last");
3167 assert_eq!(sanitize_property_name("a____b____c"), "a_b_c");
3168
3169 assert_eq!(sanitize_property_name("user[[name]]"), "user_name");
3171 assert_eq!(sanitize_property_name("field@#$value"), "field_value");
3172 }
3173
3174 #[test]
3175 fn test_sanitize_property_name_edge_cases() {
3176 assert_eq!(sanitize_property_name("_private"), "_private");
3178 assert_eq!(sanitize_property_name("__dunder"), "_dunder");
3179
3180 assert_eq!(sanitize_property_name("[[["), "param_");
3182 assert_eq!(sanitize_property_name("@@@"), "param_");
3183
3184 assert_eq!(sanitize_property_name(""), "param_");
3186
3187 assert_eq!(sanitize_property_name("_field[size]"), "_field_size");
3189 assert_eq!(sanitize_property_name("__test__"), "_test");
3190 }
3191
3192 #[test]
3193 fn test_sanitize_property_name_complex_cases() {
3194 assert_eq!(sanitize_property_name("page[size]"), "page_size");
3196 assert_eq!(sanitize_property_name("filter[status]"), "filter_status");
3197 assert_eq!(
3198 sanitize_property_name("sort[-created_at]"),
3199 "sort_-created_at"
3200 );
3201 assert_eq!(
3202 sanitize_property_name("include[author.posts]"),
3203 "include_author.posts"
3204 );
3205
3206 let long_name = "very_long_field_name_with_special[characters]_that_needs_truncation_____";
3208 let expected = "very_long_field_name_with_special_characters_that_needs_truncat";
3209 assert_eq!(sanitize_property_name(long_name), expected);
3210 }
3211
3212 #[test]
3213 fn test_property_sanitization_with_annotations() {
3214 let spec = create_test_spec();
3215 let mut visited = HashSet::new();
3216
3217 let obj_schema = ObjectSchema {
3219 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3220 properties: {
3221 let mut props = BTreeMap::new();
3222 props.insert(
3224 "user name".to_string(),
3225 ObjectOrReference::Object(ObjectSchema {
3226 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3227 ..Default::default()
3228 }),
3229 );
3230 props.insert(
3232 "price($)".to_string(),
3233 ObjectOrReference::Object(ObjectSchema {
3234 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
3235 ..Default::default()
3236 }),
3237 );
3238 props.insert(
3240 "validName".to_string(),
3241 ObjectOrReference::Object(ObjectSchema {
3242 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3243 ..Default::default()
3244 }),
3245 );
3246 props
3247 },
3248 ..Default::default()
3249 };
3250
3251 let result =
3252 ToolGenerator::convert_object_schema_to_json_schema(&obj_schema, &spec, &mut visited)
3253 .unwrap();
3254
3255 insta::assert_json_snapshot!("test_property_sanitization_with_annotations", result);
3257 }
3258
3259 #[test]
3260 fn test_parameter_sanitization_and_extraction() {
3261 let spec = create_test_spec();
3262
3263 let operation = Operation {
3265 operation_id: Some("testOp".to_string()),
3266 parameters: vec![
3267 ObjectOrReference::Object(Parameter {
3269 name: "user(id)".to_string(),
3270 location: ParameterIn::Path,
3271 description: Some("User ID".to_string()),
3272 required: Some(true),
3273 deprecated: Some(false),
3274 allow_empty_value: Some(false),
3275 style: None,
3276 explode: None,
3277 allow_reserved: Some(false),
3278 schema: Some(ObjectOrReference::Object(ObjectSchema {
3279 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3280 ..Default::default()
3281 })),
3282 example: None,
3283 examples: Default::default(),
3284 content: None,
3285 extensions: Default::default(),
3286 }),
3287 ObjectOrReference::Object(Parameter {
3289 name: "page size".to_string(),
3290 location: ParameterIn::Query,
3291 description: Some("Page size".to_string()),
3292 required: Some(false),
3293 deprecated: Some(false),
3294 allow_empty_value: Some(false),
3295 style: None,
3296 explode: None,
3297 allow_reserved: Some(false),
3298 schema: Some(ObjectOrReference::Object(ObjectSchema {
3299 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3300 ..Default::default()
3301 })),
3302 example: None,
3303 examples: Default::default(),
3304 content: None,
3305 extensions: Default::default(),
3306 }),
3307 ObjectOrReference::Object(Parameter {
3309 name: "auth-token!".to_string(),
3310 location: ParameterIn::Header,
3311 description: Some("Auth token".to_string()),
3312 required: Some(false),
3313 deprecated: Some(false),
3314 allow_empty_value: Some(false),
3315 style: None,
3316 explode: None,
3317 allow_reserved: Some(false),
3318 schema: Some(ObjectOrReference::Object(ObjectSchema {
3319 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3320 ..Default::default()
3321 })),
3322 example: None,
3323 examples: Default::default(),
3324 content: None,
3325 extensions: Default::default(),
3326 }),
3327 ],
3328 ..Default::default()
3329 };
3330
3331 let tool_metadata = ToolGenerator::generate_tool_metadata(
3332 &operation,
3333 "get".to_string(),
3334 "/users/{user(id)}".to_string(),
3335 &spec,
3336 )
3337 .unwrap();
3338
3339 let properties = tool_metadata
3341 .parameters
3342 .get("properties")
3343 .unwrap()
3344 .as_object()
3345 .unwrap();
3346
3347 assert!(properties.contains_key("user_id"));
3348 assert!(properties.contains_key("page_size"));
3349 assert!(properties.contains_key("header_auth-token"));
3350
3351 let required = tool_metadata
3353 .parameters
3354 .get("required")
3355 .unwrap()
3356 .as_array()
3357 .unwrap();
3358 assert!(required.contains(&json!("user_id")));
3359
3360 let arguments = json!({
3362 "user_id": "123",
3363 "page_size": 10,
3364 "header_auth-token": "secret"
3365 });
3366
3367 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
3368
3369 assert_eq!(extracted.path.get("user(id)"), Some(&json!("123")));
3371
3372 assert_eq!(
3374 extracted.query.get("page size").map(|q| &q.value),
3375 Some(&json!(10))
3376 );
3377
3378 assert_eq!(extracted.headers.get("auth-token!"), Some(&json!("secret")));
3380 }
3381
3382 #[test]
3383 fn test_check_unknown_parameters() {
3384 let mut properties = serde_json::Map::new();
3386 properties.insert("page_size".to_string(), json!({"type": "integer"}));
3387 properties.insert("user_id".to_string(), json!({"type": "string"}));
3388
3389 let mut args = serde_json::Map::new();
3390 args.insert("page_sixe".to_string(), json!(10)); let result = ToolGenerator::check_unknown_parameters(&args, &properties);
3393 assert!(!result.is_empty());
3394 assert_eq!(result.len(), 1);
3395
3396 match &result[0] {
3397 ValidationError::InvalidParameter {
3398 parameter,
3399 suggestions,
3400 valid_parameters,
3401 } => {
3402 assert_eq!(parameter, "page_sixe");
3403 assert_eq!(suggestions, &vec!["page_size".to_string()]);
3404 assert_eq!(
3405 valid_parameters,
3406 &vec!["page_size".to_string(), "user_id".to_string()]
3407 );
3408 }
3409 _ => panic!("Expected InvalidParameter variant"),
3410 }
3411 }
3412
3413 #[test]
3414 fn test_check_unknown_parameters_no_suggestions() {
3415 let mut properties = serde_json::Map::new();
3417 properties.insert("limit".to_string(), json!({"type": "integer"}));
3418 properties.insert("offset".to_string(), json!({"type": "integer"}));
3419
3420 let mut args = serde_json::Map::new();
3421 args.insert("xyz123".to_string(), json!("value"));
3422
3423 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
3424 assert!(!result.is_empty());
3425 assert_eq!(result.len(), 1);
3426
3427 match &result[0] {
3428 ValidationError::InvalidParameter {
3429 parameter,
3430 suggestions,
3431 valid_parameters,
3432 } => {
3433 assert_eq!(parameter, "xyz123");
3434 assert!(suggestions.is_empty());
3435 assert!(valid_parameters.contains(&"limit".to_string()));
3436 assert!(valid_parameters.contains(&"offset".to_string()));
3437 }
3438 _ => panic!("Expected InvalidParameter variant"),
3439 }
3440 }
3441
3442 #[test]
3443 fn test_check_unknown_parameters_multiple_suggestions() {
3444 let mut properties = serde_json::Map::new();
3446 properties.insert("user_id".to_string(), json!({"type": "string"}));
3447 properties.insert("user_iid".to_string(), json!({"type": "string"}));
3448 properties.insert("user_name".to_string(), json!({"type": "string"}));
3449
3450 let mut args = serde_json::Map::new();
3451 args.insert("usr_id".to_string(), json!("123"));
3452
3453 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
3454 assert!(!result.is_empty());
3455 assert_eq!(result.len(), 1);
3456
3457 match &result[0] {
3458 ValidationError::InvalidParameter {
3459 parameter,
3460 suggestions,
3461 valid_parameters,
3462 } => {
3463 assert_eq!(parameter, "usr_id");
3464 assert!(!suggestions.is_empty());
3465 assert!(suggestions.contains(&"user_id".to_string()));
3466 assert_eq!(valid_parameters.len(), 3);
3467 }
3468 _ => panic!("Expected InvalidParameter variant"),
3469 }
3470 }
3471
3472 #[test]
3473 fn test_check_unknown_parameters_valid() {
3474 let mut properties = serde_json::Map::new();
3476 properties.insert("name".to_string(), json!({"type": "string"}));
3477 properties.insert("email".to_string(), json!({"type": "string"}));
3478
3479 let mut args = serde_json::Map::new();
3480 args.insert("name".to_string(), json!("John"));
3481 args.insert("email".to_string(), json!("john@example.com"));
3482
3483 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
3484 assert!(result.is_empty());
3485 }
3486
3487 #[test]
3488 fn test_check_unknown_parameters_empty() {
3489 let properties = serde_json::Map::new();
3491
3492 let mut args = serde_json::Map::new();
3493 args.insert("any_param".to_string(), json!("value"));
3494
3495 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
3496 assert!(!result.is_empty());
3497 assert_eq!(result.len(), 1);
3498
3499 match &result[0] {
3500 ValidationError::InvalidParameter {
3501 parameter,
3502 suggestions,
3503 valid_parameters,
3504 } => {
3505 assert_eq!(parameter, "any_param");
3506 assert!(suggestions.is_empty());
3507 assert!(valid_parameters.is_empty());
3508 }
3509 _ => panic!("Expected InvalidParameter variant"),
3510 }
3511 }
3512
3513 #[test]
3514 fn test_check_unknown_parameters_gltf_pagination() {
3515 let mut properties = serde_json::Map::new();
3517 properties.insert(
3518 "page_number".to_string(),
3519 json!({
3520 "type": "integer",
3521 "x-original-name": "page[number]"
3522 }),
3523 );
3524 properties.insert(
3525 "page_size".to_string(),
3526 json!({
3527 "type": "integer",
3528 "x-original-name": "page[size]"
3529 }),
3530 );
3531
3532 let mut args = serde_json::Map::new();
3534 args.insert("page".to_string(), json!(1));
3535 args.insert("per_page".to_string(), json!(10));
3536
3537 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
3538 assert_eq!(result.len(), 2, "Should have 2 unknown parameters");
3539
3540 let page_error = result
3542 .iter()
3543 .find(|e| {
3544 if let ValidationError::InvalidParameter { parameter, .. } = e {
3545 parameter == "page"
3546 } else {
3547 false
3548 }
3549 })
3550 .expect("Should have error for 'page'");
3551
3552 let per_page_error = result
3553 .iter()
3554 .find(|e| {
3555 if let ValidationError::InvalidParameter { parameter, .. } = e {
3556 parameter == "per_page"
3557 } else {
3558 false
3559 }
3560 })
3561 .expect("Should have error for 'per_page'");
3562
3563 match page_error {
3565 ValidationError::InvalidParameter {
3566 suggestions,
3567 valid_parameters,
3568 ..
3569 } => {
3570 assert!(
3571 suggestions.contains(&"page_number".to_string()),
3572 "Should suggest 'page_number' for 'page'"
3573 );
3574 assert_eq!(valid_parameters.len(), 2);
3575 assert!(valid_parameters.contains(&"page_number".to_string()));
3576 assert!(valid_parameters.contains(&"page_size".to_string()));
3577 }
3578 _ => panic!("Expected InvalidParameter"),
3579 }
3580
3581 match per_page_error {
3583 ValidationError::InvalidParameter {
3584 parameter,
3585 suggestions,
3586 valid_parameters,
3587 ..
3588 } => {
3589 assert_eq!(parameter, "per_page");
3590 assert_eq!(valid_parameters.len(), 2);
3591 if !suggestions.is_empty() {
3594 assert!(suggestions.contains(&"page_size".to_string()));
3595 }
3596 }
3597 _ => panic!("Expected InvalidParameter"),
3598 }
3599 }
3600
3601 #[test]
3602 fn test_validate_parameters_with_invalid_params() {
3603 let tool_metadata = ToolMetadata {
3605 name: "listItems".to_string(),
3606 title: None,
3607 description: "List items".to_string(),
3608 parameters: json!({
3609 "type": "object",
3610 "properties": {
3611 "page_number": {
3612 "type": "integer",
3613 "x-original-name": "page[number]"
3614 },
3615 "page_size": {
3616 "type": "integer",
3617 "x-original-name": "page[size]"
3618 }
3619 },
3620 "required": []
3621 }),
3622 output_schema: None,
3623 method: "GET".to_string(),
3624 path: "/items".to_string(),
3625 };
3626
3627 let arguments = json!({
3629 "page": 1,
3630 "per_page": 10
3631 });
3632
3633 let result = ToolGenerator::validate_parameters(&tool_metadata, &arguments);
3634 assert!(
3635 result.is_err(),
3636 "Should fail validation with unknown parameters"
3637 );
3638
3639 let error = result.unwrap_err();
3640 match error {
3641 ToolCallValidationError::InvalidParameters { violations } => {
3642 assert_eq!(violations.len(), 2, "Should have 2 validation errors");
3643
3644 let has_page_error = violations.iter().any(|v| {
3646 if let ValidationError::InvalidParameter { parameter, .. } = v {
3647 parameter == "page"
3648 } else {
3649 false
3650 }
3651 });
3652
3653 let has_per_page_error = violations.iter().any(|v| {
3654 if let ValidationError::InvalidParameter { parameter, .. } = v {
3655 parameter == "per_page"
3656 } else {
3657 false
3658 }
3659 });
3660
3661 assert!(has_page_error, "Should have error for 'page' parameter");
3662 assert!(
3663 has_per_page_error,
3664 "Should have error for 'per_page' parameter"
3665 );
3666 }
3667 _ => panic!("Expected InvalidParameters"),
3668 }
3669 }
3670
3671 #[test]
3672 fn test_cookie_parameter_sanitization() {
3673 let spec = create_test_spec();
3674
3675 let operation = Operation {
3676 operation_id: Some("testCookie".to_string()),
3677 parameters: vec![ObjectOrReference::Object(Parameter {
3678 name: "session[id]".to_string(),
3679 location: ParameterIn::Cookie,
3680 description: Some("Session ID".to_string()),
3681 required: Some(false),
3682 deprecated: Some(false),
3683 allow_empty_value: Some(false),
3684 style: None,
3685 explode: None,
3686 allow_reserved: Some(false),
3687 schema: Some(ObjectOrReference::Object(ObjectSchema {
3688 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3689 ..Default::default()
3690 })),
3691 example: None,
3692 examples: Default::default(),
3693 content: None,
3694 extensions: Default::default(),
3695 })],
3696 ..Default::default()
3697 };
3698
3699 let tool_metadata = ToolGenerator::generate_tool_metadata(
3700 &operation,
3701 "get".to_string(),
3702 "/data".to_string(),
3703 &spec,
3704 )
3705 .unwrap();
3706
3707 let properties = tool_metadata
3708 .parameters
3709 .get("properties")
3710 .unwrap()
3711 .as_object()
3712 .unwrap();
3713
3714 assert!(properties.contains_key("cookie_session_id"));
3716
3717 let arguments = json!({
3719 "cookie_session_id": "abc123"
3720 });
3721
3722 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
3723
3724 assert_eq!(extracted.cookies.get("session[id]"), Some(&json!("abc123")));
3726 }
3727
3728 #[test]
3729 fn test_parameter_description_with_examples() {
3730 let spec = create_test_spec();
3731
3732 let param_with_example = Parameter {
3734 name: "status".to_string(),
3735 location: ParameterIn::Query,
3736 description: Some("Filter by status".to_string()),
3737 required: Some(false),
3738 deprecated: Some(false),
3739 allow_empty_value: Some(false),
3740 style: None,
3741 explode: None,
3742 allow_reserved: Some(false),
3743 schema: Some(ObjectOrReference::Object(ObjectSchema {
3744 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3745 ..Default::default()
3746 })),
3747 example: Some(json!("active")),
3748 examples: Default::default(),
3749 content: None,
3750 extensions: Default::default(),
3751 };
3752
3753 let (schema, _) =
3754 ToolGenerator::convert_parameter_schema(¶m_with_example, ParameterIn::Query, &spec)
3755 .unwrap();
3756 let description = schema.get("description").unwrap().as_str().unwrap();
3757 assert_eq!(description, "Filter by status. Example: `\"active\"`");
3758
3759 let mut examples_map = std::collections::BTreeMap::new();
3761 examples_map.insert(
3762 "example1".to_string(),
3763 ObjectOrReference::Object(oas3::spec::Example {
3764 value: Some(json!("pending")),
3765 ..Default::default()
3766 }),
3767 );
3768 examples_map.insert(
3769 "example2".to_string(),
3770 ObjectOrReference::Object(oas3::spec::Example {
3771 value: Some(json!("completed")),
3772 ..Default::default()
3773 }),
3774 );
3775
3776 let param_with_examples = Parameter {
3777 name: "status".to_string(),
3778 location: ParameterIn::Query,
3779 description: Some("Filter by status".to_string()),
3780 required: Some(false),
3781 deprecated: Some(false),
3782 allow_empty_value: Some(false),
3783 style: None,
3784 explode: None,
3785 allow_reserved: Some(false),
3786 schema: Some(ObjectOrReference::Object(ObjectSchema {
3787 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3788 ..Default::default()
3789 })),
3790 example: None,
3791 examples: examples_map,
3792 content: None,
3793 extensions: Default::default(),
3794 };
3795
3796 let (schema, _) = ToolGenerator::convert_parameter_schema(
3797 ¶m_with_examples,
3798 ParameterIn::Query,
3799 &spec,
3800 )
3801 .unwrap();
3802 let description = schema.get("description").unwrap().as_str().unwrap();
3803 assert!(description.starts_with("Filter by status. Examples:\n"));
3804 assert!(description.contains("`\"pending\"`"));
3805 assert!(description.contains("`\"completed\"`"));
3806
3807 let param_no_desc = Parameter {
3809 name: "limit".to_string(),
3810 location: ParameterIn::Query,
3811 description: None,
3812 required: Some(false),
3813 deprecated: Some(false),
3814 allow_empty_value: Some(false),
3815 style: None,
3816 explode: None,
3817 allow_reserved: Some(false),
3818 schema: Some(ObjectOrReference::Object(ObjectSchema {
3819 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3820 ..Default::default()
3821 })),
3822 example: Some(json!(100)),
3823 examples: Default::default(),
3824 content: None,
3825 extensions: Default::default(),
3826 };
3827
3828 let (schema, _) =
3829 ToolGenerator::convert_parameter_schema(¶m_no_desc, ParameterIn::Query, &spec)
3830 .unwrap();
3831 let description = schema.get("description").unwrap().as_str().unwrap();
3832 assert_eq!(description, "limit parameter. Example: `100`");
3833 }
3834
3835 #[test]
3836 fn test_format_examples_for_description() {
3837 let examples = vec![json!("active")];
3839 let result = ToolGenerator::format_examples_for_description(&examples);
3840 assert_eq!(result, Some("Example: `\"active\"`".to_string()));
3841
3842 let examples = vec![json!(42)];
3844 let result = ToolGenerator::format_examples_for_description(&examples);
3845 assert_eq!(result, Some("Example: `42`".to_string()));
3846
3847 let examples = vec![json!(true)];
3849 let result = ToolGenerator::format_examples_for_description(&examples);
3850 assert_eq!(result, Some("Example: `true`".to_string()));
3851
3852 let examples = vec![json!("active"), json!("pending"), json!("completed")];
3854 let result = ToolGenerator::format_examples_for_description(&examples);
3855 assert_eq!(
3856 result,
3857 Some("Examples:\n- `\"active\"`\n- `\"pending\"`\n- `\"completed\"`".to_string())
3858 );
3859
3860 let examples = vec![json!(["a", "b", "c"])];
3862 let result = ToolGenerator::format_examples_for_description(&examples);
3863 assert_eq!(result, Some("Example: `[\"a\",\"b\",\"c\"]`".to_string()));
3864
3865 let examples = vec![json!({"key": "value"})];
3867 let result = ToolGenerator::format_examples_for_description(&examples);
3868 assert_eq!(result, Some("Example: `{\"key\":\"value\"}`".to_string()));
3869
3870 let examples = vec![];
3872 let result = ToolGenerator::format_examples_for_description(&examples);
3873 assert_eq!(result, None);
3874
3875 let examples = vec![json!(null)];
3877 let result = ToolGenerator::format_examples_for_description(&examples);
3878 assert_eq!(result, Some("Example: `null`".to_string()));
3879
3880 let examples = vec![json!("text"), json!(123), json!(true)];
3882 let result = ToolGenerator::format_examples_for_description(&examples);
3883 assert_eq!(
3884 result,
3885 Some("Examples:\n- `\"text\"`\n- `123`\n- `true`".to_string())
3886 );
3887
3888 let examples = vec![json!(["a", "b", "c", "d", "e", "f"])];
3890 let result = ToolGenerator::format_examples_for_description(&examples);
3891 assert_eq!(
3892 result,
3893 Some("Example: `[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\"]`".to_string())
3894 );
3895
3896 let examples = vec![json!([1, 2])];
3898 let result = ToolGenerator::format_examples_for_description(&examples);
3899 assert_eq!(result, Some("Example: `[1,2]`".to_string()));
3900
3901 let examples = vec![json!({"user": {"name": "John", "age": 30}})];
3903 let result = ToolGenerator::format_examples_for_description(&examples);
3904 assert_eq!(
3905 result,
3906 Some("Example: `{\"user\":{\"age\":30,\"name\":\"John\"}}`".to_string())
3907 );
3908
3909 let examples = vec![json!("a"), json!("b"), json!("c"), json!("d"), json!("e")];
3911 let result = ToolGenerator::format_examples_for_description(&examples);
3912 assert_eq!(
3913 result,
3914 Some("Examples:\n- `\"a\"`\n- `\"b\"`\n- `\"c\"`\n- `\"d\"`\n- `\"e\"`".to_string())
3915 );
3916
3917 let examples = vec![json!(3.5)];
3919 let result = ToolGenerator::format_examples_for_description(&examples);
3920 assert_eq!(result, Some("Example: `3.5`".to_string()));
3921
3922 let examples = vec![json!(-42)];
3924 let result = ToolGenerator::format_examples_for_description(&examples);
3925 assert_eq!(result, Some("Example: `-42`".to_string()));
3926
3927 let examples = vec![json!(false)];
3929 let result = ToolGenerator::format_examples_for_description(&examples);
3930 assert_eq!(result, Some("Example: `false`".to_string()));
3931
3932 let examples = vec![json!("hello \"world\"")];
3934 let result = ToolGenerator::format_examples_for_description(&examples);
3935 assert_eq!(result, Some(r#"Example: `"hello \"world\""`"#.to_string()));
3937
3938 let examples = vec![json!("")];
3940 let result = ToolGenerator::format_examples_for_description(&examples);
3941 assert_eq!(result, Some("Example: `\"\"`".to_string()));
3942
3943 let examples = vec![json!([])];
3945 let result = ToolGenerator::format_examples_for_description(&examples);
3946 assert_eq!(result, Some("Example: `[]`".to_string()));
3947
3948 let examples = vec![json!({})];
3950 let result = ToolGenerator::format_examples_for_description(&examples);
3951 assert_eq!(result, Some("Example: `{}`".to_string()));
3952 }
3953}