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