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