1use schemars::schema_for;
2use serde::{Serialize, Serializer};
3use serde_json::{Value, json};
4use std::collections::{BTreeMap, HashMap, HashSet};
5
6use crate::error::{
7 Error, ErrorResponse, 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};
14use tracing::{trace, warn};
15
16const X_LOCATION: &str = "x-location";
18const X_PARAMETER_LOCATION: &str = "x-parameter-location";
19const X_PARAMETER_REQUIRED: &str = "x-parameter-required";
20const X_CONTENT_TYPE: &str = "x-content-type";
21const X_ORIGINAL_NAME: &str = "x-original-name";
22const X_PARAMETER_EXPLODE: &str = "x-parameter-explode";
23
24#[derive(Debug, Clone, Copy, PartialEq)]
26pub enum Location {
27 Parameter(ParameterIn),
29 Body,
31}
32
33impl Serialize for Location {
34 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
35 where
36 S: Serializer,
37 {
38 let str_value = match self {
39 Location::Parameter(param_in) => match param_in {
40 ParameterIn::Query => "query",
41 ParameterIn::Header => "header",
42 ParameterIn::Path => "path",
43 ParameterIn::Cookie => "cookie",
44 },
45 Location::Body => "body",
46 };
47 serializer.serialize_str(str_value)
48 }
49}
50
51#[derive(Debug, Clone, PartialEq)]
53pub enum Annotation {
54 Location(Location),
56 Required(bool),
58 ContentType(String),
60 OriginalName(String),
62 Explode(bool),
64}
65
66#[derive(Debug, Clone, Default)]
68pub struct Annotations {
69 annotations: Vec<Annotation>,
70}
71
72impl Annotations {
73 pub fn new() -> Self {
75 Self {
76 annotations: Vec::new(),
77 }
78 }
79
80 pub fn with_location(mut self, location: Location) -> Self {
82 self.annotations.push(Annotation::Location(location));
83 self
84 }
85
86 pub fn with_required(mut self, required: bool) -> Self {
88 self.annotations.push(Annotation::Required(required));
89 self
90 }
91
92 pub fn with_content_type(mut self, content_type: String) -> Self {
94 self.annotations.push(Annotation::ContentType(content_type));
95 self
96 }
97
98 pub fn with_original_name(mut self, original_name: String) -> Self {
100 self.annotations
101 .push(Annotation::OriginalName(original_name));
102 self
103 }
104
105 pub fn with_explode(mut self, explode: bool) -> Self {
107 self.annotations.push(Annotation::Explode(explode));
108 self
109 }
110}
111
112impl Serialize for Annotations {
113 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
114 where
115 S: Serializer,
116 {
117 use serde::ser::SerializeMap;
118
119 let mut map = serializer.serialize_map(Some(self.annotations.len()))?;
120
121 for annotation in &self.annotations {
122 match annotation {
123 Annotation::Location(location) => {
124 let key = match location {
126 Location::Parameter(param_in) => match param_in {
127 ParameterIn::Header | ParameterIn::Cookie => X_LOCATION,
128 _ => X_PARAMETER_LOCATION,
129 },
130 Location::Body => X_LOCATION,
131 };
132 map.serialize_entry(key, &location)?;
133
134 if let Location::Parameter(_) = location {
136 map.serialize_entry(X_PARAMETER_LOCATION, &location)?;
137 }
138 }
139 Annotation::Required(required) => {
140 map.serialize_entry(X_PARAMETER_REQUIRED, required)?;
141 }
142 Annotation::ContentType(content_type) => {
143 map.serialize_entry(X_CONTENT_TYPE, content_type)?;
144 }
145 Annotation::OriginalName(original_name) => {
146 map.serialize_entry(X_ORIGINAL_NAME, original_name)?;
147 }
148 Annotation::Explode(explode) => {
149 map.serialize_entry(X_PARAMETER_EXPLODE, explode)?;
150 }
151 }
152 }
153
154 map.end()
155 }
156}
157
158fn sanitize_property_name(name: &str) -> String {
167 let sanitized = name
169 .chars()
170 .map(|c| match c {
171 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '.' | '-' => c,
172 _ => '_',
173 })
174 .take(64)
175 .collect::<String>();
176
177 let mut collapsed = String::with_capacity(sanitized.len());
179 let mut prev_was_underscore = false;
180
181 for ch in sanitized.chars() {
182 if ch == '_' {
183 if !prev_was_underscore {
184 collapsed.push(ch);
185 }
186 prev_was_underscore = true;
187 } else {
188 collapsed.push(ch);
189 prev_was_underscore = false;
190 }
191 }
192
193 let trimmed = collapsed.trim_end_matches('_');
195
196 if trimmed.is_empty() || trimmed.chars().next().unwrap_or('0').is_numeric() {
198 format!("param_{trimmed}")
199 } else {
200 trimmed.to_string()
201 }
202}
203
204pub struct ToolGenerator;
206
207impl ToolGenerator {
208 pub fn generate_tool_metadata(
214 operation: &Operation,
215 method: String,
216 path: String,
217 spec: &Spec,
218 ) -> Result<ToolMetadata, Error> {
219 let name = operation.operation_id.clone().unwrap_or_else(|| {
220 format!(
221 "{}_{}",
222 method,
223 path.replace('/', "_").replace(['{', '}'], "")
224 )
225 });
226
227 let parameters = Self::generate_parameter_schema(
229 &operation.parameters,
230 &method,
231 &operation.request_body,
232 spec,
233 )?;
234
235 let description = Self::build_description(operation, &method, &path);
237
238 let output_schema = Self::extract_output_schema(&operation.responses, spec)?;
240
241 Ok(ToolMetadata {
242 name,
243 title: operation.summary.clone(),
244 description,
245 parameters,
246 output_schema,
247 method,
248 path,
249 })
250 }
251
252 pub fn generate_openapi_tools(
258 tools_metadata: Vec<ToolMetadata>,
259 base_url: Option<url::Url>,
260 default_headers: Option<reqwest::header::HeaderMap>,
261 ) -> Result<Vec<crate::tool::Tool>, Error> {
262 let mut openapi_tools = Vec::with_capacity(tools_metadata.len());
263
264 for metadata in tools_metadata {
265 let tool = crate::tool::Tool::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>, Error> {
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, Error> {
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, Error> {
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, Error> {
638 if visited.contains(ref_path) {
640 return Err(Error::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(Error::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 Error::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 Error::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, Error> {
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 warn!(
712 reference_path = %ref_path,
713 "Parameter reference not resolved"
714 );
715 continue;
716 }
717 };
718
719 match ¶m.location {
720 ParameterIn::Query => query_params.push(param),
721 ParameterIn::Header => header_params.push(param),
722 ParameterIn::Path => path_params.push(param),
723 ParameterIn::Cookie => cookie_params.push(param),
724 }
725 }
726
727 for param in path_params {
729 let (param_schema, mut annotations) =
730 Self::convert_parameter_schema(param, ParameterIn::Path, spec)?;
731
732 let sanitized_name = sanitize_property_name(¶m.name);
734 if sanitized_name != param.name {
735 annotations = annotations.with_original_name(param.name.clone());
736 }
737
738 let param_schema_with_annotations =
739 Self::apply_annotations_to_schema(param_schema, annotations);
740 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
741 required.push(sanitized_name);
742 }
743
744 for param in &query_params {
746 let (param_schema, mut annotations) =
747 Self::convert_parameter_schema(param, ParameterIn::Query, spec)?;
748
749 let sanitized_name = sanitize_property_name(¶m.name);
751 if sanitized_name != param.name {
752 annotations = annotations.with_original_name(param.name.clone());
753 }
754
755 let param_schema_with_annotations =
756 Self::apply_annotations_to_schema(param_schema, annotations);
757 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
758 if param.required.unwrap_or(false) {
759 required.push(sanitized_name);
760 }
761 }
762
763 for param in &header_params {
765 let (param_schema, mut annotations) =
766 Self::convert_parameter_schema(param, ParameterIn::Header, spec)?;
767
768 let prefixed_name = format!("header_{}", param.name);
770 let sanitized_name = sanitize_property_name(&prefixed_name);
771 if sanitized_name != prefixed_name {
772 annotations = annotations.with_original_name(param.name.clone());
773 }
774
775 let param_schema_with_annotations =
776 Self::apply_annotations_to_schema(param_schema, annotations);
777
778 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
779 if param.required.unwrap_or(false) {
780 required.push(sanitized_name);
781 }
782 }
783
784 for param in &cookie_params {
786 let (param_schema, mut annotations) =
787 Self::convert_parameter_schema(param, ParameterIn::Cookie, spec)?;
788
789 let prefixed_name = format!("cookie_{}", param.name);
791 let sanitized_name = sanitize_property_name(&prefixed_name);
792 if sanitized_name != prefixed_name {
793 annotations = annotations.with_original_name(param.name.clone());
794 }
795
796 let param_schema_with_annotations =
797 Self::apply_annotations_to_schema(param_schema, annotations);
798
799 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
800 if param.required.unwrap_or(false) {
801 required.push(sanitized_name);
802 }
803 }
804
805 if let Some(request_body) = request_body
807 && let Some((body_schema, annotations, is_required)) =
808 Self::convert_request_body_to_json_schema(request_body, spec)?
809 {
810 let body_schema_with_annotations =
811 Self::apply_annotations_to_schema(body_schema, annotations);
812 properties.insert("request_body".to_string(), body_schema_with_annotations);
813 if is_required {
814 required.push("request_body".to_string());
815 }
816 }
817
818 if !query_params.is_empty() || !header_params.is_empty() || !cookie_params.is_empty() {
820 properties.insert(
822 "timeout_seconds".to_string(),
823 json!({
824 "type": "integer",
825 "description": "Request timeout in seconds",
826 "minimum": 1,
827 "maximum": 300,
828 "default": 30
829 }),
830 );
831 }
832
833 Ok(json!({
834 "type": "object",
835 "properties": properties,
836 "required": required,
837 "additionalProperties": false
838 }))
839 }
840
841 fn convert_parameter_schema(
843 param: &Parameter,
844 location: ParameterIn,
845 spec: &Spec,
846 ) -> Result<(Value, Annotations), Error> {
847 let base_schema = if let Some(schema_ref) = ¶m.schema {
849 match schema_ref {
850 ObjectOrReference::Object(obj_schema) => {
851 let mut visited = HashSet::new();
852 Self::convert_schema_to_json_schema(
853 &Schema::Object(Box::new(ObjectOrReference::Object(obj_schema.clone()))),
854 spec,
855 &mut visited,
856 )?
857 }
858 ObjectOrReference::Ref { ref_path } => {
859 let mut visited = HashSet::new();
861 match Self::resolve_reference(ref_path, spec, &mut visited) {
862 Ok(resolved_schema) => Self::convert_schema_to_json_schema(
863 &Schema::Object(Box::new(ObjectOrReference::Object(resolved_schema))),
864 spec,
865 &mut visited,
866 )?,
867 Err(_) => {
868 json!({"type": "string"})
870 }
871 }
872 }
873 }
874 } else {
875 json!({"type": "string"})
877 };
878
879 let mut result = match base_schema {
881 Value::Object(obj) => obj,
882 _ => {
883 return Err(Error::ToolGeneration(format!(
885 "Internal error: schema converter returned non-object for parameter '{}'",
886 param.name
887 )));
888 }
889 };
890
891 let mut collected_examples = Vec::new();
893
894 if let Some(example) = ¶m.example {
896 collected_examples.push(example.clone());
897 } else if !param.examples.is_empty() {
898 for example_ref in param.examples.values() {
900 match example_ref {
901 ObjectOrReference::Object(example_obj) => {
902 if let Some(value) = &example_obj.value {
903 collected_examples.push(value.clone());
904 }
905 }
906 ObjectOrReference::Ref { .. } => {
907 }
909 }
910 }
911 } else if let Some(Value::String(ex_str)) = result.get("example") {
912 collected_examples.push(json!(ex_str));
914 } else if let Some(ex) = result.get("example") {
915 collected_examples.push(ex.clone());
916 }
917
918 let base_description = param
920 .description
921 .as_ref()
922 .map(|d| d.to_string())
923 .or_else(|| {
924 result
925 .get("description")
926 .and_then(|d| d.as_str())
927 .map(|d| d.to_string())
928 })
929 .unwrap_or_else(|| format!("{} parameter", param.name));
930
931 let description_with_examples = if let Some(examples_str) =
932 Self::format_examples_for_description(&collected_examples)
933 {
934 format!("{base_description}. {examples_str}")
935 } else {
936 base_description
937 };
938
939 result.insert("description".to_string(), json!(description_with_examples));
940
941 if let Some(example) = ¶m.example {
946 result.insert("example".to_string(), example.clone());
947 } else if !param.examples.is_empty() {
948 let mut examples_array = Vec::new();
951 for (example_name, example_ref) in ¶m.examples {
952 match example_ref {
953 ObjectOrReference::Object(example_obj) => {
954 if let Some(value) = &example_obj.value {
955 examples_array.push(json!({
956 "name": example_name,
957 "value": value
958 }));
959 }
960 }
961 ObjectOrReference::Ref { .. } => {
962 }
965 }
966 }
967
968 if !examples_array.is_empty() {
969 if let Some(first_example) = examples_array.first()
971 && let Some(value) = first_example.get("value")
972 {
973 result.insert("example".to_string(), value.clone());
974 }
975 result.insert("x-examples".to_string(), json!(examples_array));
977 }
978 }
979
980 let mut annotations = Annotations::new()
982 .with_location(Location::Parameter(location))
983 .with_required(param.required.unwrap_or(false));
984
985 if let Some(explode) = param.explode {
987 annotations = annotations.with_explode(explode);
988 } else {
989 let default_explode = match ¶m.style {
993 Some(ParameterStyle::Form) | None => true, _ => false,
995 };
996 annotations = annotations.with_explode(default_explode);
997 }
998
999 Ok((Value::Object(result), annotations))
1000 }
1001
1002 fn apply_annotations_to_schema(schema: Value, annotations: Annotations) -> Value {
1004 match schema {
1005 Value::Object(mut obj) => {
1006 if let Ok(Value::Object(ann_map)) = serde_json::to_value(&annotations) {
1008 for (key, value) in ann_map {
1009 obj.insert(key, value);
1010 }
1011 }
1012 Value::Object(obj)
1013 }
1014 _ => schema,
1015 }
1016 }
1017
1018 fn format_examples_for_description(examples: &[Value]) -> Option<String> {
1020 if examples.is_empty() {
1021 return None;
1022 }
1023
1024 if examples.len() == 1 {
1025 let example_str =
1026 serde_json::to_string(&examples[0]).unwrap_or_else(|_| "null".to_string());
1027 Some(format!("Example: `{example_str}`"))
1028 } else {
1029 let mut result = String::from("Examples:\n");
1030 for ex in examples {
1031 let json_str = serde_json::to_string(ex).unwrap_or_else(|_| "null".to_string());
1032 result.push_str(&format!("- `{json_str}`\n"));
1033 }
1034 result.pop();
1036 Some(result)
1037 }
1038 }
1039
1040 fn convert_prefix_items_to_draft07(
1051 prefix_items: &[ObjectOrReference<ObjectSchema>],
1052 items: &Option<Box<Schema>>,
1053 result: &mut serde_json::Map<String, Value>,
1054 spec: &Spec,
1055 ) -> Result<(), Error> {
1056 let prefix_count = prefix_items.len();
1057
1058 let mut item_types = Vec::new();
1060 for prefix_item in prefix_items {
1061 match prefix_item {
1062 ObjectOrReference::Object(obj_schema) => {
1063 if let Some(schema_type) = &obj_schema.schema_type {
1064 match schema_type {
1065 SchemaTypeSet::Single(SchemaType::String) => item_types.push("string"),
1066 SchemaTypeSet::Single(SchemaType::Integer) => {
1067 item_types.push("integer")
1068 }
1069 SchemaTypeSet::Single(SchemaType::Number) => item_types.push("number"),
1070 SchemaTypeSet::Single(SchemaType::Boolean) => {
1071 item_types.push("boolean")
1072 }
1073 SchemaTypeSet::Single(SchemaType::Array) => item_types.push("array"),
1074 SchemaTypeSet::Single(SchemaType::Object) => item_types.push("object"),
1075 _ => item_types.push("string"), }
1077 } else {
1078 item_types.push("string"); }
1080 }
1081 ObjectOrReference::Ref { ref_path } => {
1082 let mut visited = HashSet::new();
1084 match Self::resolve_reference(ref_path, spec, &mut visited) {
1085 Ok(resolved_schema) => {
1086 if let Some(schema_type_set) = &resolved_schema.schema_type {
1088 match schema_type_set {
1089 SchemaTypeSet::Single(SchemaType::String) => {
1090 item_types.push("string")
1091 }
1092 SchemaTypeSet::Single(SchemaType::Integer) => {
1093 item_types.push("integer")
1094 }
1095 SchemaTypeSet::Single(SchemaType::Number) => {
1096 item_types.push("number")
1097 }
1098 SchemaTypeSet::Single(SchemaType::Boolean) => {
1099 item_types.push("boolean")
1100 }
1101 SchemaTypeSet::Single(SchemaType::Array) => {
1102 item_types.push("array")
1103 }
1104 SchemaTypeSet::Single(SchemaType::Object) => {
1105 item_types.push("object")
1106 }
1107 _ => item_types.push("string"), }
1109 } else {
1110 item_types.push("string"); }
1112 }
1113 Err(_) => {
1114 item_types.push("string");
1116 }
1117 }
1118 }
1119 }
1120 }
1121
1122 let items_is_false =
1124 matches!(items.as_ref().map(|i| i.as_ref()), Some(Schema::Boolean(b)) if !b.0);
1125
1126 if items_is_false {
1127 result.insert("minItems".to_string(), json!(prefix_count));
1129 result.insert("maxItems".to_string(), json!(prefix_count));
1130 }
1131
1132 let unique_types: std::collections::BTreeSet<_> = item_types.into_iter().collect();
1134
1135 if unique_types.len() == 1 {
1136 let item_type = unique_types.into_iter().next().unwrap();
1138 result.insert("items".to_string(), json!({"type": item_type}));
1139 } else if unique_types.len() > 1 {
1140 let one_of: Vec<Value> = unique_types
1142 .into_iter()
1143 .map(|t| json!({"type": t}))
1144 .collect();
1145 result.insert("items".to_string(), json!({"oneOf": one_of}));
1146 }
1147
1148 Ok(())
1149 }
1150
1151 fn convert_request_body_to_json_schema(
1163 request_body_ref: &ObjectOrReference<RequestBody>,
1164 spec: &Spec,
1165 ) -> Result<Option<(Value, Annotations, bool)>, Error> {
1166 match request_body_ref {
1167 ObjectOrReference::Object(request_body) => {
1168 let schema_info = request_body
1171 .content
1172 .get(mime::APPLICATION_JSON.as_ref())
1173 .or_else(|| request_body.content.get("application/json"))
1174 .or_else(|| {
1175 request_body.content.values().next()
1177 });
1178
1179 if let Some(media_type) = schema_info {
1180 if let Some(schema_ref) = &media_type.schema {
1181 let schema = Schema::Object(Box::new(schema_ref.clone()));
1183
1184 let mut visited = HashSet::new();
1186 let converted_schema =
1187 Self::convert_schema_to_json_schema(&schema, spec, &mut visited)?;
1188
1189 let mut schema_obj = match converted_schema {
1191 Value::Object(obj) => obj,
1192 _ => {
1193 let mut obj = serde_json::Map::new();
1195 obj.insert("type".to_string(), json!("object"));
1196 obj.insert("additionalProperties".to_string(), json!(true));
1197 obj
1198 }
1199 };
1200
1201 let description = request_body
1203 .description
1204 .clone()
1205 .unwrap_or_else(|| "Request body data".to_string());
1206 schema_obj.insert("description".to_string(), json!(description));
1207
1208 let annotations = Annotations::new()
1210 .with_location(Location::Body)
1211 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
1212
1213 let required = request_body.required.unwrap_or(false);
1214 Ok(Some((Value::Object(schema_obj), annotations, required)))
1215 } else {
1216 Ok(None)
1217 }
1218 } else {
1219 Ok(None)
1220 }
1221 }
1222 ObjectOrReference::Ref { .. } => {
1223 let mut result = serde_json::Map::new();
1225 result.insert("type".to_string(), json!("object"));
1226 result.insert("additionalProperties".to_string(), json!(true));
1227 result.insert("description".to_string(), json!("Request body data"));
1228
1229 let annotations = Annotations::new()
1231 .with_location(Location::Body)
1232 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
1233
1234 Ok(Some((Value::Object(result), annotations, false)))
1235 }
1236 }
1237 }
1238
1239 pub fn extract_parameters(
1245 tool_metadata: &ToolMetadata,
1246 arguments: &Value,
1247 ) -> Result<ExtractedParameters, ToolCallValidationError> {
1248 let args = arguments.as_object().ok_or_else(|| {
1249 ToolCallValidationError::RequestConstructionError {
1250 reason: "Arguments must be an object".to_string(),
1251 }
1252 })?;
1253
1254 trace!(
1255 tool_name = %tool_metadata.name,
1256 raw_arguments = ?arguments,
1257 "Starting parameter extraction"
1258 );
1259
1260 let mut path_params = HashMap::new();
1261 let mut query_params = HashMap::new();
1262 let mut header_params = HashMap::new();
1263 let mut cookie_params = HashMap::new();
1264 let mut body_params = HashMap::new();
1265 let mut config = RequestConfig::default();
1266
1267 if let Some(timeout) = args.get("timeout_seconds").and_then(Value::as_u64) {
1269 config.timeout_seconds = u32::try_from(timeout).unwrap_or(u32::MAX);
1270 }
1271
1272 for (key, value) in args {
1274 if key == "timeout_seconds" {
1275 continue; }
1277
1278 if key == "request_body" {
1280 body_params.insert("request_body".to_string(), value.clone());
1281 continue;
1282 }
1283
1284 let location = Self::get_parameter_location(tool_metadata, key).map_err(|e| {
1286 ToolCallValidationError::RequestConstructionError {
1287 reason: e.to_string(),
1288 }
1289 })?;
1290
1291 let original_name = Self::get_original_parameter_name(tool_metadata, key);
1293
1294 match location.as_str() {
1295 "path" => {
1296 path_params.insert(original_name.unwrap_or_else(|| key.clone()), value.clone());
1297 }
1298 "query" => {
1299 let param_name = original_name.unwrap_or_else(|| key.clone());
1300 let explode = Self::get_parameter_explode(tool_metadata, key);
1301 query_params.insert(param_name, QueryParameter::new(value.clone(), explode));
1302 }
1303 "header" => {
1304 let header_name = if let Some(orig) = original_name {
1306 orig
1307 } else if key.starts_with("header_") {
1308 key.strip_prefix("header_").unwrap_or(key).to_string()
1309 } else {
1310 key.clone()
1311 };
1312 header_params.insert(header_name, value.clone());
1313 }
1314 "cookie" => {
1315 let cookie_name = if let Some(orig) = original_name {
1317 orig
1318 } else if key.starts_with("cookie_") {
1319 key.strip_prefix("cookie_").unwrap_or(key).to_string()
1320 } else {
1321 key.clone()
1322 };
1323 cookie_params.insert(cookie_name, value.clone());
1324 }
1325 "body" => {
1326 let body_name = if key.starts_with("body_") {
1328 key.strip_prefix("body_").unwrap_or(key).to_string()
1329 } else {
1330 key.clone()
1331 };
1332 body_params.insert(body_name, value.clone());
1333 }
1334 _ => {
1335 return Err(ToolCallValidationError::RequestConstructionError {
1336 reason: format!("Unknown parameter location for parameter: {key}"),
1337 });
1338 }
1339 }
1340 }
1341
1342 let extracted = ExtractedParameters {
1343 path: path_params,
1344 query: query_params,
1345 headers: header_params,
1346 cookies: cookie_params,
1347 body: body_params,
1348 config,
1349 };
1350
1351 trace!(
1352 tool_name = %tool_metadata.name,
1353 extracted_parameters = ?extracted,
1354 "Parameter extraction completed"
1355 );
1356
1357 Self::validate_parameters(tool_metadata, arguments)?;
1359
1360 Ok(extracted)
1361 }
1362
1363 fn get_original_parameter_name(
1365 tool_metadata: &ToolMetadata,
1366 param_name: &str,
1367 ) -> Option<String> {
1368 tool_metadata
1369 .parameters
1370 .get("properties")
1371 .and_then(|p| p.as_object())
1372 .and_then(|props| props.get(param_name))
1373 .and_then(|schema| schema.get(X_ORIGINAL_NAME))
1374 .and_then(|v| v.as_str())
1375 .map(|s| s.to_string())
1376 }
1377
1378 fn get_parameter_explode(tool_metadata: &ToolMetadata, param_name: &str) -> bool {
1380 tool_metadata
1381 .parameters
1382 .get("properties")
1383 .and_then(|p| p.as_object())
1384 .and_then(|props| props.get(param_name))
1385 .and_then(|schema| schema.get(X_PARAMETER_EXPLODE))
1386 .and_then(|v| v.as_bool())
1387 .unwrap_or(true) }
1389
1390 fn get_parameter_location(
1392 tool_metadata: &ToolMetadata,
1393 param_name: &str,
1394 ) -> Result<String, Error> {
1395 let properties = tool_metadata
1396 .parameters
1397 .get("properties")
1398 .and_then(|p| p.as_object())
1399 .ok_or_else(|| Error::ToolGeneration("Invalid tool parameters schema".to_string()))?;
1400
1401 if let Some(param_schema) = properties.get(param_name)
1402 && let Some(location) = param_schema
1403 .get(X_PARAMETER_LOCATION)
1404 .and_then(|v| v.as_str())
1405 {
1406 return Ok(location.to_string());
1407 }
1408
1409 if param_name.starts_with("header_") {
1411 Ok("header".to_string())
1412 } else if param_name.starts_with("cookie_") {
1413 Ok("cookie".to_string())
1414 } else if param_name.starts_with("body_") {
1415 Ok("body".to_string())
1416 } else {
1417 Ok("query".to_string())
1419 }
1420 }
1421
1422 fn validate_parameters(
1424 tool_metadata: &ToolMetadata,
1425 arguments: &Value,
1426 ) -> Result<(), ToolCallValidationError> {
1427 let schema = &tool_metadata.parameters;
1428
1429 let required_params = schema
1431 .get("required")
1432 .and_then(|r| r.as_array())
1433 .map(|arr| {
1434 arr.iter()
1435 .filter_map(|v| v.as_str())
1436 .collect::<std::collections::HashSet<_>>()
1437 })
1438 .unwrap_or_default();
1439
1440 let properties = schema
1441 .get("properties")
1442 .and_then(|p| p.as_object())
1443 .ok_or_else(|| ToolCallValidationError::RequestConstructionError {
1444 reason: "Tool schema missing properties".to_string(),
1445 })?;
1446
1447 let args = arguments.as_object().ok_or_else(|| {
1448 ToolCallValidationError::RequestConstructionError {
1449 reason: "Arguments must be an object".to_string(),
1450 }
1451 })?;
1452
1453 let mut all_errors = Vec::new();
1455
1456 all_errors.extend(Self::check_unknown_parameters(args, properties));
1458
1459 all_errors.extend(Self::check_missing_required(
1461 args,
1462 properties,
1463 &required_params,
1464 ));
1465
1466 all_errors.extend(Self::validate_parameter_values(args, properties));
1468
1469 if !all_errors.is_empty() {
1471 return Err(ToolCallValidationError::InvalidParameters {
1472 violations: all_errors,
1473 });
1474 }
1475
1476 Ok(())
1477 }
1478
1479 fn check_unknown_parameters(
1481 args: &serde_json::Map<String, Value>,
1482 properties: &serde_json::Map<String, Value>,
1483 ) -> Vec<ValidationError> {
1484 let mut errors = Vec::new();
1485
1486 let valid_params: Vec<String> = properties.keys().map(|s| s.to_string()).collect();
1488
1489 for (arg_name, _) in args.iter() {
1491 if !properties.contains_key(arg_name) {
1492 errors.push(ValidationError::invalid_parameter(
1494 arg_name.clone(),
1495 &valid_params,
1496 ));
1497 }
1498 }
1499
1500 errors
1501 }
1502
1503 fn check_missing_required(
1505 args: &serde_json::Map<String, Value>,
1506 properties: &serde_json::Map<String, Value>,
1507 required_params: &HashSet<&str>,
1508 ) -> Vec<ValidationError> {
1509 let mut errors = Vec::new();
1510
1511 for required_param in required_params {
1512 if !args.contains_key(*required_param) {
1513 let param_schema = properties.get(*required_param);
1515
1516 let description = param_schema
1517 .and_then(|schema| schema.get("description"))
1518 .and_then(|d| d.as_str())
1519 .map(|s| s.to_string());
1520
1521 let expected_type = param_schema
1522 .and_then(Self::get_expected_type)
1523 .unwrap_or_else(|| "unknown".to_string());
1524
1525 errors.push(ValidationError::MissingRequiredParameter {
1526 parameter: (*required_param).to_string(),
1527 description,
1528 expected_type,
1529 });
1530 }
1531 }
1532
1533 errors
1534 }
1535
1536 fn validate_parameter_values(
1538 args: &serde_json::Map<String, Value>,
1539 properties: &serde_json::Map<String, Value>,
1540 ) -> Vec<ValidationError> {
1541 let mut errors = Vec::new();
1542
1543 for (param_name, param_value) in args {
1544 if let Some(param_schema) = properties.get(param_name) {
1545 let schema = json!({
1547 "type": "object",
1548 "properties": {
1549 param_name: param_schema
1550 }
1551 });
1552
1553 let compiled = match jsonschema::validator_for(&schema) {
1555 Ok(compiled) => compiled,
1556 Err(e) => {
1557 errors.push(ValidationError::ConstraintViolation {
1558 parameter: param_name.clone(),
1559 message: format!(
1560 "Failed to compile schema for parameter '{param_name}': {e}"
1561 ),
1562 field_path: None,
1563 actual_value: None,
1564 expected_type: None,
1565 constraints: vec![],
1566 });
1567 continue;
1568 }
1569 };
1570
1571 let instance = json!({ param_name: param_value });
1573
1574 let validation_errors: Vec<_> =
1576 compiled.validate(&instance).err().into_iter().collect();
1577
1578 for validation_error in validation_errors {
1579 let error_message = validation_error.to_string();
1581 let instance_path_str = validation_error.instance_path.to_string();
1582 let field_path = if instance_path_str.is_empty() || instance_path_str == "/" {
1583 Some(param_name.clone())
1584 } else {
1585 Some(instance_path_str.trim_start_matches('/').to_string())
1586 };
1587
1588 let constraints = Self::extract_constraints_from_schema(param_schema);
1590
1591 let expected_type = Self::get_expected_type(param_schema);
1593
1594 errors.push(ValidationError::ConstraintViolation {
1595 parameter: param_name.clone(),
1596 message: error_message,
1597 field_path,
1598 actual_value: Some(Box::new(param_value.clone())),
1599 expected_type,
1600 constraints,
1601 });
1602 }
1603 }
1604 }
1605
1606 errors
1607 }
1608
1609 fn extract_constraints_from_schema(schema: &Value) -> Vec<ValidationConstraint> {
1611 let mut constraints = Vec::new();
1612
1613 if let Some(min_value) = schema.get("minimum").and_then(|v| v.as_f64()) {
1615 let exclusive = schema
1616 .get("exclusiveMinimum")
1617 .and_then(|v| v.as_bool())
1618 .unwrap_or(false);
1619 constraints.push(ValidationConstraint::Minimum {
1620 value: min_value,
1621 exclusive,
1622 });
1623 }
1624
1625 if let Some(max_value) = schema.get("maximum").and_then(|v| v.as_f64()) {
1627 let exclusive = schema
1628 .get("exclusiveMaximum")
1629 .and_then(|v| v.as_bool())
1630 .unwrap_or(false);
1631 constraints.push(ValidationConstraint::Maximum {
1632 value: max_value,
1633 exclusive,
1634 });
1635 }
1636
1637 if let Some(min_len) = schema
1639 .get("minLength")
1640 .and_then(|v| v.as_u64())
1641 .map(|v| v as usize)
1642 {
1643 constraints.push(ValidationConstraint::MinLength { value: min_len });
1644 }
1645
1646 if let Some(max_len) = schema
1648 .get("maxLength")
1649 .and_then(|v| v.as_u64())
1650 .map(|v| v as usize)
1651 {
1652 constraints.push(ValidationConstraint::MaxLength { value: max_len });
1653 }
1654
1655 if let Some(pattern) = schema
1657 .get("pattern")
1658 .and_then(|v| v.as_str())
1659 .map(|s| s.to_string())
1660 {
1661 constraints.push(ValidationConstraint::Pattern { pattern });
1662 }
1663
1664 if let Some(enum_values) = schema.get("enum").and_then(|v| v.as_array()).cloned() {
1666 constraints.push(ValidationConstraint::EnumValues {
1667 values: enum_values,
1668 });
1669 }
1670
1671 if let Some(format) = schema
1673 .get("format")
1674 .and_then(|v| v.as_str())
1675 .map(|s| s.to_string())
1676 {
1677 constraints.push(ValidationConstraint::Format { format });
1678 }
1679
1680 if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
1682 constraints.push(ValidationConstraint::MultipleOf { value: multiple_of });
1683 }
1684
1685 if let Some(min_items) = schema
1687 .get("minItems")
1688 .and_then(|v| v.as_u64())
1689 .map(|v| v as usize)
1690 {
1691 constraints.push(ValidationConstraint::MinItems { value: min_items });
1692 }
1693
1694 if let Some(max_items) = schema
1696 .get("maxItems")
1697 .and_then(|v| v.as_u64())
1698 .map(|v| v as usize)
1699 {
1700 constraints.push(ValidationConstraint::MaxItems { value: max_items });
1701 }
1702
1703 if let Some(true) = schema.get("uniqueItems").and_then(|v| v.as_bool()) {
1705 constraints.push(ValidationConstraint::UniqueItems);
1706 }
1707
1708 if let Some(min_props) = schema
1710 .get("minProperties")
1711 .and_then(|v| v.as_u64())
1712 .map(|v| v as usize)
1713 {
1714 constraints.push(ValidationConstraint::MinProperties { value: min_props });
1715 }
1716
1717 if let Some(max_props) = schema
1719 .get("maxProperties")
1720 .and_then(|v| v.as_u64())
1721 .map(|v| v as usize)
1722 {
1723 constraints.push(ValidationConstraint::MaxProperties { value: max_props });
1724 }
1725
1726 if let Some(const_value) = schema.get("const").cloned() {
1728 constraints.push(ValidationConstraint::ConstValue { value: const_value });
1729 }
1730
1731 if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
1733 let properties: Vec<String> = required
1734 .iter()
1735 .filter_map(|v| v.as_str().map(|s| s.to_string()))
1736 .collect();
1737 if !properties.is_empty() {
1738 constraints.push(ValidationConstraint::Required { properties });
1739 }
1740 }
1741
1742 constraints
1743 }
1744
1745 fn get_expected_type(schema: &Value) -> Option<String> {
1747 if let Some(type_value) = schema.get("type") {
1748 if let Some(type_str) = type_value.as_str() {
1749 return Some(type_str.to_string());
1750 } else if let Some(type_array) = type_value.as_array() {
1751 let types: Vec<String> = type_array
1753 .iter()
1754 .filter_map(|v| v.as_str())
1755 .map(|s| s.to_string())
1756 .collect();
1757 if !types.is_empty() {
1758 return Some(types.join(" | "));
1759 }
1760 }
1761 }
1762 None
1763 }
1764
1765 fn wrap_output_schema(
1789 body_schema: &ObjectOrReference<ObjectSchema>,
1790 spec: &Spec,
1791 ) -> Result<Value, Error> {
1792 let mut visited = HashSet::new();
1794 let body_schema_json = match body_schema {
1795 ObjectOrReference::Object(obj_schema) => {
1796 Self::convert_object_schema_to_json_schema(obj_schema, spec, &mut visited)?
1797 }
1798 ObjectOrReference::Ref { ref_path } => {
1799 let resolved = Self::resolve_reference(ref_path, spec, &mut visited)?;
1800 Self::convert_object_schema_to_json_schema(&resolved, spec, &mut visited)?
1801 }
1802 };
1803
1804 let error_schema = create_error_response_schema();
1805
1806 Ok(json!({
1807 "type": "object",
1808 "description": "Unified response structure with success and error variants",
1809 "required": ["status", "body"],
1810 "additionalProperties": false,
1811 "properties": {
1812 "status": {
1813 "type": "integer",
1814 "description": "HTTP status code",
1815 "minimum": 100,
1816 "maximum": 599
1817 },
1818 "body": {
1819 "description": "Response body - either success data or error information",
1820 "oneOf": [
1821 body_schema_json,
1822 error_schema
1823 ]
1824 }
1825 }
1826 }))
1827 }
1828}
1829
1830fn create_error_response_schema() -> Value {
1832 let root_schema = schema_for!(ErrorResponse);
1833 let schema_json = serde_json::to_value(root_schema).expect("Valid error schema");
1834
1835 let definitions = schema_json
1837 .get("$defs")
1838 .or_else(|| schema_json.get("definitions"))
1839 .cloned()
1840 .unwrap_or_else(|| json!({}));
1841
1842 let mut result = schema_json.clone();
1844 if let Some(obj) = result.as_object_mut() {
1845 obj.remove("$schema");
1846 obj.remove("$defs");
1847 obj.remove("definitions");
1848 obj.remove("title");
1849 }
1850
1851 inline_refs(&mut result, &definitions);
1853
1854 result
1855}
1856
1857fn inline_refs(schema: &mut Value, definitions: &Value) {
1859 match schema {
1860 Value::Object(obj) => {
1861 if let Some(ref_value) = obj.get("$ref").cloned()
1863 && let Some(ref_str) = ref_value.as_str()
1864 {
1865 let def_name = ref_str
1867 .strip_prefix("#/$defs/")
1868 .or_else(|| ref_str.strip_prefix("#/definitions/"));
1869
1870 if let Some(name) = def_name
1871 && let Some(definition) = definitions.get(name)
1872 {
1873 *schema = definition.clone();
1875 inline_refs(schema, definitions);
1877 return;
1878 }
1879 }
1880
1881 for (_, value) in obj.iter_mut() {
1883 inline_refs(value, definitions);
1884 }
1885 }
1886 Value::Array(arr) => {
1887 for item in arr.iter_mut() {
1889 inline_refs(item, definitions);
1890 }
1891 }
1892 _ => {} }
1894}
1895
1896#[derive(Debug, Clone)]
1898pub struct QueryParameter {
1899 pub value: Value,
1900 pub explode: bool,
1901}
1902
1903impl QueryParameter {
1904 pub fn new(value: Value, explode: bool) -> Self {
1905 Self { value, explode }
1906 }
1907}
1908
1909#[derive(Debug, Clone)]
1911pub struct ExtractedParameters {
1912 pub path: HashMap<String, Value>,
1913 pub query: HashMap<String, QueryParameter>,
1914 pub headers: HashMap<String, Value>,
1915 pub cookies: HashMap<String, Value>,
1916 pub body: HashMap<String, Value>,
1917 pub config: RequestConfig,
1918}
1919
1920#[derive(Debug, Clone)]
1922pub struct RequestConfig {
1923 pub timeout_seconds: u32,
1924 pub content_type: String,
1925}
1926
1927impl Default for RequestConfig {
1928 fn default() -> Self {
1929 Self {
1930 timeout_seconds: 30,
1931 content_type: mime::APPLICATION_JSON.to_string(),
1932 }
1933 }
1934}
1935
1936#[cfg(test)]
1937mod tests {
1938 use super::*;
1939
1940 use insta::assert_json_snapshot;
1941 use oas3::spec::{
1942 BooleanSchema, Components, MediaType, ObjectOrReference, ObjectSchema, Operation,
1943 Parameter, ParameterIn, RequestBody, Schema, SchemaType, SchemaTypeSet, Spec,
1944 };
1945 use rmcp::model::Tool;
1946 use serde_json::{Value, json};
1947 use std::collections::BTreeMap;
1948
1949 fn create_test_spec() -> Spec {
1951 Spec {
1952 openapi: "3.0.0".to_string(),
1953 info: oas3::spec::Info {
1954 title: "Test API".to_string(),
1955 version: "1.0.0".to_string(),
1956 summary: None,
1957 description: Some("Test API for unit tests".to_string()),
1958 terms_of_service: None,
1959 contact: None,
1960 license: None,
1961 extensions: Default::default(),
1962 },
1963 components: Some(Components {
1964 schemas: BTreeMap::new(),
1965 responses: BTreeMap::new(),
1966 parameters: BTreeMap::new(),
1967 examples: BTreeMap::new(),
1968 request_bodies: BTreeMap::new(),
1969 headers: BTreeMap::new(),
1970 security_schemes: BTreeMap::new(),
1971 links: BTreeMap::new(),
1972 callbacks: BTreeMap::new(),
1973 path_items: BTreeMap::new(),
1974 extensions: Default::default(),
1975 }),
1976 servers: vec![],
1977 paths: None,
1978 external_docs: None,
1979 tags: vec![],
1980 security: vec![],
1981 webhooks: BTreeMap::new(),
1982 extensions: Default::default(),
1983 }
1984 }
1985
1986 fn validate_tool_against_mcp_schema(metadata: &ToolMetadata) {
1987 let schema_content = std::fs::read_to_string("schema/2025-06-18/schema.json")
1988 .expect("Failed to read MCP schema file");
1989 let full_schema: Value =
1990 serde_json::from_str(&schema_content).expect("Failed to parse MCP schema JSON");
1991
1992 let tool_schema = json!({
1994 "$schema": "http://json-schema.org/draft-07/schema#",
1995 "definitions": full_schema.get("definitions"),
1996 "$ref": "#/definitions/Tool"
1997 });
1998
1999 let validator =
2000 jsonschema::validator_for(&tool_schema).expect("Failed to compile MCP Tool schema");
2001
2002 let tool = Tool::from(metadata);
2004
2005 let mcp_tool_json = serde_json::to_value(&tool).expect("Failed to serialize Tool to JSON");
2007
2008 let errors: Vec<String> = validator
2010 .iter_errors(&mcp_tool_json)
2011 .map(|e| e.to_string())
2012 .collect();
2013
2014 if !errors.is_empty() {
2015 panic!("Generated tool failed MCP schema validation: {errors:?}");
2016 }
2017 }
2018
2019 #[test]
2020 fn test_error_schema_structure() {
2021 let error_schema = create_error_response_schema();
2022
2023 assert!(error_schema.get("$schema").is_none());
2025 assert!(error_schema.get("definitions").is_none());
2026
2027 assert_json_snapshot!(error_schema);
2029 }
2030
2031 #[test]
2032 fn test_petstore_get_pet_by_id() {
2033 use oas3::spec::Response;
2034
2035 let mut operation = Operation {
2036 operation_id: Some("getPetById".to_string()),
2037 summary: Some("Find pet by ID".to_string()),
2038 description: Some("Returns a single pet".to_string()),
2039 tags: vec![],
2040 external_docs: None,
2041 parameters: vec![],
2042 request_body: None,
2043 responses: Default::default(),
2044 callbacks: Default::default(),
2045 deprecated: Some(false),
2046 security: vec![],
2047 servers: vec![],
2048 extensions: Default::default(),
2049 };
2050
2051 let param = Parameter {
2053 name: "petId".to_string(),
2054 location: ParameterIn::Path,
2055 description: Some("ID of pet to return".to_string()),
2056 required: Some(true),
2057 deprecated: Some(false),
2058 allow_empty_value: Some(false),
2059 style: None,
2060 explode: None,
2061 allow_reserved: Some(false),
2062 schema: Some(ObjectOrReference::Object(ObjectSchema {
2063 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2064 minimum: Some(serde_json::Number::from(1_i64)),
2065 format: Some("int64".to_string()),
2066 ..Default::default()
2067 })),
2068 example: None,
2069 examples: Default::default(),
2070 content: None,
2071 extensions: Default::default(),
2072 };
2073
2074 operation.parameters.push(ObjectOrReference::Object(param));
2075
2076 let mut responses = BTreeMap::new();
2078 let mut content = BTreeMap::new();
2079 content.insert(
2080 "application/json".to_string(),
2081 MediaType {
2082 schema: Some(ObjectOrReference::Object(ObjectSchema {
2083 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2084 properties: {
2085 let mut props = BTreeMap::new();
2086 props.insert(
2087 "id".to_string(),
2088 ObjectOrReference::Object(ObjectSchema {
2089 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2090 format: Some("int64".to_string()),
2091 ..Default::default()
2092 }),
2093 );
2094 props.insert(
2095 "name".to_string(),
2096 ObjectOrReference::Object(ObjectSchema {
2097 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2098 ..Default::default()
2099 }),
2100 );
2101 props.insert(
2102 "status".to_string(),
2103 ObjectOrReference::Object(ObjectSchema {
2104 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2105 ..Default::default()
2106 }),
2107 );
2108 props
2109 },
2110 required: vec!["id".to_string(), "name".to_string()],
2111 ..Default::default()
2112 })),
2113 examples: None,
2114 encoding: Default::default(),
2115 },
2116 );
2117
2118 responses.insert(
2119 "200".to_string(),
2120 ObjectOrReference::Object(Response {
2121 description: Some("successful operation".to_string()),
2122 headers: Default::default(),
2123 content,
2124 links: Default::default(),
2125 extensions: Default::default(),
2126 }),
2127 );
2128 operation.responses = Some(responses);
2129
2130 let spec = create_test_spec();
2131 let metadata = ToolGenerator::generate_tool_metadata(
2132 &operation,
2133 "get".to_string(),
2134 "/pet/{petId}".to_string(),
2135 &spec,
2136 )
2137 .unwrap();
2138
2139 assert_eq!(metadata.name, "getPetById");
2140 assert_eq!(metadata.method, "get");
2141 assert_eq!(metadata.path, "/pet/{petId}");
2142 assert!(metadata.description.contains("Find pet by ID"));
2143
2144 assert!(metadata.output_schema.is_some());
2146 let output_schema = metadata.output_schema.as_ref().unwrap();
2147
2148 insta::assert_json_snapshot!("test_petstore_get_pet_by_id_output_schema", output_schema);
2150
2151 validate_tool_against_mcp_schema(&metadata);
2153 }
2154
2155 #[test]
2156 fn test_convert_prefix_items_to_draft07_mixed_types() {
2157 let prefix_items = vec![
2160 ObjectOrReference::Object(ObjectSchema {
2161 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2162 format: Some("int32".to_string()),
2163 ..Default::default()
2164 }),
2165 ObjectOrReference::Object(ObjectSchema {
2166 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2167 ..Default::default()
2168 }),
2169 ];
2170
2171 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
2173
2174 let mut result = serde_json::Map::new();
2175 let spec = create_test_spec();
2176 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
2177 .unwrap();
2178
2179 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_mixed_types", result);
2181 }
2182
2183 #[test]
2184 fn test_convert_prefix_items_to_draft07_uniform_types() {
2185 let prefix_items = vec![
2187 ObjectOrReference::Object(ObjectSchema {
2188 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2189 ..Default::default()
2190 }),
2191 ObjectOrReference::Object(ObjectSchema {
2192 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2193 ..Default::default()
2194 }),
2195 ];
2196
2197 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
2199
2200 let mut result = serde_json::Map::new();
2201 let spec = create_test_spec();
2202 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
2203 .unwrap();
2204
2205 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_uniform_types", result);
2207 }
2208
2209 #[test]
2210 fn test_array_with_prefix_items_integration() {
2211 let param = Parameter {
2213 name: "coordinates".to_string(),
2214 location: ParameterIn::Query,
2215 description: Some("X,Y coordinates as tuple".to_string()),
2216 required: Some(true),
2217 deprecated: Some(false),
2218 allow_empty_value: Some(false),
2219 style: None,
2220 explode: None,
2221 allow_reserved: Some(false),
2222 schema: Some(ObjectOrReference::Object(ObjectSchema {
2223 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
2224 prefix_items: vec![
2225 ObjectOrReference::Object(ObjectSchema {
2226 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
2227 format: Some("double".to_string()),
2228 ..Default::default()
2229 }),
2230 ObjectOrReference::Object(ObjectSchema {
2231 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
2232 format: Some("double".to_string()),
2233 ..Default::default()
2234 }),
2235 ],
2236 items: Some(Box::new(Schema::Boolean(BooleanSchema(false)))),
2237 ..Default::default()
2238 })),
2239 example: None,
2240 examples: Default::default(),
2241 content: None,
2242 extensions: Default::default(),
2243 };
2244
2245 let spec = create_test_spec();
2246 let (result, _annotations) =
2247 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec).unwrap();
2248
2249 insta::assert_json_snapshot!("test_array_with_prefix_items_integration", result);
2251 }
2252
2253 #[test]
2254 fn test_array_with_regular_items_schema() {
2255 let param = Parameter {
2257 name: "tags".to_string(),
2258 location: ParameterIn::Query,
2259 description: Some("List of tags".to_string()),
2260 required: Some(false),
2261 deprecated: Some(false),
2262 allow_empty_value: Some(false),
2263 style: None,
2264 explode: None,
2265 allow_reserved: Some(false),
2266 schema: Some(ObjectOrReference::Object(ObjectSchema {
2267 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
2268 items: Some(Box::new(Schema::Object(Box::new(
2269 ObjectOrReference::Object(ObjectSchema {
2270 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2271 min_length: Some(1),
2272 max_length: Some(50),
2273 ..Default::default()
2274 }),
2275 )))),
2276 ..Default::default()
2277 })),
2278 example: None,
2279 examples: Default::default(),
2280 content: None,
2281 extensions: Default::default(),
2282 };
2283
2284 let spec = create_test_spec();
2285 let (result, _annotations) =
2286 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec).unwrap();
2287
2288 insta::assert_json_snapshot!("test_array_with_regular_items_schema", result);
2290 }
2291
2292 #[test]
2293 fn test_request_body_object_schema() {
2294 let operation = Operation {
2296 operation_id: Some("createPet".to_string()),
2297 summary: Some("Create a new pet".to_string()),
2298 description: Some("Creates a new pet in the store".to_string()),
2299 tags: vec![],
2300 external_docs: None,
2301 parameters: vec![],
2302 request_body: Some(ObjectOrReference::Object(RequestBody {
2303 description: Some("Pet object that needs to be added to the store".to_string()),
2304 content: {
2305 let mut content = BTreeMap::new();
2306 content.insert(
2307 "application/json".to_string(),
2308 MediaType {
2309 schema: Some(ObjectOrReference::Object(ObjectSchema {
2310 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2311 ..Default::default()
2312 })),
2313 examples: None,
2314 encoding: Default::default(),
2315 },
2316 );
2317 content
2318 },
2319 required: Some(true),
2320 })),
2321 responses: Default::default(),
2322 callbacks: Default::default(),
2323 deprecated: Some(false),
2324 security: vec![],
2325 servers: vec![],
2326 extensions: Default::default(),
2327 };
2328
2329 let spec = create_test_spec();
2330 let metadata = ToolGenerator::generate_tool_metadata(
2331 &operation,
2332 "post".to_string(),
2333 "/pets".to_string(),
2334 &spec,
2335 )
2336 .unwrap();
2337
2338 let properties = metadata
2340 .parameters
2341 .get("properties")
2342 .unwrap()
2343 .as_object()
2344 .unwrap();
2345 assert!(properties.contains_key("request_body"));
2346
2347 let required = metadata
2349 .parameters
2350 .get("required")
2351 .unwrap()
2352 .as_array()
2353 .unwrap();
2354 assert!(required.contains(&json!("request_body")));
2355
2356 let request_body_schema = properties.get("request_body").unwrap();
2358 insta::assert_json_snapshot!("test_request_body_object_schema", request_body_schema);
2359
2360 validate_tool_against_mcp_schema(&metadata);
2362 }
2363
2364 #[test]
2365 fn test_request_body_array_schema() {
2366 let operation = Operation {
2368 operation_id: Some("createPets".to_string()),
2369 summary: Some("Create multiple pets".to_string()),
2370 description: None,
2371 tags: vec![],
2372 external_docs: None,
2373 parameters: vec![],
2374 request_body: Some(ObjectOrReference::Object(RequestBody {
2375 description: Some("Array of pet objects".to_string()),
2376 content: {
2377 let mut content = BTreeMap::new();
2378 content.insert(
2379 "application/json".to_string(),
2380 MediaType {
2381 schema: Some(ObjectOrReference::Object(ObjectSchema {
2382 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
2383 items: Some(Box::new(Schema::Object(Box::new(
2384 ObjectOrReference::Object(ObjectSchema {
2385 schema_type: Some(SchemaTypeSet::Single(
2386 SchemaType::Object,
2387 )),
2388 ..Default::default()
2389 }),
2390 )))),
2391 ..Default::default()
2392 })),
2393 examples: None,
2394 encoding: Default::default(),
2395 },
2396 );
2397 content
2398 },
2399 required: Some(false),
2400 })),
2401 responses: Default::default(),
2402 callbacks: Default::default(),
2403 deprecated: Some(false),
2404 security: vec![],
2405 servers: vec![],
2406 extensions: Default::default(),
2407 };
2408
2409 let spec = create_test_spec();
2410 let metadata = ToolGenerator::generate_tool_metadata(
2411 &operation,
2412 "post".to_string(),
2413 "/pets/batch".to_string(),
2414 &spec,
2415 )
2416 .unwrap();
2417
2418 let properties = metadata
2420 .parameters
2421 .get("properties")
2422 .unwrap()
2423 .as_object()
2424 .unwrap();
2425 assert!(properties.contains_key("request_body"));
2426
2427 let required = metadata
2429 .parameters
2430 .get("required")
2431 .unwrap()
2432 .as_array()
2433 .unwrap();
2434 assert!(!required.contains(&json!("request_body")));
2435
2436 let request_body_schema = properties.get("request_body").unwrap();
2438 insta::assert_json_snapshot!("test_request_body_array_schema", request_body_schema);
2439
2440 validate_tool_against_mcp_schema(&metadata);
2442 }
2443
2444 #[test]
2445 fn test_request_body_string_schema() {
2446 let operation = Operation {
2448 operation_id: Some("updatePetName".to_string()),
2449 summary: Some("Update pet name".to_string()),
2450 description: None,
2451 tags: vec![],
2452 external_docs: None,
2453 parameters: vec![],
2454 request_body: Some(ObjectOrReference::Object(RequestBody {
2455 description: None,
2456 content: {
2457 let mut content = BTreeMap::new();
2458 content.insert(
2459 "text/plain".to_string(),
2460 MediaType {
2461 schema: Some(ObjectOrReference::Object(ObjectSchema {
2462 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2463 min_length: Some(1),
2464 max_length: Some(100),
2465 ..Default::default()
2466 })),
2467 examples: None,
2468 encoding: Default::default(),
2469 },
2470 );
2471 content
2472 },
2473 required: Some(true),
2474 })),
2475 responses: Default::default(),
2476 callbacks: Default::default(),
2477 deprecated: Some(false),
2478 security: vec![],
2479 servers: vec![],
2480 extensions: Default::default(),
2481 };
2482
2483 let spec = create_test_spec();
2484 let metadata = ToolGenerator::generate_tool_metadata(
2485 &operation,
2486 "put".to_string(),
2487 "/pets/{petId}/name".to_string(),
2488 &spec,
2489 )
2490 .unwrap();
2491
2492 let properties = metadata
2494 .parameters
2495 .get("properties")
2496 .unwrap()
2497 .as_object()
2498 .unwrap();
2499 let request_body_schema = properties.get("request_body").unwrap();
2500 insta::assert_json_snapshot!("test_request_body_string_schema", request_body_schema);
2501
2502 validate_tool_against_mcp_schema(&metadata);
2504 }
2505
2506 #[test]
2507 fn test_request_body_ref_schema() {
2508 let operation = Operation {
2510 operation_id: Some("updatePet".to_string()),
2511 summary: Some("Update existing pet".to_string()),
2512 description: None,
2513 tags: vec![],
2514 external_docs: None,
2515 parameters: vec![],
2516 request_body: Some(ObjectOrReference::Ref {
2517 ref_path: "#/components/requestBodies/PetBody".to_string(),
2518 }),
2519 responses: Default::default(),
2520 callbacks: Default::default(),
2521 deprecated: Some(false),
2522 security: vec![],
2523 servers: vec![],
2524 extensions: Default::default(),
2525 };
2526
2527 let spec = create_test_spec();
2528 let metadata = ToolGenerator::generate_tool_metadata(
2529 &operation,
2530 "put".to_string(),
2531 "/pets/{petId}".to_string(),
2532 &spec,
2533 )
2534 .unwrap();
2535
2536 let properties = metadata
2538 .parameters
2539 .get("properties")
2540 .unwrap()
2541 .as_object()
2542 .unwrap();
2543 let request_body_schema = properties.get("request_body").unwrap();
2544 insta::assert_json_snapshot!("test_request_body_ref_schema", request_body_schema);
2545
2546 validate_tool_against_mcp_schema(&metadata);
2548 }
2549
2550 #[test]
2551 fn test_no_request_body_for_get() {
2552 let operation = Operation {
2554 operation_id: Some("listPets".to_string()),
2555 summary: Some("List all pets".to_string()),
2556 description: None,
2557 tags: vec![],
2558 external_docs: None,
2559 parameters: vec![],
2560 request_body: None,
2561 responses: Default::default(),
2562 callbacks: Default::default(),
2563 deprecated: Some(false),
2564 security: vec![],
2565 servers: vec![],
2566 extensions: Default::default(),
2567 };
2568
2569 let spec = create_test_spec();
2570 let metadata = ToolGenerator::generate_tool_metadata(
2571 &operation,
2572 "get".to_string(),
2573 "/pets".to_string(),
2574 &spec,
2575 )
2576 .unwrap();
2577
2578 let properties = metadata
2580 .parameters
2581 .get("properties")
2582 .unwrap()
2583 .as_object()
2584 .unwrap();
2585 assert!(!properties.contains_key("request_body"));
2586
2587 validate_tool_against_mcp_schema(&metadata);
2589 }
2590
2591 #[test]
2592 fn test_request_body_simple_object_with_properties() {
2593 let operation = Operation {
2595 operation_id: Some("updatePetStatus".to_string()),
2596 summary: Some("Update pet status".to_string()),
2597 description: None,
2598 tags: vec![],
2599 external_docs: None,
2600 parameters: vec![],
2601 request_body: Some(ObjectOrReference::Object(RequestBody {
2602 description: Some("Pet status update".to_string()),
2603 content: {
2604 let mut content = BTreeMap::new();
2605 content.insert(
2606 "application/json".to_string(),
2607 MediaType {
2608 schema: Some(ObjectOrReference::Object(ObjectSchema {
2609 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2610 properties: {
2611 let mut props = BTreeMap::new();
2612 props.insert(
2613 "status".to_string(),
2614 ObjectOrReference::Object(ObjectSchema {
2615 schema_type: Some(SchemaTypeSet::Single(
2616 SchemaType::String,
2617 )),
2618 ..Default::default()
2619 }),
2620 );
2621 props.insert(
2622 "reason".to_string(),
2623 ObjectOrReference::Object(ObjectSchema {
2624 schema_type: Some(SchemaTypeSet::Single(
2625 SchemaType::String,
2626 )),
2627 ..Default::default()
2628 }),
2629 );
2630 props
2631 },
2632 required: vec!["status".to_string()],
2633 ..Default::default()
2634 })),
2635 examples: None,
2636 encoding: Default::default(),
2637 },
2638 );
2639 content
2640 },
2641 required: Some(false),
2642 })),
2643 responses: Default::default(),
2644 callbacks: Default::default(),
2645 deprecated: Some(false),
2646 security: vec![],
2647 servers: vec![],
2648 extensions: Default::default(),
2649 };
2650
2651 let spec = create_test_spec();
2652 let metadata = ToolGenerator::generate_tool_metadata(
2653 &operation,
2654 "patch".to_string(),
2655 "/pets/{petId}/status".to_string(),
2656 &spec,
2657 )
2658 .unwrap();
2659
2660 let properties = metadata
2662 .parameters
2663 .get("properties")
2664 .unwrap()
2665 .as_object()
2666 .unwrap();
2667 let request_body_schema = properties.get("request_body").unwrap();
2668 insta::assert_json_snapshot!(
2669 "test_request_body_simple_object_with_properties",
2670 request_body_schema
2671 );
2672
2673 let required = metadata
2675 .parameters
2676 .get("required")
2677 .unwrap()
2678 .as_array()
2679 .unwrap();
2680 assert!(!required.contains(&json!("request_body")));
2681
2682 validate_tool_against_mcp_schema(&metadata);
2684 }
2685
2686 #[test]
2687 fn test_request_body_with_nested_properties() {
2688 let operation = Operation {
2690 operation_id: Some("createUser".to_string()),
2691 summary: Some("Create a new user".to_string()),
2692 description: None,
2693 tags: vec![],
2694 external_docs: None,
2695 parameters: vec![],
2696 request_body: Some(ObjectOrReference::Object(RequestBody {
2697 description: Some("User creation data".to_string()),
2698 content: {
2699 let mut content = BTreeMap::new();
2700 content.insert(
2701 "application/json".to_string(),
2702 MediaType {
2703 schema: Some(ObjectOrReference::Object(ObjectSchema {
2704 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2705 properties: {
2706 let mut props = BTreeMap::new();
2707 props.insert(
2708 "name".to_string(),
2709 ObjectOrReference::Object(ObjectSchema {
2710 schema_type: Some(SchemaTypeSet::Single(
2711 SchemaType::String,
2712 )),
2713 ..Default::default()
2714 }),
2715 );
2716 props.insert(
2717 "age".to_string(),
2718 ObjectOrReference::Object(ObjectSchema {
2719 schema_type: Some(SchemaTypeSet::Single(
2720 SchemaType::Integer,
2721 )),
2722 minimum: Some(serde_json::Number::from(0)),
2723 maximum: Some(serde_json::Number::from(150)),
2724 ..Default::default()
2725 }),
2726 );
2727 props
2728 },
2729 required: vec!["name".to_string()],
2730 ..Default::default()
2731 })),
2732 examples: None,
2733 encoding: Default::default(),
2734 },
2735 );
2736 content
2737 },
2738 required: Some(true),
2739 })),
2740 responses: Default::default(),
2741 callbacks: Default::default(),
2742 deprecated: Some(false),
2743 security: vec![],
2744 servers: vec![],
2745 extensions: Default::default(),
2746 };
2747
2748 let spec = create_test_spec();
2749 let metadata = ToolGenerator::generate_tool_metadata(
2750 &operation,
2751 "post".to_string(),
2752 "/users".to_string(),
2753 &spec,
2754 )
2755 .unwrap();
2756
2757 let properties = metadata
2759 .parameters
2760 .get("properties")
2761 .unwrap()
2762 .as_object()
2763 .unwrap();
2764 let request_body_schema = properties.get("request_body").unwrap();
2765 insta::assert_json_snapshot!(
2766 "test_request_body_with_nested_properties",
2767 request_body_schema
2768 );
2769
2770 validate_tool_against_mcp_schema(&metadata);
2772 }
2773
2774 #[test]
2775 fn test_operation_without_responses_has_no_output_schema() {
2776 let operation = Operation {
2777 operation_id: Some("testOperation".to_string()),
2778 summary: Some("Test operation".to_string()),
2779 description: None,
2780 tags: vec![],
2781 external_docs: None,
2782 parameters: vec![],
2783 request_body: None,
2784 responses: None,
2785 callbacks: Default::default(),
2786 deprecated: Some(false),
2787 security: vec![],
2788 servers: vec![],
2789 extensions: Default::default(),
2790 };
2791
2792 let spec = create_test_spec();
2793 let metadata = ToolGenerator::generate_tool_metadata(
2794 &operation,
2795 "get".to_string(),
2796 "/test".to_string(),
2797 &spec,
2798 )
2799 .unwrap();
2800
2801 assert!(metadata.output_schema.is_none());
2803
2804 validate_tool_against_mcp_schema(&metadata);
2806 }
2807
2808 #[test]
2809 fn test_extract_output_schema_with_200_response() {
2810 use oas3::spec::Response;
2811
2812 let mut responses = BTreeMap::new();
2814 let mut content = BTreeMap::new();
2815 content.insert(
2816 "application/json".to_string(),
2817 MediaType {
2818 schema: Some(ObjectOrReference::Object(ObjectSchema {
2819 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2820 properties: {
2821 let mut props = BTreeMap::new();
2822 props.insert(
2823 "id".to_string(),
2824 ObjectOrReference::Object(ObjectSchema {
2825 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2826 ..Default::default()
2827 }),
2828 );
2829 props.insert(
2830 "name".to_string(),
2831 ObjectOrReference::Object(ObjectSchema {
2832 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2833 ..Default::default()
2834 }),
2835 );
2836 props
2837 },
2838 required: vec!["id".to_string(), "name".to_string()],
2839 ..Default::default()
2840 })),
2841 examples: None,
2842 encoding: Default::default(),
2843 },
2844 );
2845
2846 responses.insert(
2847 "200".to_string(),
2848 ObjectOrReference::Object(Response {
2849 description: Some("Successful response".to_string()),
2850 headers: Default::default(),
2851 content,
2852 links: Default::default(),
2853 extensions: Default::default(),
2854 }),
2855 );
2856
2857 let spec = create_test_spec();
2858 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
2859
2860 insta::assert_json_snapshot!(result);
2862 }
2863
2864 #[test]
2865 fn test_extract_output_schema_with_201_response() {
2866 use oas3::spec::Response;
2867
2868 let mut responses = BTreeMap::new();
2870 let mut content = BTreeMap::new();
2871 content.insert(
2872 "application/json".to_string(),
2873 MediaType {
2874 schema: Some(ObjectOrReference::Object(ObjectSchema {
2875 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2876 properties: {
2877 let mut props = BTreeMap::new();
2878 props.insert(
2879 "created".to_string(),
2880 ObjectOrReference::Object(ObjectSchema {
2881 schema_type: Some(SchemaTypeSet::Single(SchemaType::Boolean)),
2882 ..Default::default()
2883 }),
2884 );
2885 props
2886 },
2887 ..Default::default()
2888 })),
2889 examples: None,
2890 encoding: Default::default(),
2891 },
2892 );
2893
2894 responses.insert(
2895 "201".to_string(),
2896 ObjectOrReference::Object(Response {
2897 description: Some("Created".to_string()),
2898 headers: Default::default(),
2899 content,
2900 links: Default::default(),
2901 extensions: Default::default(),
2902 }),
2903 );
2904
2905 let spec = create_test_spec();
2906 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
2907
2908 insta::assert_json_snapshot!(result);
2910 }
2911
2912 #[test]
2913 fn test_extract_output_schema_with_2xx_response() {
2914 use oas3::spec::Response;
2915
2916 let mut responses = BTreeMap::new();
2918 let mut content = BTreeMap::new();
2919 content.insert(
2920 "application/json".to_string(),
2921 MediaType {
2922 schema: Some(ObjectOrReference::Object(ObjectSchema {
2923 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
2924 items: Some(Box::new(Schema::Object(Box::new(
2925 ObjectOrReference::Object(ObjectSchema {
2926 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2927 ..Default::default()
2928 }),
2929 )))),
2930 ..Default::default()
2931 })),
2932 examples: None,
2933 encoding: Default::default(),
2934 },
2935 );
2936
2937 responses.insert(
2938 "2XX".to_string(),
2939 ObjectOrReference::Object(Response {
2940 description: Some("Success".to_string()),
2941 headers: Default::default(),
2942 content,
2943 links: Default::default(),
2944 extensions: Default::default(),
2945 }),
2946 );
2947
2948 let spec = create_test_spec();
2949 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
2950
2951 insta::assert_json_snapshot!(result);
2953 }
2954
2955 #[test]
2956 fn test_extract_output_schema_no_responses() {
2957 let spec = create_test_spec();
2958 let result = ToolGenerator::extract_output_schema(&None, &spec).unwrap();
2959
2960 insta::assert_json_snapshot!(result);
2962 }
2963
2964 #[test]
2965 fn test_extract_output_schema_only_error_responses() {
2966 use oas3::spec::Response;
2967
2968 let mut responses = BTreeMap::new();
2970 responses.insert(
2971 "404".to_string(),
2972 ObjectOrReference::Object(Response {
2973 description: Some("Not found".to_string()),
2974 headers: Default::default(),
2975 content: Default::default(),
2976 links: Default::default(),
2977 extensions: Default::default(),
2978 }),
2979 );
2980 responses.insert(
2981 "500".to_string(),
2982 ObjectOrReference::Object(Response {
2983 description: Some("Server error".to_string()),
2984 headers: Default::default(),
2985 content: Default::default(),
2986 links: Default::default(),
2987 extensions: Default::default(),
2988 }),
2989 );
2990
2991 let spec = create_test_spec();
2992 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
2993
2994 insta::assert_json_snapshot!(result);
2996 }
2997
2998 #[test]
2999 fn test_extract_output_schema_with_ref() {
3000 use oas3::spec::Response;
3001
3002 let mut spec = create_test_spec();
3004 let mut schemas = BTreeMap::new();
3005 schemas.insert(
3006 "Pet".to_string(),
3007 ObjectOrReference::Object(ObjectSchema {
3008 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3009 properties: {
3010 let mut props = BTreeMap::new();
3011 props.insert(
3012 "name".to_string(),
3013 ObjectOrReference::Object(ObjectSchema {
3014 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3015 ..Default::default()
3016 }),
3017 );
3018 props
3019 },
3020 ..Default::default()
3021 }),
3022 );
3023 spec.components.as_mut().unwrap().schemas = schemas;
3024
3025 let mut responses = BTreeMap::new();
3027 let mut content = BTreeMap::new();
3028 content.insert(
3029 "application/json".to_string(),
3030 MediaType {
3031 schema: Some(ObjectOrReference::Ref {
3032 ref_path: "#/components/schemas/Pet".to_string(),
3033 }),
3034 examples: None,
3035 encoding: Default::default(),
3036 },
3037 );
3038
3039 responses.insert(
3040 "200".to_string(),
3041 ObjectOrReference::Object(Response {
3042 description: Some("Success".to_string()),
3043 headers: Default::default(),
3044 content,
3045 links: Default::default(),
3046 extensions: Default::default(),
3047 }),
3048 );
3049
3050 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
3051
3052 insta::assert_json_snapshot!(result);
3054 }
3055
3056 #[test]
3057 fn test_generate_tool_metadata_includes_output_schema() {
3058 use oas3::spec::Response;
3059
3060 let mut operation = Operation {
3061 operation_id: Some("getPet".to_string()),
3062 summary: Some("Get a pet".to_string()),
3063 description: None,
3064 tags: vec![],
3065 external_docs: None,
3066 parameters: vec![],
3067 request_body: None,
3068 responses: Default::default(),
3069 callbacks: Default::default(),
3070 deprecated: Some(false),
3071 security: vec![],
3072 servers: vec![],
3073 extensions: Default::default(),
3074 };
3075
3076 let mut responses = BTreeMap::new();
3078 let mut content = BTreeMap::new();
3079 content.insert(
3080 "application/json".to_string(),
3081 MediaType {
3082 schema: Some(ObjectOrReference::Object(ObjectSchema {
3083 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3084 properties: {
3085 let mut props = BTreeMap::new();
3086 props.insert(
3087 "id".to_string(),
3088 ObjectOrReference::Object(ObjectSchema {
3089 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3090 ..Default::default()
3091 }),
3092 );
3093 props
3094 },
3095 ..Default::default()
3096 })),
3097 examples: None,
3098 encoding: Default::default(),
3099 },
3100 );
3101
3102 responses.insert(
3103 "200".to_string(),
3104 ObjectOrReference::Object(Response {
3105 description: Some("Success".to_string()),
3106 headers: Default::default(),
3107 content,
3108 links: Default::default(),
3109 extensions: Default::default(),
3110 }),
3111 );
3112 operation.responses = Some(responses);
3113
3114 let spec = create_test_spec();
3115 let metadata = ToolGenerator::generate_tool_metadata(
3116 &operation,
3117 "get".to_string(),
3118 "/pets/{id}".to_string(),
3119 &spec,
3120 )
3121 .unwrap();
3122
3123 assert!(metadata.output_schema.is_some());
3125 let output_schema = metadata.output_schema.as_ref().unwrap();
3126
3127 insta::assert_json_snapshot!(
3129 "test_generate_tool_metadata_includes_output_schema",
3130 output_schema
3131 );
3132
3133 validate_tool_against_mcp_schema(&metadata);
3135 }
3136
3137 #[test]
3138 fn test_sanitize_property_name() {
3139 assert_eq!(sanitize_property_name("user name"), "user_name");
3141 assert_eq!(
3142 sanitize_property_name("first name last name"),
3143 "first_name_last_name"
3144 );
3145
3146 assert_eq!(sanitize_property_name("user(admin)"), "user_admin");
3148 assert_eq!(sanitize_property_name("user[admin]"), "user_admin");
3149 assert_eq!(sanitize_property_name("price($)"), "price");
3150 assert_eq!(sanitize_property_name("email@address"), "email_address");
3151 assert_eq!(sanitize_property_name("item#1"), "item_1");
3152 assert_eq!(sanitize_property_name("a/b/c"), "a_b_c");
3153
3154 assert_eq!(sanitize_property_name("user_name"), "user_name");
3156 assert_eq!(sanitize_property_name("userName123"), "userName123");
3157 assert_eq!(sanitize_property_name("user.name"), "user.name");
3158 assert_eq!(sanitize_property_name("user-name"), "user-name");
3159
3160 assert_eq!(sanitize_property_name("123name"), "param_123name");
3162 assert_eq!(sanitize_property_name("1st_place"), "param_1st_place");
3163
3164 assert_eq!(sanitize_property_name(""), "param_");
3166
3167 let long_name = "a".repeat(100);
3169 assert_eq!(sanitize_property_name(&long_name).len(), 64);
3170
3171 assert_eq!(sanitize_property_name("!@#$%^&*()"), "param_");
3174 }
3175
3176 #[test]
3177 fn test_sanitize_property_name_trailing_underscores() {
3178 assert_eq!(sanitize_property_name("page[size]"), "page_size");
3180 assert_eq!(sanitize_property_name("user[id]"), "user_id");
3181 assert_eq!(sanitize_property_name("field[]"), "field");
3182
3183 assert_eq!(sanitize_property_name("field___"), "field");
3185 assert_eq!(sanitize_property_name("test[[["), "test");
3186 }
3187
3188 #[test]
3189 fn test_sanitize_property_name_consecutive_underscores() {
3190 assert_eq!(sanitize_property_name("user__name"), "user_name");
3192 assert_eq!(sanitize_property_name("first___last"), "first_last");
3193 assert_eq!(sanitize_property_name("a____b____c"), "a_b_c");
3194
3195 assert_eq!(sanitize_property_name("user[[name]]"), "user_name");
3197 assert_eq!(sanitize_property_name("field@#$value"), "field_value");
3198 }
3199
3200 #[test]
3201 fn test_sanitize_property_name_edge_cases() {
3202 assert_eq!(sanitize_property_name("_private"), "_private");
3204 assert_eq!(sanitize_property_name("__dunder"), "_dunder");
3205
3206 assert_eq!(sanitize_property_name("[[["), "param_");
3208 assert_eq!(sanitize_property_name("@@@"), "param_");
3209
3210 assert_eq!(sanitize_property_name(""), "param_");
3212
3213 assert_eq!(sanitize_property_name("_field[size]"), "_field_size");
3215 assert_eq!(sanitize_property_name("__test__"), "_test");
3216 }
3217
3218 #[test]
3219 fn test_sanitize_property_name_complex_cases() {
3220 assert_eq!(sanitize_property_name("page[size]"), "page_size");
3222 assert_eq!(sanitize_property_name("filter[status]"), "filter_status");
3223 assert_eq!(
3224 sanitize_property_name("sort[-created_at]"),
3225 "sort_-created_at"
3226 );
3227 assert_eq!(
3228 sanitize_property_name("include[author.posts]"),
3229 "include_author.posts"
3230 );
3231
3232 let long_name = "very_long_field_name_with_special[characters]_that_needs_truncation_____";
3234 let expected = "very_long_field_name_with_special_characters_that_needs_truncat";
3235 assert_eq!(sanitize_property_name(long_name), expected);
3236 }
3237
3238 #[test]
3239 fn test_property_sanitization_with_annotations() {
3240 let spec = create_test_spec();
3241 let mut visited = HashSet::new();
3242
3243 let obj_schema = ObjectSchema {
3245 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3246 properties: {
3247 let mut props = BTreeMap::new();
3248 props.insert(
3250 "user name".to_string(),
3251 ObjectOrReference::Object(ObjectSchema {
3252 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3253 ..Default::default()
3254 }),
3255 );
3256 props.insert(
3258 "price($)".to_string(),
3259 ObjectOrReference::Object(ObjectSchema {
3260 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
3261 ..Default::default()
3262 }),
3263 );
3264 props.insert(
3266 "validName".to_string(),
3267 ObjectOrReference::Object(ObjectSchema {
3268 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3269 ..Default::default()
3270 }),
3271 );
3272 props
3273 },
3274 ..Default::default()
3275 };
3276
3277 let result =
3278 ToolGenerator::convert_object_schema_to_json_schema(&obj_schema, &spec, &mut visited)
3279 .unwrap();
3280
3281 insta::assert_json_snapshot!("test_property_sanitization_with_annotations", result);
3283 }
3284
3285 #[test]
3286 fn test_parameter_sanitization_and_extraction() {
3287 let spec = create_test_spec();
3288
3289 let operation = Operation {
3291 operation_id: Some("testOp".to_string()),
3292 parameters: vec![
3293 ObjectOrReference::Object(Parameter {
3295 name: "user(id)".to_string(),
3296 location: ParameterIn::Path,
3297 description: Some("User ID".to_string()),
3298 required: Some(true),
3299 deprecated: Some(false),
3300 allow_empty_value: Some(false),
3301 style: None,
3302 explode: None,
3303 allow_reserved: Some(false),
3304 schema: Some(ObjectOrReference::Object(ObjectSchema {
3305 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3306 ..Default::default()
3307 })),
3308 example: None,
3309 examples: Default::default(),
3310 content: None,
3311 extensions: Default::default(),
3312 }),
3313 ObjectOrReference::Object(Parameter {
3315 name: "page size".to_string(),
3316 location: ParameterIn::Query,
3317 description: Some("Page size".to_string()),
3318 required: Some(false),
3319 deprecated: Some(false),
3320 allow_empty_value: Some(false),
3321 style: None,
3322 explode: None,
3323 allow_reserved: Some(false),
3324 schema: Some(ObjectOrReference::Object(ObjectSchema {
3325 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3326 ..Default::default()
3327 })),
3328 example: None,
3329 examples: Default::default(),
3330 content: None,
3331 extensions: Default::default(),
3332 }),
3333 ObjectOrReference::Object(Parameter {
3335 name: "auth-token!".to_string(),
3336 location: ParameterIn::Header,
3337 description: Some("Auth token".to_string()),
3338 required: Some(false),
3339 deprecated: Some(false),
3340 allow_empty_value: Some(false),
3341 style: None,
3342 explode: None,
3343 allow_reserved: Some(false),
3344 schema: Some(ObjectOrReference::Object(ObjectSchema {
3345 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3346 ..Default::default()
3347 })),
3348 example: None,
3349 examples: Default::default(),
3350 content: None,
3351 extensions: Default::default(),
3352 }),
3353 ],
3354 ..Default::default()
3355 };
3356
3357 let tool_metadata = ToolGenerator::generate_tool_metadata(
3358 &operation,
3359 "get".to_string(),
3360 "/users/{user(id)}".to_string(),
3361 &spec,
3362 )
3363 .unwrap();
3364
3365 let properties = tool_metadata
3367 .parameters
3368 .get("properties")
3369 .unwrap()
3370 .as_object()
3371 .unwrap();
3372
3373 assert!(properties.contains_key("user_id"));
3374 assert!(properties.contains_key("page_size"));
3375 assert!(properties.contains_key("header_auth-token"));
3376
3377 let required = tool_metadata
3379 .parameters
3380 .get("required")
3381 .unwrap()
3382 .as_array()
3383 .unwrap();
3384 assert!(required.contains(&json!("user_id")));
3385
3386 let arguments = json!({
3388 "user_id": "123",
3389 "page_size": 10,
3390 "header_auth-token": "secret"
3391 });
3392
3393 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
3394
3395 assert_eq!(extracted.path.get("user(id)"), Some(&json!("123")));
3397
3398 assert_eq!(
3400 extracted.query.get("page size").map(|q| &q.value),
3401 Some(&json!(10))
3402 );
3403
3404 assert_eq!(extracted.headers.get("auth-token!"), Some(&json!("secret")));
3406 }
3407
3408 #[test]
3409 fn test_check_unknown_parameters() {
3410 let mut properties = serde_json::Map::new();
3412 properties.insert("page_size".to_string(), json!({"type": "integer"}));
3413 properties.insert("user_id".to_string(), json!({"type": "string"}));
3414
3415 let mut args = serde_json::Map::new();
3416 args.insert("page_sixe".to_string(), json!(10)); let result = ToolGenerator::check_unknown_parameters(&args, &properties);
3419 assert!(!result.is_empty());
3420 assert_eq!(result.len(), 1);
3421
3422 match &result[0] {
3423 ValidationError::InvalidParameter {
3424 parameter,
3425 suggestions,
3426 valid_parameters,
3427 } => {
3428 assert_eq!(parameter, "page_sixe");
3429 assert_eq!(suggestions, &vec!["page_size".to_string()]);
3430 assert_eq!(
3431 valid_parameters,
3432 &vec!["page_size".to_string(), "user_id".to_string()]
3433 );
3434 }
3435 _ => panic!("Expected InvalidParameter variant"),
3436 }
3437 }
3438
3439 #[test]
3440 fn test_check_unknown_parameters_no_suggestions() {
3441 let mut properties = serde_json::Map::new();
3443 properties.insert("limit".to_string(), json!({"type": "integer"}));
3444 properties.insert("offset".to_string(), json!({"type": "integer"}));
3445
3446 let mut args = serde_json::Map::new();
3447 args.insert("xyz123".to_string(), json!("value"));
3448
3449 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
3450 assert!(!result.is_empty());
3451 assert_eq!(result.len(), 1);
3452
3453 match &result[0] {
3454 ValidationError::InvalidParameter {
3455 parameter,
3456 suggestions,
3457 valid_parameters,
3458 } => {
3459 assert_eq!(parameter, "xyz123");
3460 assert!(suggestions.is_empty());
3461 assert!(valid_parameters.contains(&"limit".to_string()));
3462 assert!(valid_parameters.contains(&"offset".to_string()));
3463 }
3464 _ => panic!("Expected InvalidParameter variant"),
3465 }
3466 }
3467
3468 #[test]
3469 fn test_check_unknown_parameters_multiple_suggestions() {
3470 let mut properties = serde_json::Map::new();
3472 properties.insert("user_id".to_string(), json!({"type": "string"}));
3473 properties.insert("user_iid".to_string(), json!({"type": "string"}));
3474 properties.insert("user_name".to_string(), json!({"type": "string"}));
3475
3476 let mut args = serde_json::Map::new();
3477 args.insert("usr_id".to_string(), json!("123"));
3478
3479 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
3480 assert!(!result.is_empty());
3481 assert_eq!(result.len(), 1);
3482
3483 match &result[0] {
3484 ValidationError::InvalidParameter {
3485 parameter,
3486 suggestions,
3487 valid_parameters,
3488 } => {
3489 assert_eq!(parameter, "usr_id");
3490 assert!(!suggestions.is_empty());
3491 assert!(suggestions.contains(&"user_id".to_string()));
3492 assert_eq!(valid_parameters.len(), 3);
3493 }
3494 _ => panic!("Expected InvalidParameter variant"),
3495 }
3496 }
3497
3498 #[test]
3499 fn test_check_unknown_parameters_valid() {
3500 let mut properties = serde_json::Map::new();
3502 properties.insert("name".to_string(), json!({"type": "string"}));
3503 properties.insert("email".to_string(), json!({"type": "string"}));
3504
3505 let mut args = serde_json::Map::new();
3506 args.insert("name".to_string(), json!("John"));
3507 args.insert("email".to_string(), json!("john@example.com"));
3508
3509 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
3510 assert!(result.is_empty());
3511 }
3512
3513 #[test]
3514 fn test_check_unknown_parameters_empty() {
3515 let properties = serde_json::Map::new();
3517
3518 let mut args = serde_json::Map::new();
3519 args.insert("any_param".to_string(), json!("value"));
3520
3521 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
3522 assert!(!result.is_empty());
3523 assert_eq!(result.len(), 1);
3524
3525 match &result[0] {
3526 ValidationError::InvalidParameter {
3527 parameter,
3528 suggestions,
3529 valid_parameters,
3530 } => {
3531 assert_eq!(parameter, "any_param");
3532 assert!(suggestions.is_empty());
3533 assert!(valid_parameters.is_empty());
3534 }
3535 _ => panic!("Expected InvalidParameter variant"),
3536 }
3537 }
3538
3539 #[test]
3540 fn test_check_unknown_parameters_gltf_pagination() {
3541 let mut properties = serde_json::Map::new();
3543 properties.insert(
3544 "page_number".to_string(),
3545 json!({
3546 "type": "integer",
3547 "x-original-name": "page[number]"
3548 }),
3549 );
3550 properties.insert(
3551 "page_size".to_string(),
3552 json!({
3553 "type": "integer",
3554 "x-original-name": "page[size]"
3555 }),
3556 );
3557
3558 let mut args = serde_json::Map::new();
3560 args.insert("page".to_string(), json!(1));
3561 args.insert("per_page".to_string(), json!(10));
3562
3563 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
3564 assert_eq!(result.len(), 2, "Should have 2 unknown parameters");
3565
3566 let page_error = result
3568 .iter()
3569 .find(|e| {
3570 if let ValidationError::InvalidParameter { parameter, .. } = e {
3571 parameter == "page"
3572 } else {
3573 false
3574 }
3575 })
3576 .expect("Should have error for 'page'");
3577
3578 let per_page_error = result
3579 .iter()
3580 .find(|e| {
3581 if let ValidationError::InvalidParameter { parameter, .. } = e {
3582 parameter == "per_page"
3583 } else {
3584 false
3585 }
3586 })
3587 .expect("Should have error for 'per_page'");
3588
3589 match page_error {
3591 ValidationError::InvalidParameter {
3592 suggestions,
3593 valid_parameters,
3594 ..
3595 } => {
3596 assert!(
3597 suggestions.contains(&"page_number".to_string()),
3598 "Should suggest 'page_number' for 'page'"
3599 );
3600 assert_eq!(valid_parameters.len(), 2);
3601 assert!(valid_parameters.contains(&"page_number".to_string()));
3602 assert!(valid_parameters.contains(&"page_size".to_string()));
3603 }
3604 _ => panic!("Expected InvalidParameter"),
3605 }
3606
3607 match per_page_error {
3609 ValidationError::InvalidParameter {
3610 parameter,
3611 suggestions,
3612 valid_parameters,
3613 ..
3614 } => {
3615 assert_eq!(parameter, "per_page");
3616 assert_eq!(valid_parameters.len(), 2);
3617 if !suggestions.is_empty() {
3620 assert!(suggestions.contains(&"page_size".to_string()));
3621 }
3622 }
3623 _ => panic!("Expected InvalidParameter"),
3624 }
3625 }
3626
3627 #[test]
3628 fn test_validate_parameters_with_invalid_params() {
3629 let tool_metadata = ToolMetadata {
3631 name: "listItems".to_string(),
3632 title: None,
3633 description: "List items".to_string(),
3634 parameters: json!({
3635 "type": "object",
3636 "properties": {
3637 "page_number": {
3638 "type": "integer",
3639 "x-original-name": "page[number]"
3640 },
3641 "page_size": {
3642 "type": "integer",
3643 "x-original-name": "page[size]"
3644 }
3645 },
3646 "required": []
3647 }),
3648 output_schema: None,
3649 method: "GET".to_string(),
3650 path: "/items".to_string(),
3651 };
3652
3653 let arguments = json!({
3655 "page": 1,
3656 "per_page": 10
3657 });
3658
3659 let result = ToolGenerator::validate_parameters(&tool_metadata, &arguments);
3660 assert!(
3661 result.is_err(),
3662 "Should fail validation with unknown parameters"
3663 );
3664
3665 let error = result.unwrap_err();
3666 match error {
3667 ToolCallValidationError::InvalidParameters { violations } => {
3668 assert_eq!(violations.len(), 2, "Should have 2 validation errors");
3669
3670 let has_page_error = violations.iter().any(|v| {
3672 if let ValidationError::InvalidParameter { parameter, .. } = v {
3673 parameter == "page"
3674 } else {
3675 false
3676 }
3677 });
3678
3679 let has_per_page_error = violations.iter().any(|v| {
3680 if let ValidationError::InvalidParameter { parameter, .. } = v {
3681 parameter == "per_page"
3682 } else {
3683 false
3684 }
3685 });
3686
3687 assert!(has_page_error, "Should have error for 'page' parameter");
3688 assert!(
3689 has_per_page_error,
3690 "Should have error for 'per_page' parameter"
3691 );
3692 }
3693 _ => panic!("Expected InvalidParameters"),
3694 }
3695 }
3696
3697 #[test]
3698 fn test_cookie_parameter_sanitization() {
3699 let spec = create_test_spec();
3700
3701 let operation = Operation {
3702 operation_id: Some("testCookie".to_string()),
3703 parameters: vec![ObjectOrReference::Object(Parameter {
3704 name: "session[id]".to_string(),
3705 location: ParameterIn::Cookie,
3706 description: Some("Session ID".to_string()),
3707 required: Some(false),
3708 deprecated: Some(false),
3709 allow_empty_value: Some(false),
3710 style: None,
3711 explode: None,
3712 allow_reserved: Some(false),
3713 schema: Some(ObjectOrReference::Object(ObjectSchema {
3714 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3715 ..Default::default()
3716 })),
3717 example: None,
3718 examples: Default::default(),
3719 content: None,
3720 extensions: Default::default(),
3721 })],
3722 ..Default::default()
3723 };
3724
3725 let tool_metadata = ToolGenerator::generate_tool_metadata(
3726 &operation,
3727 "get".to_string(),
3728 "/data".to_string(),
3729 &spec,
3730 )
3731 .unwrap();
3732
3733 let properties = tool_metadata
3734 .parameters
3735 .get("properties")
3736 .unwrap()
3737 .as_object()
3738 .unwrap();
3739
3740 assert!(properties.contains_key("cookie_session_id"));
3742
3743 let arguments = json!({
3745 "cookie_session_id": "abc123"
3746 });
3747
3748 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
3749
3750 assert_eq!(extracted.cookies.get("session[id]"), Some(&json!("abc123")));
3752 }
3753
3754 #[test]
3755 fn test_parameter_description_with_examples() {
3756 let spec = create_test_spec();
3757
3758 let param_with_example = Parameter {
3760 name: "status".to_string(),
3761 location: ParameterIn::Query,
3762 description: Some("Filter by status".to_string()),
3763 required: Some(false),
3764 deprecated: Some(false),
3765 allow_empty_value: Some(false),
3766 style: None,
3767 explode: None,
3768 allow_reserved: Some(false),
3769 schema: Some(ObjectOrReference::Object(ObjectSchema {
3770 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3771 ..Default::default()
3772 })),
3773 example: Some(json!("active")),
3774 examples: Default::default(),
3775 content: None,
3776 extensions: Default::default(),
3777 };
3778
3779 let (schema, _) =
3780 ToolGenerator::convert_parameter_schema(¶m_with_example, ParameterIn::Query, &spec)
3781 .unwrap();
3782 let description = schema.get("description").unwrap().as_str().unwrap();
3783 assert_eq!(description, "Filter by status. Example: `\"active\"`");
3784
3785 let mut examples_map = std::collections::BTreeMap::new();
3787 examples_map.insert(
3788 "example1".to_string(),
3789 ObjectOrReference::Object(oas3::spec::Example {
3790 value: Some(json!("pending")),
3791 ..Default::default()
3792 }),
3793 );
3794 examples_map.insert(
3795 "example2".to_string(),
3796 ObjectOrReference::Object(oas3::spec::Example {
3797 value: Some(json!("completed")),
3798 ..Default::default()
3799 }),
3800 );
3801
3802 let param_with_examples = Parameter {
3803 name: "status".to_string(),
3804 location: ParameterIn::Query,
3805 description: Some("Filter by status".to_string()),
3806 required: Some(false),
3807 deprecated: Some(false),
3808 allow_empty_value: Some(false),
3809 style: None,
3810 explode: None,
3811 allow_reserved: Some(false),
3812 schema: Some(ObjectOrReference::Object(ObjectSchema {
3813 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3814 ..Default::default()
3815 })),
3816 example: None,
3817 examples: examples_map,
3818 content: None,
3819 extensions: Default::default(),
3820 };
3821
3822 let (schema, _) = ToolGenerator::convert_parameter_schema(
3823 ¶m_with_examples,
3824 ParameterIn::Query,
3825 &spec,
3826 )
3827 .unwrap();
3828 let description = schema.get("description").unwrap().as_str().unwrap();
3829 assert!(description.starts_with("Filter by status. Examples:\n"));
3830 assert!(description.contains("`\"pending\"`"));
3831 assert!(description.contains("`\"completed\"`"));
3832
3833 let param_no_desc = Parameter {
3835 name: "limit".to_string(),
3836 location: ParameterIn::Query,
3837 description: None,
3838 required: Some(false),
3839 deprecated: Some(false),
3840 allow_empty_value: Some(false),
3841 style: None,
3842 explode: None,
3843 allow_reserved: Some(false),
3844 schema: Some(ObjectOrReference::Object(ObjectSchema {
3845 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3846 ..Default::default()
3847 })),
3848 example: Some(json!(100)),
3849 examples: Default::default(),
3850 content: None,
3851 extensions: Default::default(),
3852 };
3853
3854 let (schema, _) =
3855 ToolGenerator::convert_parameter_schema(¶m_no_desc, ParameterIn::Query, &spec)
3856 .unwrap();
3857 let description = schema.get("description").unwrap().as_str().unwrap();
3858 assert_eq!(description, "limit parameter. Example: `100`");
3859 }
3860
3861 #[test]
3862 fn test_format_examples_for_description() {
3863 let examples = vec![json!("active")];
3865 let result = ToolGenerator::format_examples_for_description(&examples);
3866 assert_eq!(result, Some("Example: `\"active\"`".to_string()));
3867
3868 let examples = vec![json!(42)];
3870 let result = ToolGenerator::format_examples_for_description(&examples);
3871 assert_eq!(result, Some("Example: `42`".to_string()));
3872
3873 let examples = vec![json!(true)];
3875 let result = ToolGenerator::format_examples_for_description(&examples);
3876 assert_eq!(result, Some("Example: `true`".to_string()));
3877
3878 let examples = vec![json!("active"), json!("pending"), json!("completed")];
3880 let result = ToolGenerator::format_examples_for_description(&examples);
3881 assert_eq!(
3882 result,
3883 Some("Examples:\n- `\"active\"`\n- `\"pending\"`\n- `\"completed\"`".to_string())
3884 );
3885
3886 let examples = vec![json!(["a", "b", "c"])];
3888 let result = ToolGenerator::format_examples_for_description(&examples);
3889 assert_eq!(result, Some("Example: `[\"a\",\"b\",\"c\"]`".to_string()));
3890
3891 let examples = vec![json!({"key": "value"})];
3893 let result = ToolGenerator::format_examples_for_description(&examples);
3894 assert_eq!(result, Some("Example: `{\"key\":\"value\"}`".to_string()));
3895
3896 let examples = vec![];
3898 let result = ToolGenerator::format_examples_for_description(&examples);
3899 assert_eq!(result, None);
3900
3901 let examples = vec![json!(null)];
3903 let result = ToolGenerator::format_examples_for_description(&examples);
3904 assert_eq!(result, Some("Example: `null`".to_string()));
3905
3906 let examples = vec![json!("text"), json!(123), json!(true)];
3908 let result = ToolGenerator::format_examples_for_description(&examples);
3909 assert_eq!(
3910 result,
3911 Some("Examples:\n- `\"text\"`\n- `123`\n- `true`".to_string())
3912 );
3913
3914 let examples = vec![json!(["a", "b", "c", "d", "e", "f"])];
3916 let result = ToolGenerator::format_examples_for_description(&examples);
3917 assert_eq!(
3918 result,
3919 Some("Example: `[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\"]`".to_string())
3920 );
3921
3922 let examples = vec![json!([1, 2])];
3924 let result = ToolGenerator::format_examples_for_description(&examples);
3925 assert_eq!(result, Some("Example: `[1,2]`".to_string()));
3926
3927 let examples = vec![json!({"user": {"name": "John", "age": 30}})];
3929 let result = ToolGenerator::format_examples_for_description(&examples);
3930 assert_eq!(
3931 result,
3932 Some("Example: `{\"user\":{\"age\":30,\"name\":\"John\"}}`".to_string())
3933 );
3934
3935 let examples = vec![json!("a"), json!("b"), json!("c"), json!("d"), json!("e")];
3937 let result = ToolGenerator::format_examples_for_description(&examples);
3938 assert_eq!(
3939 result,
3940 Some("Examples:\n- `\"a\"`\n- `\"b\"`\n- `\"c\"`\n- `\"d\"`\n- `\"e\"`".to_string())
3941 );
3942
3943 let examples = vec![json!(3.5)];
3945 let result = ToolGenerator::format_examples_for_description(&examples);
3946 assert_eq!(result, Some("Example: `3.5`".to_string()));
3947
3948 let examples = vec![json!(-42)];
3950 let result = ToolGenerator::format_examples_for_description(&examples);
3951 assert_eq!(result, Some("Example: `-42`".to_string()));
3952
3953 let examples = vec![json!(false)];
3955 let result = ToolGenerator::format_examples_for_description(&examples);
3956 assert_eq!(result, Some("Example: `false`".to_string()));
3957
3958 let examples = vec![json!("hello \"world\"")];
3960 let result = ToolGenerator::format_examples_for_description(&examples);
3961 assert_eq!(result, Some(r#"Example: `"hello \"world\""`"#.to_string()));
3963
3964 let examples = vec![json!("")];
3966 let result = ToolGenerator::format_examples_for_description(&examples);
3967 assert_eq!(result, Some("Example: `\"\"`".to_string()));
3968
3969 let examples = vec![json!([])];
3971 let result = ToolGenerator::format_examples_for_description(&examples);
3972 assert_eq!(result, Some("Example: `[]`".to_string()));
3973
3974 let examples = vec![json!({})];
3976 let result = ToolGenerator::format_examples_for_description(&examples);
3977 assert_eq!(result, Some("Example: `{}`".to_string()));
3978 }
3979}