1use jsonschema::error::{TypeKind, ValidationErrorKind};
108use schemars::schema_for;
109use serde::{Serialize, Serializer};
110use serde_json::{Value, json};
111use std::collections::{BTreeMap, HashMap, HashSet};
112
113use crate::HttpClient;
114use crate::error::{
115 Error, ErrorResponse, ToolCallValidationError, ValidationConstraint, ValidationError,
116};
117use crate::tool::ToolMetadata;
118use oas3::spec::{
119 BooleanSchema, ObjectOrReference, ObjectSchema, Operation, Parameter, ParameterIn,
120 ParameterStyle, RequestBody, Response, Schema, SchemaType, SchemaTypeSet, Spec,
121};
122use tracing::{trace, warn};
123
124const X_LOCATION: &str = "x-location";
126const X_PARAMETER_LOCATION: &str = "x-parameter-location";
127const X_PARAMETER_REQUIRED: &str = "x-parameter-required";
128const X_CONTENT_TYPE: &str = "x-content-type";
129const X_ORIGINAL_NAME: &str = "x-original-name";
130const X_PARAMETER_EXPLODE: &str = "x-parameter-explode";
131const X_FILE_FIELDS: &str = "x-file-fields";
132
133#[derive(Debug, Clone, Copy, PartialEq)]
135pub enum Location {
136 Parameter(ParameterIn),
138 Body,
140}
141
142impl Serialize for Location {
143 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
144 where
145 S: Serializer,
146 {
147 let str_value = match self {
148 Location::Parameter(param_in) => match param_in {
149 ParameterIn::Query => "query",
150 ParameterIn::Header => "header",
151 ParameterIn::Path => "path",
152 ParameterIn::Cookie => "cookie",
153 },
154 Location::Body => "body",
155 };
156 serializer.serialize_str(str_value)
157 }
158}
159
160#[derive(Debug, Clone, PartialEq)]
162pub enum Annotation {
163 Location(Location),
165 Required(bool),
167 ContentType(String),
169 OriginalName(String),
171 Explode(bool),
173 FileFields(Vec<String>),
175}
176
177#[derive(Debug, Clone, Default)]
179pub struct Annotations {
180 annotations: Vec<Annotation>,
181}
182
183impl Annotations {
184 pub fn new() -> Self {
186 Self {
187 annotations: Vec::new(),
188 }
189 }
190
191 pub fn with_location(mut self, location: Location) -> Self {
193 self.annotations.push(Annotation::Location(location));
194 self
195 }
196
197 pub fn with_required(mut self, required: bool) -> Self {
199 self.annotations.push(Annotation::Required(required));
200 self
201 }
202
203 pub fn with_content_type(mut self, content_type: String) -> Self {
205 self.annotations.push(Annotation::ContentType(content_type));
206 self
207 }
208
209 pub fn with_original_name(mut self, original_name: String) -> Self {
211 self.annotations
212 .push(Annotation::OriginalName(original_name));
213 self
214 }
215
216 pub fn with_explode(mut self, explode: bool) -> Self {
218 self.annotations.push(Annotation::Explode(explode));
219 self
220 }
221
222 pub fn with_file_fields(mut self, file_fields: Vec<String>) -> Self {
224 self.annotations.push(Annotation::FileFields(file_fields));
225 self
226 }
227}
228
229impl Serialize for Annotations {
230 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
231 where
232 S: Serializer,
233 {
234 use serde::ser::SerializeMap;
235
236 let mut map = serializer.serialize_map(Some(self.annotations.len()))?;
237
238 for annotation in &self.annotations {
239 match annotation {
240 Annotation::Location(location) => {
241 let key = match location {
243 Location::Parameter(param_in) => match param_in {
244 ParameterIn::Header | ParameterIn::Cookie => X_LOCATION,
245 _ => X_PARAMETER_LOCATION,
246 },
247 Location::Body => X_LOCATION,
248 };
249 map.serialize_entry(key, &location)?;
250
251 if let Location::Parameter(_) = location {
253 map.serialize_entry(X_PARAMETER_LOCATION, &location)?;
254 }
255 }
256 Annotation::Required(required) => {
257 map.serialize_entry(X_PARAMETER_REQUIRED, required)?;
258 }
259 Annotation::ContentType(content_type) => {
260 map.serialize_entry(X_CONTENT_TYPE, content_type)?;
261 }
262 Annotation::OriginalName(original_name) => {
263 map.serialize_entry(X_ORIGINAL_NAME, original_name)?;
264 }
265 Annotation::Explode(explode) => {
266 map.serialize_entry(X_PARAMETER_EXPLODE, explode)?;
267 }
268 Annotation::FileFields(file_fields) => {
269 map.serialize_entry(X_FILE_FIELDS, file_fields)?;
270 }
271 }
272 }
273
274 map.end()
275 }
276}
277
278fn sanitize_property_name(name: &str) -> String {
287 let sanitized = name
289 .chars()
290 .map(|c| match c {
291 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '.' | '-' => c,
292 _ => '_',
293 })
294 .take(64)
295 .collect::<String>();
296
297 let mut collapsed = String::with_capacity(sanitized.len());
299 let mut prev_was_underscore = false;
300
301 for ch in sanitized.chars() {
302 if ch == '_' {
303 if !prev_was_underscore {
304 collapsed.push(ch);
305 }
306 prev_was_underscore = true;
307 } else {
308 collapsed.push(ch);
309 prev_was_underscore = false;
310 }
311 }
312
313 let trimmed = collapsed.trim_end_matches('_');
315
316 if trimmed.is_empty() || trimmed.chars().next().unwrap_or('0').is_numeric() {
318 format!("param_{trimmed}")
319 } else {
320 trimmed.to_string()
321 }
322}
323
324#[derive(Debug, Clone, Default)]
388pub struct ReferenceMetadata {
389 pub summary: Option<String>,
396
397 pub description: Option<String>,
404}
405
406impl ReferenceMetadata {
407 pub fn new(summary: Option<String>, description: Option<String>) -> Self {
409 Self {
410 summary,
411 description,
412 }
413 }
414
415 pub fn is_empty(&self) -> bool {
417 self.summary.is_none() && self.description.is_none()
418 }
419
420 pub fn best_description(&self) -> Option<&str> {
470 self.description.as_deref().or(self.summary.as_deref())
471 }
472
473 pub fn summary(&self) -> Option<&str> {
520 self.summary.as_deref()
521 }
522
523 pub fn merge_with_description(
609 &self,
610 existing_desc: Option<&str>,
611 prepend_summary: bool,
612 ) -> Option<String> {
613 match (self.best_description(), self.summary(), existing_desc) {
614 (Some(ref_desc), _, _) => Some(ref_desc.to_string()),
616
617 (None, Some(ref_summary), Some(existing)) if prepend_summary => {
619 if ref_summary != existing {
620 Some(format!("{}\n\n{}", ref_summary, existing))
621 } else {
622 Some(existing.to_string())
623 }
624 }
625 (None, Some(ref_summary), _) => Some(ref_summary.to_string()),
626
627 (None, None, Some(existing)) => Some(existing.to_string()),
629
630 (None, None, None) => None,
632 }
633 }
634
635 pub fn enhance_parameter_description(
721 &self,
722 param_name: &str,
723 existing_desc: Option<&str>,
724 ) -> Option<String> {
725 match (self.best_description(), self.summary(), existing_desc) {
726 (Some(ref_desc), _, _) => Some(format!("{}: {}", param_name, ref_desc)),
728
729 (None, Some(ref_summary), _) => Some(format!("{}: {}", param_name, ref_summary)),
731
732 (None, None, Some(existing)) => Some(existing.to_string()),
734
735 (None, None, None) => Some(format!("{} parameter", param_name)),
737 }
738 }
739}
740
741pub struct ToolGenerator;
743
744impl ToolGenerator {
745 pub fn generate_tool_metadata(
751 operation: &Operation,
752 method: String,
753 path: String,
754 spec: &Spec,
755 skip_tool_description: bool,
756 skip_parameter_descriptions: bool,
757 ) -> Result<ToolMetadata, Error> {
758 let name = operation.operation_id.clone().unwrap_or_else(|| {
759 format!(
760 "{}_{}",
761 method,
762 path.replace('/', "_").replace(['{', '}'], "")
763 )
764 });
765
766 let (parameters, parameter_mappings) = Self::generate_parameter_schema(
768 &operation.parameters,
769 &method,
770 &operation.request_body,
771 spec,
772 skip_parameter_descriptions,
773 )?;
774
775 let description =
777 (!skip_tool_description).then(|| Self::build_description(operation, &method, &path));
778
779 let output_schema = Self::extract_output_schema(&operation.responses, spec)?;
781
782 Ok(ToolMetadata {
783 name,
784 title: operation.summary.clone(),
785 description,
786 parameters,
787 output_schema,
788 method,
789 path,
790 security: None, parameter_mappings,
792 })
793 }
794
795 pub fn generate_openapi_tools(
801 tools_metadata: Vec<ToolMetadata>,
802 base_url: Option<url::Url>,
803 default_headers: Option<reqwest::header::HeaderMap>,
804 ) -> Result<Vec<crate::tool::Tool>, Error> {
805 let mut openapi_tools = Vec::with_capacity(tools_metadata.len());
806
807 let mut http_client = HttpClient::new();
808
809 if let Some(url) = base_url {
810 http_client = http_client.with_base_url(url)?;
811 }
812
813 if let Some(headers) = default_headers {
814 http_client = http_client.with_default_headers(headers);
815 }
816
817 for metadata in tools_metadata {
818 let tool = crate::tool::Tool::new(metadata, http_client.clone())?;
819 openapi_tools.push(tool);
820 }
821
822 Ok(openapi_tools)
823 }
824
825 fn build_description(operation: &Operation, method: &str, path: &str) -> String {
827 match (&operation.summary, &operation.description) {
828 (Some(summary), Some(desc)) => {
829 format!(
830 "{}\n\n{}\n\nEndpoint: {} {}",
831 summary,
832 desc,
833 method.to_uppercase(),
834 path
835 )
836 }
837 (Some(summary), None) => {
838 format!(
839 "{}\n\nEndpoint: {} {}",
840 summary,
841 method.to_uppercase(),
842 path
843 )
844 }
845 (None, Some(desc)) => {
846 format!("{}\n\nEndpoint: {} {}", desc, method.to_uppercase(), path)
847 }
848 (None, None) => {
849 format!("API endpoint: {} {}", method.to_uppercase(), path)
850 }
851 }
852 }
853
854 fn extract_output_schema(
858 responses: &Option<BTreeMap<String, ObjectOrReference<Response>>>,
859 spec: &Spec,
860 ) -> Result<Option<Value>, Error> {
861 let responses = match responses {
862 Some(r) => r,
863 None => return Ok(None),
864 };
865 let priority_codes = vec![
867 "200", "201", "202", "203", "204", "2XX", "default", ];
875
876 for status_code in priority_codes {
877 if let Some(response_or_ref) = responses.get(status_code) {
878 let response = match response_or_ref {
880 ObjectOrReference::Object(response) => response,
881 ObjectOrReference::Ref {
882 ref_path,
883 summary,
884 description,
885 } => {
886 let ref_metadata =
889 ReferenceMetadata::new(summary.clone(), description.clone());
890
891 if let Some(ref_desc) = ref_metadata.best_description() {
892 let response_schema = json!({
894 "type": "object",
895 "description": "Unified response structure with success and error variants",
896 "properties": {
897 "status_code": {
898 "type": "integer",
899 "description": "HTTP status code"
900 },
901 "body": {
902 "type": "object",
903 "description": ref_desc,
904 "additionalProperties": true
905 }
906 },
907 "required": ["status_code", "body"]
908 });
909
910 trace!(
911 reference_path = %ref_path,
912 reference_description = %ref_desc,
913 "Created response schema using reference metadata"
914 );
915
916 return Ok(Some(response_schema));
917 }
918
919 continue;
921 }
922 };
923
924 if status_code == "204" {
926 continue;
927 }
928
929 if !response.content.is_empty() {
931 let content = &response.content;
932 let json_media_types = vec![
934 "application/json",
935 "application/ld+json",
936 "application/vnd.api+json",
937 ];
938
939 for media_type_str in json_media_types {
940 if let Some(media_type) = content.get(media_type_str)
941 && let Some(schema_or_ref) = &media_type.schema
942 {
943 let wrapped_schema = Self::wrap_output_schema(schema_or_ref, spec)?;
945 return Ok(Some(wrapped_schema));
946 }
947 }
948
949 for media_type in content.values() {
951 if let Some(schema_or_ref) = &media_type.schema {
952 let wrapped_schema = Self::wrap_output_schema(schema_or_ref, spec)?;
954 return Ok(Some(wrapped_schema));
955 }
956 }
957 }
958 }
959 }
960
961 Ok(None)
963 }
964
965 fn convert_schema_to_json_schema(
975 schema: &Schema,
976 spec: &Spec,
977 visited: &mut HashSet<String>,
978 ) -> Result<Value, Error> {
979 match schema {
980 Schema::Object(obj_schema_or_ref) => match obj_schema_or_ref.as_ref() {
981 ObjectOrReference::Object(obj_schema) => {
982 Self::convert_object_schema_to_json_schema(obj_schema, spec, visited)
983 }
984 ObjectOrReference::Ref { ref_path, .. } => {
985 let resolved = Self::resolve_reference(ref_path, spec, visited)?;
986 let result =
987 Self::convert_object_schema_to_json_schema(&resolved, spec, visited);
988 visited.remove(ref_path);
992 result
993 }
994 },
995 Schema::Boolean(bool_schema) => {
996 if bool_schema.0 {
998 Ok(json!({})) } else {
1000 Ok(json!({"not": {}})) }
1002 }
1003 }
1004 }
1005
1006 fn convert_object_schema_to_json_schema(
1016 obj_schema: &ObjectSchema,
1017 spec: &Spec,
1018 visited: &mut HashSet<String>,
1019 ) -> Result<Value, Error> {
1020 let mut schema_obj = serde_json::Map::new();
1021
1022 if let Some(schema_type) = &obj_schema.schema_type {
1024 match schema_type {
1025 SchemaTypeSet::Single(single_type) => {
1026 schema_obj.insert(
1027 "type".to_string(),
1028 json!(Self::schema_type_to_string(single_type)),
1029 );
1030 }
1031 SchemaTypeSet::Multiple(type_set) => {
1032 let types: Vec<String> =
1033 type_set.iter().map(Self::schema_type_to_string).collect();
1034 schema_obj.insert("type".to_string(), json!(types));
1035 }
1036 }
1037 }
1038
1039 if let Some(desc) = &obj_schema.description {
1041 schema_obj.insert("description".to_string(), json!(desc));
1042 }
1043
1044 if !obj_schema.one_of.is_empty() {
1046 let mut one_of_schemas = Vec::new();
1047 for schema_ref in &obj_schema.one_of {
1048 let schema_json = match schema_ref {
1049 ObjectOrReference::Object(schema) => {
1050 Self::convert_object_schema_to_json_schema(schema, spec, visited)?
1051 }
1052 ObjectOrReference::Ref { ref_path, .. } => {
1053 let resolved = Self::resolve_reference(ref_path, spec, visited)?;
1054 let result =
1055 Self::convert_object_schema_to_json_schema(&resolved, spec, visited)?;
1056 visited.remove(ref_path);
1058 result
1059 }
1060 };
1061 one_of_schemas.push(schema_json);
1062 }
1063 schema_obj.insert("oneOf".to_string(), json!(one_of_schemas));
1064 return Ok(Value::Object(schema_obj));
1067 }
1068
1069 if !obj_schema.properties.is_empty() {
1071 let properties = &obj_schema.properties;
1072 let mut props_map = serde_json::Map::new();
1073 for (prop_name, prop_schema_or_ref) in properties {
1074 let prop_schema = match prop_schema_or_ref {
1075 ObjectOrReference::Object(schema) => {
1076 Self::convert_schema_to_json_schema(
1078 &Schema::Object(Box::new(ObjectOrReference::Object(schema.clone()))),
1079 spec,
1080 visited,
1081 )?
1082 }
1083 ObjectOrReference::Ref { ref_path, .. } => {
1084 let resolved = Self::resolve_reference(ref_path, spec, visited)?;
1085 let result =
1086 Self::convert_object_schema_to_json_schema(&resolved, spec, visited)?;
1087 visited.remove(ref_path);
1089 result
1090 }
1091 };
1092
1093 let sanitized_name = sanitize_property_name(prop_name);
1095 props_map.insert(sanitized_name, prop_schema);
1096 }
1097 schema_obj.insert("properties".to_string(), Value::Object(props_map));
1098 }
1099
1100 if !obj_schema.required.is_empty() {
1102 schema_obj.insert("required".to_string(), json!(&obj_schema.required));
1103 }
1104
1105 if let Some(schema_type) = &obj_schema.schema_type
1107 && matches!(schema_type, SchemaTypeSet::Single(SchemaType::Object))
1108 {
1109 match &obj_schema.additional_properties {
1111 None => {
1112 schema_obj.insert("additionalProperties".to_string(), json!(true));
1114 }
1115 Some(Schema::Boolean(BooleanSchema(value))) => {
1116 schema_obj.insert("additionalProperties".to_string(), json!(value));
1118 }
1119 Some(Schema::Object(schema_ref)) => {
1120 let additional_props_schema = Self::convert_schema_to_json_schema(
1122 &Schema::Object(schema_ref.clone()),
1123 spec,
1124 visited,
1125 )?;
1126 schema_obj.insert("additionalProperties".to_string(), additional_props_schema);
1127 }
1128 }
1129 }
1130
1131 if let Some(schema_type) = &obj_schema.schema_type {
1133 if matches!(schema_type, SchemaTypeSet::Single(SchemaType::Array)) {
1134 if !obj_schema.prefix_items.is_empty() {
1136 Self::convert_prefix_items_to_draft07(
1138 &obj_schema.prefix_items,
1139 &obj_schema.items,
1140 &mut schema_obj,
1141 spec,
1142 )?;
1143 } else if let Some(items_schema) = &obj_schema.items {
1144 let items_json =
1146 Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
1147 schema_obj.insert("items".to_string(), items_json);
1148 }
1149
1150 if let Some(min_items) = obj_schema.min_items {
1152 schema_obj.insert("minItems".to_string(), json!(min_items));
1153 }
1154 if let Some(max_items) = obj_schema.max_items {
1155 schema_obj.insert("maxItems".to_string(), json!(max_items));
1156 }
1157 } else if let Some(items_schema) = &obj_schema.items {
1158 let items_json = Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
1160 schema_obj.insert("items".to_string(), items_json);
1161 }
1162 }
1163
1164 if let Some(format) = &obj_schema.format {
1166 schema_obj.insert("format".to_string(), json!(format));
1167 }
1168
1169 if let Some(example) = &obj_schema.example {
1170 schema_obj.insert("example".to_string(), example.clone());
1171 }
1172
1173 if let Some(default) = &obj_schema.default {
1174 schema_obj.insert("default".to_string(), default.clone());
1175 }
1176
1177 if !obj_schema.enum_values.is_empty() {
1178 schema_obj.insert("enum".to_string(), json!(&obj_schema.enum_values));
1179 }
1180
1181 if let Some(min) = &obj_schema.minimum {
1182 schema_obj.insert("minimum".to_string(), json!(min));
1183 }
1184
1185 if let Some(max) = &obj_schema.maximum {
1186 schema_obj.insert("maximum".to_string(), json!(max));
1187 }
1188
1189 if let Some(min_length) = &obj_schema.min_length {
1190 schema_obj.insert("minLength".to_string(), json!(min_length));
1191 }
1192
1193 if let Some(max_length) = &obj_schema.max_length {
1194 schema_obj.insert("maxLength".to_string(), json!(max_length));
1195 }
1196
1197 if let Some(pattern) = &obj_schema.pattern {
1198 schema_obj.insert("pattern".to_string(), json!(pattern));
1199 }
1200
1201 Ok(Value::Object(schema_obj))
1202 }
1203
1204 fn schema_type_to_string(schema_type: &SchemaType) -> String {
1206 match schema_type {
1207 SchemaType::Boolean => "boolean",
1208 SchemaType::Integer => "integer",
1209 SchemaType::Number => "number",
1210 SchemaType::String => "string",
1211 SchemaType::Array => "array",
1212 SchemaType::Object => "object",
1213 SchemaType::Null => "null",
1214 }
1215 .to_string()
1216 }
1217
1218 fn resolve_reference(
1228 ref_path: &str,
1229 spec: &Spec,
1230 visited: &mut HashSet<String>,
1231 ) -> Result<ObjectSchema, Error> {
1232 if visited.contains(ref_path) {
1234 return Err(Error::ToolGeneration(format!(
1235 "Circular reference detected: {ref_path}"
1236 )));
1237 }
1238
1239 visited.insert(ref_path.to_string());
1241
1242 if !ref_path.starts_with("#/components/schemas/") {
1245 return Err(Error::ToolGeneration(format!(
1246 "Unsupported reference format: {ref_path}. Only #/components/schemas/ references are supported"
1247 )));
1248 }
1249
1250 let schema_name = ref_path.strip_prefix("#/components/schemas/").unwrap();
1251
1252 let components = spec.components.as_ref().ok_or_else(|| {
1254 Error::ToolGeneration(format!(
1255 "Reference {ref_path} points to components, but spec has no components section"
1256 ))
1257 })?;
1258
1259 let schema_ref = components.schemas.get(schema_name).ok_or_else(|| {
1260 Error::ToolGeneration(format!(
1261 "Schema '{schema_name}' not found in components/schemas"
1262 ))
1263 })?;
1264
1265 let resolved_schema = match schema_ref {
1267 ObjectOrReference::Object(obj_schema) => obj_schema.clone(),
1268 ObjectOrReference::Ref {
1269 ref_path: nested_ref,
1270 ..
1271 } => {
1272 Self::resolve_reference(nested_ref, spec, visited)?
1274 }
1275 };
1276
1277 Ok(resolved_schema)
1283 }
1284
1285 fn resolve_reference_with_metadata(
1290 ref_path: &str,
1291 summary: Option<String>,
1292 description: Option<String>,
1293 spec: &Spec,
1294 visited: &mut HashSet<String>,
1295 ) -> Result<(ObjectSchema, ReferenceMetadata), Error> {
1296 let resolved_schema = Self::resolve_reference(ref_path, spec, visited)?;
1297 let metadata = ReferenceMetadata::new(summary, description);
1298 Ok((resolved_schema, metadata))
1299 }
1300
1301 fn generate_parameter_schema(
1303 parameters: &[ObjectOrReference<Parameter>],
1304 _method: &str,
1305 request_body: &Option<ObjectOrReference<RequestBody>>,
1306 spec: &Spec,
1307 skip_parameter_descriptions: bool,
1308 ) -> Result<
1309 (
1310 Value,
1311 std::collections::HashMap<String, crate::tool::ParameterMapping>,
1312 ),
1313 Error,
1314 > {
1315 let mut properties = serde_json::Map::new();
1316 let mut required = Vec::new();
1317 let mut parameter_mappings = std::collections::HashMap::new();
1318
1319 let mut path_params = Vec::new();
1321 let mut query_params = Vec::new();
1322 let mut header_params = Vec::new();
1323 let mut cookie_params = Vec::new();
1324
1325 for param_ref in parameters {
1326 let param = match param_ref {
1327 ObjectOrReference::Object(param) => param,
1328 ObjectOrReference::Ref { ref_path, .. } => {
1329 warn!(
1333 reference_path = %ref_path,
1334 "Parameter reference not resolved"
1335 );
1336 continue;
1337 }
1338 };
1339
1340 match ¶m.location {
1341 ParameterIn::Query => query_params.push(param),
1342 ParameterIn::Header => header_params.push(param),
1343 ParameterIn::Path => path_params.push(param),
1344 ParameterIn::Cookie => cookie_params.push(param),
1345 }
1346 }
1347
1348 for param in path_params {
1350 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1351 param,
1352 ParameterIn::Path,
1353 spec,
1354 skip_parameter_descriptions,
1355 )?;
1356
1357 let sanitized_name = sanitize_property_name(¶m.name);
1359 if sanitized_name != param.name {
1360 annotations = annotations.with_original_name(param.name.clone());
1361 }
1362
1363 let explode = annotations
1365 .annotations
1366 .iter()
1367 .find_map(|a| {
1368 if let Annotation::Explode(e) = a {
1369 Some(*e)
1370 } else {
1371 None
1372 }
1373 })
1374 .unwrap_or(true);
1375
1376 parameter_mappings.insert(
1378 sanitized_name.clone(),
1379 crate::tool::ParameterMapping {
1380 sanitized_name: sanitized_name.clone(),
1381 original_name: param.name.clone(),
1382 location: "path".to_string(),
1383 explode,
1384 },
1385 );
1386
1387 properties.insert(sanitized_name.clone(), param_schema);
1389 required.push(sanitized_name);
1390 }
1391
1392 for param in &query_params {
1394 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1395 param,
1396 ParameterIn::Query,
1397 spec,
1398 skip_parameter_descriptions,
1399 )?;
1400
1401 let sanitized_name = sanitize_property_name(¶m.name);
1403 if sanitized_name != param.name {
1404 annotations = annotations.with_original_name(param.name.clone());
1405 }
1406
1407 let explode = annotations
1409 .annotations
1410 .iter()
1411 .find_map(|a| {
1412 if let Annotation::Explode(e) = a {
1413 Some(*e)
1414 } else {
1415 None
1416 }
1417 })
1418 .unwrap_or(true);
1419
1420 parameter_mappings.insert(
1422 sanitized_name.clone(),
1423 crate::tool::ParameterMapping {
1424 sanitized_name: sanitized_name.clone(),
1425 original_name: param.name.clone(),
1426 location: "query".to_string(),
1427 explode,
1428 },
1429 );
1430
1431 properties.insert(sanitized_name.clone(), param_schema);
1433 if param.required.unwrap_or(false) {
1434 required.push(sanitized_name);
1435 }
1436 }
1437
1438 for param in &header_params {
1440 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1441 param,
1442 ParameterIn::Header,
1443 spec,
1444 skip_parameter_descriptions,
1445 )?;
1446
1447 let prefixed_name = format!("header_{}", param.name);
1449 let sanitized_name = sanitize_property_name(&prefixed_name);
1450 if sanitized_name != prefixed_name {
1451 annotations = annotations.with_original_name(param.name.clone());
1452 }
1453
1454 let explode = annotations
1456 .annotations
1457 .iter()
1458 .find_map(|a| {
1459 if let Annotation::Explode(e) = a {
1460 Some(*e)
1461 } else {
1462 None
1463 }
1464 })
1465 .unwrap_or(true);
1466
1467 parameter_mappings.insert(
1469 sanitized_name.clone(),
1470 crate::tool::ParameterMapping {
1471 sanitized_name: sanitized_name.clone(),
1472 original_name: param.name.clone(),
1473 location: "header".to_string(),
1474 explode,
1475 },
1476 );
1477
1478 properties.insert(sanitized_name.clone(), param_schema);
1480 if param.required.unwrap_or(false) {
1481 required.push(sanitized_name);
1482 }
1483 }
1484
1485 for param in &cookie_params {
1487 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1488 param,
1489 ParameterIn::Cookie,
1490 spec,
1491 skip_parameter_descriptions,
1492 )?;
1493
1494 let prefixed_name = format!("cookie_{}", param.name);
1496 let sanitized_name = sanitize_property_name(&prefixed_name);
1497 if sanitized_name != prefixed_name {
1498 annotations = annotations.with_original_name(param.name.clone());
1499 }
1500
1501 let explode = annotations
1503 .annotations
1504 .iter()
1505 .find_map(|a| {
1506 if let Annotation::Explode(e) = a {
1507 Some(*e)
1508 } else {
1509 None
1510 }
1511 })
1512 .unwrap_or(true);
1513
1514 parameter_mappings.insert(
1516 sanitized_name.clone(),
1517 crate::tool::ParameterMapping {
1518 sanitized_name: sanitized_name.clone(),
1519 original_name: param.name.clone(),
1520 location: "cookie".to_string(),
1521 explode,
1522 },
1523 );
1524
1525 properties.insert(sanitized_name.clone(), param_schema);
1527 if param.required.unwrap_or(false) {
1528 required.push(sanitized_name);
1529 }
1530 }
1531
1532 if let Some(request_body) = request_body
1534 && let Some((body_schema, _annotations, is_required)) =
1535 Self::convert_request_body_to_json_schema(request_body, spec)?
1536 {
1537 parameter_mappings.insert(
1539 "request_body".to_string(),
1540 crate::tool::ParameterMapping {
1541 sanitized_name: "request_body".to_string(),
1542 original_name: "request_body".to_string(),
1543 location: "body".to_string(),
1544 explode: false,
1545 },
1546 );
1547
1548 properties.insert("request_body".to_string(), body_schema);
1550 if is_required {
1551 required.push("request_body".to_string());
1552 }
1553 }
1554
1555 if !query_params.is_empty() || !header_params.is_empty() || !cookie_params.is_empty() {
1557 properties.insert(
1559 "timeout_seconds".to_string(),
1560 json!({
1561 "type": "integer",
1562 "description": "Request timeout in seconds",
1563 "minimum": 1,
1564 "maximum": 300,
1565 "default": 30
1566 }),
1567 );
1568 }
1569
1570 let schema = json!({
1571 "type": "object",
1572 "properties": properties,
1573 "required": required,
1574 "additionalProperties": false
1575 });
1576
1577 Ok((schema, parameter_mappings))
1578 }
1579
1580 fn convert_parameter_schema(
1582 param: &Parameter,
1583 location: ParameterIn,
1584 spec: &Spec,
1585 skip_parameter_descriptions: bool,
1586 ) -> Result<(Value, Annotations), Error> {
1587 let base_schema = if let Some(schema_ref) = ¶m.schema {
1589 match schema_ref {
1590 ObjectOrReference::Object(obj_schema) => {
1591 let mut visited = HashSet::new();
1592 Self::convert_schema_to_json_schema(
1593 &Schema::Object(Box::new(ObjectOrReference::Object(obj_schema.clone()))),
1594 spec,
1595 &mut visited,
1596 )?
1597 }
1598 ObjectOrReference::Ref {
1599 ref_path,
1600 summary,
1601 description,
1602 } => {
1603 let mut visited = HashSet::new();
1605 match Self::resolve_reference_with_metadata(
1606 ref_path,
1607 summary.clone(),
1608 description.clone(),
1609 spec,
1610 &mut visited,
1611 ) {
1612 Ok((resolved_schema, ref_metadata)) => {
1613 let mut schema_json = Self::convert_schema_to_json_schema(
1614 &Schema::Object(Box::new(ObjectOrReference::Object(
1615 resolved_schema,
1616 ))),
1617 spec,
1618 &mut visited,
1619 )?;
1620
1621 if let Value::Object(ref mut schema_obj) = schema_json {
1623 if let Some(ref_desc) = ref_metadata.best_description() {
1625 schema_obj.insert("description".to_string(), json!(ref_desc));
1626 }
1627 }
1630
1631 schema_json
1632 }
1633 Err(_) => {
1634 json!({"type": "string"})
1636 }
1637 }
1638 }
1639 }
1640 } else {
1641 json!({"type": "string"})
1643 };
1644
1645 let mut result = match base_schema {
1647 Value::Object(obj) => obj,
1648 _ => {
1649 return Err(Error::ToolGeneration(format!(
1651 "Internal error: schema converter returned non-object for parameter '{}'",
1652 param.name
1653 )));
1654 }
1655 };
1656
1657 let mut collected_examples = Vec::new();
1659
1660 if let Some(example) = ¶m.example {
1662 collected_examples.push(example.clone());
1663 } else if !param.examples.is_empty() {
1664 for example_ref in param.examples.values() {
1666 match example_ref {
1667 ObjectOrReference::Object(example_obj) => {
1668 if let Some(value) = &example_obj.value {
1669 collected_examples.push(value.clone());
1670 }
1671 }
1672 ObjectOrReference::Ref { .. } => {
1673 }
1675 }
1676 }
1677 } else if let Some(Value::String(ex_str)) = result.get("example") {
1678 collected_examples.push(json!(ex_str));
1680 } else if let Some(ex) = result.get("example") {
1681 collected_examples.push(ex.clone());
1682 }
1683
1684 let base_description = param
1686 .description
1687 .as_ref()
1688 .map(|d| d.to_string())
1689 .or_else(|| {
1690 result
1691 .get("description")
1692 .and_then(|d| d.as_str())
1693 .map(|d| d.to_string())
1694 })
1695 .unwrap_or_else(|| format!("{} parameter", param.name));
1696
1697 let description_with_examples = if let Some(examples_str) =
1698 Self::format_examples_for_description(&collected_examples)
1699 {
1700 format!("{base_description}. {examples_str}")
1701 } else {
1702 base_description
1703 };
1704
1705 if !skip_parameter_descriptions {
1706 result.insert("description".to_string(), json!(description_with_examples));
1707 }
1708
1709 if let Some(example) = ¶m.example {
1714 result.insert("example".to_string(), example.clone());
1715 } else if !param.examples.is_empty() {
1716 let mut examples_array = Vec::new();
1719 for (example_name, example_ref) in ¶m.examples {
1720 match example_ref {
1721 ObjectOrReference::Object(example_obj) => {
1722 if let Some(value) = &example_obj.value {
1723 examples_array.push(json!({
1724 "name": example_name,
1725 "value": value
1726 }));
1727 }
1728 }
1729 ObjectOrReference::Ref { .. } => {
1730 }
1733 }
1734 }
1735
1736 if !examples_array.is_empty() {
1737 if let Some(first_example) = examples_array.first()
1739 && let Some(value) = first_example.get("value")
1740 {
1741 result.insert("example".to_string(), value.clone());
1742 }
1743 result.insert("x-examples".to_string(), json!(examples_array));
1745 }
1746 }
1747
1748 let mut annotations = Annotations::new()
1750 .with_location(Location::Parameter(location))
1751 .with_required(param.required.unwrap_or(false));
1752
1753 if let Some(explode) = param.explode {
1755 annotations = annotations.with_explode(explode);
1756 } else {
1757 let default_explode = match ¶m.style {
1761 Some(ParameterStyle::Form) | None => true, _ => false,
1763 };
1764 annotations = annotations.with_explode(default_explode);
1765 }
1766
1767 Ok((Value::Object(result), annotations))
1768 }
1769
1770 fn format_examples_for_description(examples: &[Value]) -> Option<String> {
1772 if examples.is_empty() {
1773 return None;
1774 }
1775
1776 if examples.len() == 1 {
1777 let example_str =
1778 serde_json::to_string(&examples[0]).unwrap_or_else(|_| "null".to_string());
1779 Some(format!("Example: `{example_str}`"))
1780 } else {
1781 let mut result = String::from("Examples:\n");
1782 for ex in examples {
1783 let json_str = serde_json::to_string(ex).unwrap_or_else(|_| "null".to_string());
1784 result.push_str(&format!("- `{json_str}`\n"));
1785 }
1786 result.pop();
1788 Some(result)
1789 }
1790 }
1791
1792 fn convert_prefix_items_to_draft07(
1803 prefix_items: &[ObjectOrReference<ObjectSchema>],
1804 items: &Option<Box<Schema>>,
1805 result: &mut serde_json::Map<String, Value>,
1806 spec: &Spec,
1807 ) -> Result<(), Error> {
1808 let prefix_count = prefix_items.len();
1809
1810 let mut item_types = Vec::new();
1812 for prefix_item in prefix_items {
1813 match prefix_item {
1814 ObjectOrReference::Object(obj_schema) => {
1815 if let Some(schema_type) = &obj_schema.schema_type {
1816 match schema_type {
1817 SchemaTypeSet::Single(SchemaType::String) => item_types.push("string"),
1818 SchemaTypeSet::Single(SchemaType::Integer) => {
1819 item_types.push("integer")
1820 }
1821 SchemaTypeSet::Single(SchemaType::Number) => item_types.push("number"),
1822 SchemaTypeSet::Single(SchemaType::Boolean) => {
1823 item_types.push("boolean")
1824 }
1825 SchemaTypeSet::Single(SchemaType::Array) => item_types.push("array"),
1826 SchemaTypeSet::Single(SchemaType::Object) => item_types.push("object"),
1827 _ => item_types.push("string"), }
1829 } else {
1830 item_types.push("string"); }
1832 }
1833 ObjectOrReference::Ref { ref_path, .. } => {
1834 let mut visited = HashSet::new();
1836 match Self::resolve_reference(ref_path, spec, &mut visited) {
1837 Ok(resolved_schema) => {
1838 if let Some(schema_type_set) = &resolved_schema.schema_type {
1840 match schema_type_set {
1841 SchemaTypeSet::Single(SchemaType::String) => {
1842 item_types.push("string")
1843 }
1844 SchemaTypeSet::Single(SchemaType::Integer) => {
1845 item_types.push("integer")
1846 }
1847 SchemaTypeSet::Single(SchemaType::Number) => {
1848 item_types.push("number")
1849 }
1850 SchemaTypeSet::Single(SchemaType::Boolean) => {
1851 item_types.push("boolean")
1852 }
1853 SchemaTypeSet::Single(SchemaType::Array) => {
1854 item_types.push("array")
1855 }
1856 SchemaTypeSet::Single(SchemaType::Object) => {
1857 item_types.push("object")
1858 }
1859 _ => item_types.push("string"), }
1861 } else {
1862 item_types.push("string"); }
1864 }
1865 Err(_) => {
1866 item_types.push("string");
1868 }
1869 }
1870 }
1871 }
1872 }
1873
1874 let items_is_false =
1876 matches!(items.as_ref().map(|i| i.as_ref()), Some(Schema::Boolean(b)) if !b.0);
1877
1878 if items_is_false {
1879 result.insert("minItems".to_string(), json!(prefix_count));
1881 result.insert("maxItems".to_string(), json!(prefix_count));
1882 }
1883
1884 let unique_types: std::collections::BTreeSet<_> = item_types.into_iter().collect();
1886
1887 if unique_types.len() == 1 {
1888 let item_type = unique_types.into_iter().next().unwrap();
1890 result.insert("items".to_string(), json!({"type": item_type}));
1891 } else if unique_types.len() > 1 {
1892 let one_of: Vec<Value> = unique_types
1894 .into_iter()
1895 .map(|t| json!({"type": t}))
1896 .collect();
1897 result.insert("items".to_string(), json!({"oneOf": one_of}));
1898 }
1899
1900 Ok(())
1901 }
1902
1903 fn convert_request_body_to_json_schema(
1915 request_body_ref: &ObjectOrReference<RequestBody>,
1916 spec: &Spec,
1917 ) -> Result<Option<(Value, Annotations, bool)>, Error> {
1918 match request_body_ref {
1919 ObjectOrReference::Object(request_body) => {
1920 if let Some(media_type) = request_body.content.get("multipart/form-data") {
1922 return Self::convert_multipart_request_body(request_body, media_type, spec);
1923 }
1924
1925 let schema_info = request_body
1928 .content
1929 .get(mime::APPLICATION_JSON.as_ref())
1930 .or_else(|| request_body.content.get("application/json"))
1931 .or_else(|| {
1932 request_body.content.values().next()
1934 });
1935
1936 if let Some(media_type) = schema_info {
1937 if let Some(schema_ref) = &media_type.schema {
1938 let schema = Schema::Object(Box::new(schema_ref.clone()));
1940
1941 let mut visited = HashSet::new();
1943 let converted_schema =
1944 Self::convert_schema_to_json_schema(&schema, spec, &mut visited)?;
1945
1946 let mut schema_obj = match converted_schema {
1948 Value::Object(obj) => obj,
1949 _ => {
1950 let mut obj = serde_json::Map::new();
1952 obj.insert("type".to_string(), json!("object"));
1953 obj.insert("additionalProperties".to_string(), json!(true));
1954 obj
1955 }
1956 };
1957
1958 if !schema_obj.contains_key("description") {
1960 let description = request_body
1961 .description
1962 .clone()
1963 .unwrap_or_else(|| "Request body data".to_string());
1964 schema_obj.insert("description".to_string(), json!(description));
1965 }
1966
1967 let annotations = Annotations::new()
1969 .with_location(Location::Body)
1970 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
1971
1972 let required = request_body.required.unwrap_or(false);
1973 Ok(Some((Value::Object(schema_obj), annotations, required)))
1974 } else {
1975 Ok(None)
1976 }
1977 } else {
1978 Ok(None)
1979 }
1980 }
1981 ObjectOrReference::Ref {
1982 ref_path: _,
1983 summary,
1984 description,
1985 } => {
1986 let ref_metadata = ReferenceMetadata::new(summary.clone(), description.clone());
1988 let enhanced_description = ref_metadata
1989 .best_description()
1990 .map(|desc| desc.to_string())
1991 .unwrap_or_else(|| "Request body data".to_string());
1992
1993 let mut result = serde_json::Map::new();
1994 result.insert("type".to_string(), json!("object"));
1995 result.insert("additionalProperties".to_string(), json!(true));
1996 result.insert("description".to_string(), json!(enhanced_description));
1997
1998 let annotations = Annotations::new()
2000 .with_location(Location::Body)
2001 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
2002
2003 Ok(Some((Value::Object(result), annotations, false)))
2004 }
2005 }
2006 }
2007
2008 fn convert_multipart_request_body(
2017 request_body: &RequestBody,
2018 media_type: &oas3::spec::MediaType,
2019 spec: &Spec,
2020 ) -> Result<Option<(Value, Annotations, bool)>, Error> {
2021 let Some(schema_ref) = &media_type.schema else {
2022 return Ok(None);
2023 };
2024
2025 let obj_schema = match schema_ref {
2027 ObjectOrReference::Object(obj) => obj.clone(),
2028 ObjectOrReference::Ref { ref_path, .. } => {
2029 let mut visited = HashSet::new();
2031 Self::resolve_reference(ref_path, spec, &mut visited)?
2032 }
2033 };
2034
2035 let mut props_map = serde_json::Map::new();
2037 let mut file_fields = Vec::new();
2038
2039 for (prop_name, prop_schema_or_ref) in &obj_schema.properties {
2040 let sanitized_name = sanitize_property_name(prop_name);
2041
2042 let prop_schema = if Self::is_file_field_property(prop_schema_or_ref) {
2043 file_fields.push(sanitized_name.clone());
2045
2046 let description = match prop_schema_or_ref {
2048 ObjectOrReference::Object(obj) => obj.description.as_deref(),
2049 ObjectOrReference::Ref { .. } => None,
2050 };
2051
2052 Self::convert_file_field_to_schema(description)
2054 } else {
2055 let schema = Schema::Object(Box::new(prop_schema_or_ref.clone()));
2057 let mut visited = HashSet::new();
2058 Self::convert_schema_to_json_schema(&schema, spec, &mut visited)?
2059 };
2060
2061 props_map.insert(sanitized_name, prop_schema);
2062 }
2063
2064 let mut schema_obj = serde_json::Map::new();
2066 schema_obj.insert("type".to_string(), json!("object"));
2067
2068 if !props_map.is_empty() {
2069 schema_obj.insert("properties".to_string(), Value::Object(props_map));
2070 }
2071
2072 if !obj_schema.required.is_empty() {
2074 let sanitized_required: Vec<String> = obj_schema
2076 .required
2077 .iter()
2078 .map(|name| sanitize_property_name(name))
2079 .collect();
2080 schema_obj.insert("required".to_string(), json!(sanitized_required));
2081 }
2082
2083 let description = obj_schema
2085 .description
2086 .clone()
2087 .or_else(|| request_body.description.clone())
2088 .unwrap_or_else(|| "Request body data".to_string());
2089 schema_obj.insert("description".to_string(), json!(description));
2090
2091 let mut annotations = Annotations::new()
2093 .with_location(Location::Body)
2094 .with_content_type("multipart/form-data".to_string());
2095
2096 if !file_fields.is_empty() {
2097 annotations = annotations.with_file_fields(file_fields);
2098 }
2099
2100 let required = request_body.required.unwrap_or(false);
2101 Ok(Some((Value::Object(schema_obj), annotations, required)))
2102 }
2103
2104 pub fn extract_parameters(
2110 tool_metadata: &ToolMetadata,
2111 arguments: &Value,
2112 ) -> Result<ExtractedParameters, ToolCallValidationError> {
2113 let args = arguments.as_object().ok_or_else(|| {
2114 ToolCallValidationError::RequestConstructionError {
2115 reason: "Arguments must be an object".to_string(),
2116 }
2117 })?;
2118
2119 trace!(
2120 tool_name = %tool_metadata.name,
2121 raw_arguments = ?arguments,
2122 "Starting parameter extraction"
2123 );
2124
2125 let mut path_params = HashMap::new();
2126 let mut query_params = HashMap::new();
2127 let mut header_params = HashMap::new();
2128 let mut cookie_params = HashMap::new();
2129 let mut body_params = HashMap::new();
2130 let mut config = RequestConfig::default();
2131
2132 if let Some(timeout) = args.get("timeout_seconds").and_then(Value::as_u64) {
2134 config.timeout_seconds = u32::try_from(timeout).unwrap_or(u32::MAX);
2135 }
2136
2137 for (key, value) in args {
2139 if key == "timeout_seconds" {
2140 continue; }
2142
2143 if key == "request_body" {
2145 body_params.insert("request_body".to_string(), value.clone());
2146 continue;
2147 }
2148
2149 let mapping = tool_metadata.parameter_mappings.get(key);
2151
2152 if let Some(mapping) = mapping {
2153 match mapping.location.as_str() {
2155 "path" => {
2156 path_params.insert(mapping.original_name.clone(), value.clone());
2157 }
2158 "query" => {
2159 query_params.insert(
2160 mapping.original_name.clone(),
2161 QueryParameter::new(value.clone(), mapping.explode),
2162 );
2163 }
2164 "header" => {
2165 header_params.insert(mapping.original_name.clone(), value.clone());
2166 }
2167 "cookie" => {
2168 cookie_params.insert(mapping.original_name.clone(), value.clone());
2169 }
2170 "body" => {
2171 body_params.insert(mapping.original_name.clone(), value.clone());
2172 }
2173 _ => {
2174 return Err(ToolCallValidationError::RequestConstructionError {
2175 reason: format!("Unknown parameter location for parameter: {key}"),
2176 });
2177 }
2178 }
2179 } else {
2180 let location = Self::get_parameter_location(tool_metadata, key).map_err(|e| {
2182 ToolCallValidationError::RequestConstructionError {
2183 reason: e.to_string(),
2184 }
2185 })?;
2186
2187 let original_name = Self::get_original_parameter_name(tool_metadata, key);
2188
2189 match location.as_str() {
2190 "path" => {
2191 path_params
2192 .insert(original_name.unwrap_or_else(|| key.clone()), value.clone());
2193 }
2194 "query" => {
2195 let param_name = original_name.unwrap_or_else(|| key.clone());
2196 let explode = Self::get_parameter_explode(tool_metadata, key);
2197 query_params
2198 .insert(param_name, QueryParameter::new(value.clone(), explode));
2199 }
2200 "header" => {
2201 let header_name = if let Some(orig) = original_name {
2202 orig
2203 } else if key.starts_with("header_") {
2204 key.strip_prefix("header_").unwrap_or(key).to_string()
2205 } else {
2206 key.clone()
2207 };
2208 header_params.insert(header_name, value.clone());
2209 }
2210 "cookie" => {
2211 let cookie_name = if let Some(orig) = original_name {
2212 orig
2213 } else if key.starts_with("cookie_") {
2214 key.strip_prefix("cookie_").unwrap_or(key).to_string()
2215 } else {
2216 key.clone()
2217 };
2218 cookie_params.insert(cookie_name, value.clone());
2219 }
2220 "body" => {
2221 let body_name = if key.starts_with("body_") {
2222 key.strip_prefix("body_").unwrap_or(key).to_string()
2223 } else {
2224 key.clone()
2225 };
2226 body_params.insert(body_name, value.clone());
2227 }
2228 _ => {
2229 return Err(ToolCallValidationError::RequestConstructionError {
2230 reason: format!("Unknown parameter location for parameter: {key}"),
2231 });
2232 }
2233 }
2234 }
2235 }
2236
2237 let extracted = ExtractedParameters {
2238 path: path_params,
2239 query: query_params,
2240 headers: header_params,
2241 cookies: cookie_params,
2242 body: body_params,
2243 config,
2244 };
2245
2246 trace!(
2247 tool_name = %tool_metadata.name,
2248 extracted_parameters = ?extracted,
2249 "Parameter extraction completed"
2250 );
2251
2252 Self::validate_parameters(tool_metadata, arguments)?;
2254
2255 Ok(extracted)
2256 }
2257
2258 fn get_original_parameter_name(
2260 tool_metadata: &ToolMetadata,
2261 param_name: &str,
2262 ) -> Option<String> {
2263 tool_metadata
2264 .parameters
2265 .get("properties")
2266 .and_then(|p| p.as_object())
2267 .and_then(|props| props.get(param_name))
2268 .and_then(|schema| schema.get(X_ORIGINAL_NAME))
2269 .and_then(|v| v.as_str())
2270 .map(|s| s.to_string())
2271 }
2272
2273 fn get_parameter_explode(tool_metadata: &ToolMetadata, param_name: &str) -> bool {
2275 tool_metadata
2276 .parameters
2277 .get("properties")
2278 .and_then(|p| p.as_object())
2279 .and_then(|props| props.get(param_name))
2280 .and_then(|schema| schema.get(X_PARAMETER_EXPLODE))
2281 .and_then(|v| v.as_bool())
2282 .unwrap_or(true) }
2284
2285 fn get_parameter_location(
2287 tool_metadata: &ToolMetadata,
2288 param_name: &str,
2289 ) -> Result<String, Error> {
2290 let properties = tool_metadata
2291 .parameters
2292 .get("properties")
2293 .and_then(|p| p.as_object())
2294 .ok_or_else(|| Error::ToolGeneration("Invalid tool parameters schema".to_string()))?;
2295
2296 if let Some(param_schema) = properties.get(param_name)
2297 && let Some(location) = param_schema
2298 .get(X_PARAMETER_LOCATION)
2299 .and_then(|v| v.as_str())
2300 {
2301 return Ok(location.to_string());
2302 }
2303
2304 if param_name.starts_with("header_") {
2306 Ok("header".to_string())
2307 } else if param_name.starts_with("cookie_") {
2308 Ok("cookie".to_string())
2309 } else if param_name.starts_with("body_") {
2310 Ok("body".to_string())
2311 } else {
2312 Ok("query".to_string())
2314 }
2315 }
2316
2317 fn validate_parameters(
2319 tool_metadata: &ToolMetadata,
2320 arguments: &Value,
2321 ) -> Result<(), ToolCallValidationError> {
2322 let schema = &tool_metadata.parameters;
2323
2324 let required_params = schema
2326 .get("required")
2327 .and_then(|r| r.as_array())
2328 .map(|arr| {
2329 arr.iter()
2330 .filter_map(|v| v.as_str())
2331 .collect::<std::collections::HashSet<_>>()
2332 })
2333 .unwrap_or_default();
2334
2335 let properties = schema
2336 .get("properties")
2337 .and_then(|p| p.as_object())
2338 .ok_or_else(|| ToolCallValidationError::RequestConstructionError {
2339 reason: "Tool schema missing properties".to_string(),
2340 })?;
2341
2342 let args = arguments.as_object().ok_or_else(|| {
2343 ToolCallValidationError::RequestConstructionError {
2344 reason: "Arguments must be an object".to_string(),
2345 }
2346 })?;
2347
2348 let mut all_errors = Vec::new();
2350
2351 all_errors.extend(Self::check_unknown_parameters(args, properties));
2353
2354 all_errors.extend(Self::check_missing_required(
2356 args,
2357 properties,
2358 &required_params,
2359 ));
2360
2361 all_errors.extend(Self::validate_parameter_values(
2363 args,
2364 properties,
2365 &required_params,
2366 ));
2367
2368 if !all_errors.is_empty() {
2370 return Err(ToolCallValidationError::InvalidParameters {
2371 violations: all_errors,
2372 });
2373 }
2374
2375 Ok(())
2376 }
2377
2378 fn check_unknown_parameters(
2380 args: &serde_json::Map<String, Value>,
2381 properties: &serde_json::Map<String, Value>,
2382 ) -> Vec<ValidationError> {
2383 let mut errors = Vec::new();
2384
2385 let valid_params: Vec<String> = properties.keys().map(|s| s.to_string()).collect();
2387
2388 for (arg_name, _) in args.iter() {
2390 if !properties.contains_key(arg_name) {
2391 errors.push(ValidationError::invalid_parameter(
2393 arg_name.clone(),
2394 &valid_params,
2395 ));
2396 }
2397 }
2398
2399 errors
2400 }
2401
2402 fn check_missing_required(
2404 args: &serde_json::Map<String, Value>,
2405 properties: &serde_json::Map<String, Value>,
2406 required_params: &HashSet<&str>,
2407 ) -> Vec<ValidationError> {
2408 let mut errors = Vec::new();
2409
2410 for required_param in required_params {
2411 if !args.contains_key(*required_param) {
2412 let param_schema = properties.get(*required_param);
2414
2415 let description = param_schema
2416 .and_then(|schema| schema.get("description"))
2417 .and_then(|d| d.as_str())
2418 .map(|s| s.to_string());
2419
2420 let expected_type = param_schema
2421 .and_then(Self::get_expected_type)
2422 .unwrap_or_else(|| "unknown".to_string());
2423
2424 errors.push(ValidationError::MissingRequiredParameter {
2425 parameter: (*required_param).to_string(),
2426 description,
2427 expected_type,
2428 });
2429 }
2430 }
2431
2432 errors
2433 }
2434
2435 fn validate_parameter_values(
2437 args: &serde_json::Map<String, Value>,
2438 properties: &serde_json::Map<String, Value>,
2439 required_params: &std::collections::HashSet<&str>,
2440 ) -> Vec<ValidationError> {
2441 let mut errors = Vec::new();
2442
2443 for (param_name, param_value) in args {
2444 if let Some(param_schema) = properties.get(param_name) {
2445 let is_null_value = param_value.is_null();
2447 let is_required = required_params.contains(param_name.as_str());
2448
2449 let schema = json!({
2451 "type": "object",
2452 "properties": {
2453 param_name: param_schema
2454 }
2455 });
2456
2457 let compiled = match jsonschema::validator_for(&schema) {
2459 Ok(compiled) => compiled,
2460 Err(e) => {
2461 errors.push(ValidationError::ConstraintViolation {
2462 parameter: param_name.clone(),
2463 message: format!(
2464 "Failed to compile schema for parameter '{param_name}': {e}"
2465 ),
2466 field_path: None,
2467 actual_value: None,
2468 expected_type: None,
2469 constraints: vec![],
2470 });
2471 continue;
2472 }
2473 };
2474
2475 let instance = json!({ param_name: param_value });
2477
2478 let validation_errors: Vec<_> =
2480 compiled.validate(&instance).err().into_iter().collect();
2481
2482 for validation_error in validation_errors {
2483 let error_message = validation_error.to_string();
2485 let instance_path_str = validation_error.instance_path().to_string();
2486 let field_path = if instance_path_str.is_empty() || instance_path_str == "/" {
2487 Some(param_name.clone())
2488 } else {
2489 Some(instance_path_str.trim_start_matches('/').to_string())
2490 };
2491
2492 let constraints = Self::extract_constraints_from_schema(param_schema);
2494
2495 let expected_type = Self::get_expected_type(param_schema);
2497
2498 let maybe_type_error = match &validation_error.kind() {
2502 ValidationErrorKind::Type { kind } => Some(kind),
2503 _ => None,
2504 };
2505 let is_type_error = maybe_type_error.is_some();
2506 let is_null_error = is_null_value
2507 || (is_type_error && validation_error.instance().as_null().is_some());
2508 let message = if is_null_error && let Some(type_error) = maybe_type_error {
2509 let field_name = field_path.as_ref().unwrap_or(param_name);
2511
2512 let final_expected_type =
2514 expected_type.clone().unwrap_or_else(|| match type_error {
2515 TypeKind::Single(json_type) => json_type.to_string(),
2516 TypeKind::Multiple(json_type_set) => json_type_set
2517 .iter()
2518 .map(|t| t.to_string())
2519 .collect::<Vec<_>>()
2520 .join(", "),
2521 });
2522
2523 let actual_field_name = field_path
2526 .as_ref()
2527 .and_then(|path| path.split('/').next_back())
2528 .unwrap_or(param_name);
2529
2530 let is_nested_field = field_path.as_ref().is_some_and(|p| p.contains('/'));
2533
2534 let field_is_required = if is_nested_field {
2535 constraints.iter().any(|c| {
2536 if let ValidationConstraint::Required { properties } = c {
2537 properties.contains(&actual_field_name.to_string())
2538 } else {
2539 false
2540 }
2541 })
2542 } else {
2543 is_required
2544 };
2545
2546 if field_is_required {
2547 format!(
2548 "Parameter '{field_name}' is required and must not be null (expected: {final_expected_type})"
2549 )
2550 } else {
2551 format!(
2552 "Parameter '{field_name}' is optional but must not be null (expected: {final_expected_type})"
2553 )
2554 }
2555 } else {
2556 error_message
2557 };
2558
2559 errors.push(ValidationError::ConstraintViolation {
2560 parameter: param_name.clone(),
2561 message,
2562 field_path,
2563 actual_value: Some(Box::new(param_value.clone())),
2564 expected_type,
2565 constraints,
2566 });
2567 }
2568 }
2569 }
2570
2571 errors
2572 }
2573
2574 fn extract_constraints_from_schema(schema: &Value) -> Vec<ValidationConstraint> {
2576 let mut constraints = Vec::new();
2577
2578 if let Some(min_value) = schema.get("minimum").and_then(|v| v.as_f64()) {
2580 let exclusive = schema
2581 .get("exclusiveMinimum")
2582 .and_then(|v| v.as_bool())
2583 .unwrap_or(false);
2584 constraints.push(ValidationConstraint::Minimum {
2585 value: min_value,
2586 exclusive,
2587 });
2588 }
2589
2590 if let Some(max_value) = schema.get("maximum").and_then(|v| v.as_f64()) {
2592 let exclusive = schema
2593 .get("exclusiveMaximum")
2594 .and_then(|v| v.as_bool())
2595 .unwrap_or(false);
2596 constraints.push(ValidationConstraint::Maximum {
2597 value: max_value,
2598 exclusive,
2599 });
2600 }
2601
2602 if let Some(min_len) = schema
2604 .get("minLength")
2605 .and_then(|v| v.as_u64())
2606 .map(|v| v as usize)
2607 {
2608 constraints.push(ValidationConstraint::MinLength { value: min_len });
2609 }
2610
2611 if let Some(max_len) = schema
2613 .get("maxLength")
2614 .and_then(|v| v.as_u64())
2615 .map(|v| v as usize)
2616 {
2617 constraints.push(ValidationConstraint::MaxLength { value: max_len });
2618 }
2619
2620 if let Some(pattern) = schema
2622 .get("pattern")
2623 .and_then(|v| v.as_str())
2624 .map(|s| s.to_string())
2625 {
2626 constraints.push(ValidationConstraint::Pattern { pattern });
2627 }
2628
2629 if let Some(enum_values) = schema.get("enum").and_then(|v| v.as_array()).cloned() {
2631 constraints.push(ValidationConstraint::EnumValues {
2632 values: enum_values,
2633 });
2634 }
2635
2636 if let Some(format) = schema
2638 .get("format")
2639 .and_then(|v| v.as_str())
2640 .map(|s| s.to_string())
2641 {
2642 constraints.push(ValidationConstraint::Format { format });
2643 }
2644
2645 if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
2647 constraints.push(ValidationConstraint::MultipleOf { value: multiple_of });
2648 }
2649
2650 if let Some(min_items) = schema
2652 .get("minItems")
2653 .and_then(|v| v.as_u64())
2654 .map(|v| v as usize)
2655 {
2656 constraints.push(ValidationConstraint::MinItems { value: min_items });
2657 }
2658
2659 if let Some(max_items) = schema
2661 .get("maxItems")
2662 .and_then(|v| v.as_u64())
2663 .map(|v| v as usize)
2664 {
2665 constraints.push(ValidationConstraint::MaxItems { value: max_items });
2666 }
2667
2668 if let Some(true) = schema.get("uniqueItems").and_then(|v| v.as_bool()) {
2670 constraints.push(ValidationConstraint::UniqueItems);
2671 }
2672
2673 if let Some(min_props) = schema
2675 .get("minProperties")
2676 .and_then(|v| v.as_u64())
2677 .map(|v| v as usize)
2678 {
2679 constraints.push(ValidationConstraint::MinProperties { value: min_props });
2680 }
2681
2682 if let Some(max_props) = schema
2684 .get("maxProperties")
2685 .and_then(|v| v.as_u64())
2686 .map(|v| v as usize)
2687 {
2688 constraints.push(ValidationConstraint::MaxProperties { value: max_props });
2689 }
2690
2691 if let Some(const_value) = schema.get("const").cloned() {
2693 constraints.push(ValidationConstraint::ConstValue { value: const_value });
2694 }
2695
2696 if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
2698 let properties: Vec<String> = required
2699 .iter()
2700 .filter_map(|v| v.as_str().map(|s| s.to_string()))
2701 .collect();
2702 if !properties.is_empty() {
2703 constraints.push(ValidationConstraint::Required { properties });
2704 }
2705 }
2706
2707 constraints
2708 }
2709
2710 fn get_expected_type(schema: &Value) -> Option<String> {
2712 if let Some(type_value) = schema.get("type") {
2713 if let Some(type_str) = type_value.as_str() {
2714 return Some(type_str.to_string());
2715 } else if let Some(type_array) = type_value.as_array() {
2716 let types: Vec<String> = type_array
2718 .iter()
2719 .filter_map(|v| v.as_str())
2720 .map(|s| s.to_string())
2721 .collect();
2722 if !types.is_empty() {
2723 return Some(types.join(" | "));
2724 }
2725 }
2726 }
2727 None
2728 }
2729
2730 fn wrap_output_schema(
2754 body_schema: &ObjectOrReference<ObjectSchema>,
2755 spec: &Spec,
2756 ) -> Result<Value, Error> {
2757 let mut visited = HashSet::new();
2759 let body_schema_json = match body_schema {
2760 ObjectOrReference::Object(obj_schema) => {
2761 Self::convert_object_schema_to_json_schema(obj_schema, spec, &mut visited)?
2762 }
2763 ObjectOrReference::Ref { ref_path, .. } => {
2764 let resolved = Self::resolve_reference(ref_path, spec, &mut visited)?;
2765 let result =
2766 Self::convert_object_schema_to_json_schema(&resolved, spec, &mut visited)?;
2767 visited.remove(ref_path);
2769 result
2770 }
2771 };
2772
2773 let error_schema = create_error_response_schema();
2774
2775 Ok(json!({
2776 "type": "object",
2777 "description": "Unified response structure with success and error variants",
2778 "required": ["status", "body"],
2779 "additionalProperties": false,
2780 "properties": {
2781 "status": {
2782 "type": "integer",
2783 "description": "HTTP status code",
2784 "minimum": 100,
2785 "maximum": 599
2786 },
2787 "body": {
2788 "description": "Response body - either success data or error information",
2789 "oneOf": [
2790 body_schema_json,
2791 error_schema
2792 ]
2793 }
2794 }
2795 }))
2796 }
2797
2798 #[must_use]
2809 pub fn is_file_field(schema: &Schema) -> bool {
2810 match schema {
2811 Schema::Object(obj_or_ref) => match obj_or_ref.as_ref() {
2812 ObjectOrReference::Object(obj_schema) => {
2813 Self::is_file_field_object_schema(obj_schema)
2814 }
2815 ObjectOrReference::Ref { .. } => {
2816 false
2818 }
2819 },
2820 Schema::Boolean(_) => false,
2821 }
2822 }
2823
2824 fn is_file_field_object_schema(obj_schema: &ObjectSchema) -> bool {
2829 if let Some(format) = &obj_schema.format {
2830 format == "binary" || format == "byte"
2831 } else {
2832 false
2833 }
2834 }
2835
2836 fn is_file_field_property(prop_schema: &ObjectOrReference<ObjectSchema>) -> bool {
2841 match prop_schema {
2842 ObjectOrReference::Object(obj_schema) => Self::is_file_field_object_schema(obj_schema),
2843 ObjectOrReference::Ref { .. } => {
2844 false
2846 }
2847 }
2848 }
2849
2850 fn convert_file_field_to_schema(original_description: Option<&str>) -> Value {
2862 let description = original_description.unwrap_or("File upload");
2863 json!({
2864 "type": "object",
2865 "description": description,
2866 "properties": {
2867 "content": {
2868 "type": "string",
2869 "description": "File content as data URI (e.g., data:image/png;base64,...)"
2870 },
2871 "filename": {
2872 "type": "string",
2873 "description": "Optional filename for the upload"
2874 }
2875 },
2876 "required": ["content"]
2877 })
2878 }
2879}
2880
2881fn create_error_response_schema() -> Value {
2883 let root_schema = schema_for!(ErrorResponse);
2884 let schema_json = serde_json::to_value(root_schema).expect("Valid error schema");
2885
2886 let definitions = schema_json
2888 .get("$defs")
2889 .or_else(|| schema_json.get("definitions"))
2890 .cloned()
2891 .unwrap_or_else(|| json!({}));
2892
2893 let mut result = schema_json.clone();
2895 if let Some(obj) = result.as_object_mut() {
2896 obj.remove("$schema");
2897 obj.remove("$defs");
2898 obj.remove("definitions");
2899 obj.remove("title");
2900 }
2901
2902 inline_refs(&mut result, &definitions);
2904
2905 result
2906}
2907
2908fn inline_refs(schema: &mut Value, definitions: &Value) {
2910 match schema {
2911 Value::Object(obj) => {
2912 if let Some(ref_value) = obj.get("$ref").cloned()
2914 && let Some(ref_str) = ref_value.as_str()
2915 {
2916 let def_name = ref_str
2918 .strip_prefix("#/$defs/")
2919 .or_else(|| ref_str.strip_prefix("#/definitions/"));
2920
2921 if let Some(name) = def_name
2922 && let Some(definition) = definitions.get(name)
2923 {
2924 *schema = definition.clone();
2926 inline_refs(schema, definitions);
2928 return;
2929 }
2930 }
2931
2932 for (_, value) in obj.iter_mut() {
2934 inline_refs(value, definitions);
2935 }
2936 }
2937 Value::Array(arr) => {
2938 for item in arr.iter_mut() {
2940 inline_refs(item, definitions);
2941 }
2942 }
2943 _ => {} }
2945}
2946
2947#[derive(Debug, Clone)]
2949pub struct QueryParameter {
2950 pub value: Value,
2951 pub explode: bool,
2952}
2953
2954impl QueryParameter {
2955 pub fn new(value: Value, explode: bool) -> Self {
2956 Self { value, explode }
2957 }
2958}
2959
2960#[derive(Debug, Clone)]
2962pub struct ExtractedParameters {
2963 pub path: HashMap<String, Value>,
2964 pub query: HashMap<String, QueryParameter>,
2965 pub headers: HashMap<String, Value>,
2966 pub cookies: HashMap<String, Value>,
2967 pub body: HashMap<String, Value>,
2968 pub config: RequestConfig,
2969}
2970
2971#[derive(Debug, Clone)]
2973pub struct RequestConfig {
2974 pub timeout_seconds: u32,
2975 pub content_type: String,
2976}
2977
2978impl Default for RequestConfig {
2979 fn default() -> Self {
2980 Self {
2981 timeout_seconds: 30,
2982 content_type: mime::APPLICATION_JSON.to_string(),
2983 }
2984 }
2985}
2986
2987#[cfg(test)]
2988mod tests {
2989 use super::*;
2990
2991 use insta::assert_json_snapshot;
2992 use oas3::spec::{
2993 BooleanSchema, Components, MediaType, ObjectOrReference, ObjectSchema, Operation,
2994 Parameter, ParameterIn, RequestBody, Schema, SchemaType, SchemaTypeSet, Spec,
2995 };
2996 use rmcp::model::Tool;
2997 use serde_json::{Value, json};
2998 use std::collections::BTreeMap;
2999
3000 fn create_test_spec() -> Spec {
3002 Spec {
3003 openapi: "3.0.0".to_string(),
3004 info: oas3::spec::Info {
3005 title: "Test API".to_string(),
3006 version: "1.0.0".to_string(),
3007 summary: None,
3008 description: Some("Test API for unit tests".to_string()),
3009 terms_of_service: None,
3010 contact: None,
3011 license: None,
3012 extensions: Default::default(),
3013 },
3014 components: Some(Components {
3015 schemas: BTreeMap::new(),
3016 responses: BTreeMap::new(),
3017 parameters: BTreeMap::new(),
3018 examples: BTreeMap::new(),
3019 request_bodies: BTreeMap::new(),
3020 headers: BTreeMap::new(),
3021 security_schemes: BTreeMap::new(),
3022 links: BTreeMap::new(),
3023 callbacks: BTreeMap::new(),
3024 path_items: BTreeMap::new(),
3025 extensions: Default::default(),
3026 }),
3027 servers: vec![],
3028 paths: None,
3029 external_docs: None,
3030 tags: vec![],
3031 security: vec![],
3032 webhooks: BTreeMap::new(),
3033 extensions: Default::default(),
3034 }
3035 }
3036
3037 fn validate_tool_against_mcp_schema(metadata: &ToolMetadata) {
3038 let schema_content = std::fs::read_to_string("schema/2025-06-18/schema.json")
3039 .expect("Failed to read MCP schema file");
3040 let full_schema: Value =
3041 serde_json::from_str(&schema_content).expect("Failed to parse MCP schema JSON");
3042
3043 let tool_schema = json!({
3045 "$schema": "http://json-schema.org/draft-07/schema#",
3046 "definitions": full_schema.get("definitions"),
3047 "$ref": "#/definitions/Tool"
3048 });
3049
3050 let validator =
3051 jsonschema::validator_for(&tool_schema).expect("Failed to compile MCP Tool schema");
3052
3053 let tool = Tool::from(metadata);
3055
3056 let mcp_tool_json = serde_json::to_value(&tool).expect("Failed to serialize Tool to JSON");
3058
3059 let errors: Vec<String> = validator
3061 .iter_errors(&mcp_tool_json)
3062 .map(|e| e.to_string())
3063 .collect();
3064
3065 if !errors.is_empty() {
3066 panic!("Generated tool failed MCP schema validation: {errors:?}");
3067 }
3068 }
3069
3070 #[test]
3071 fn test_error_schema_structure() {
3072 let error_schema = create_error_response_schema();
3073
3074 assert!(error_schema.get("$schema").is_none());
3076 assert!(error_schema.get("definitions").is_none());
3077
3078 assert_json_snapshot!(error_schema);
3080 }
3081
3082 #[test]
3083 fn test_petstore_get_pet_by_id() {
3084 use oas3::spec::Response;
3085
3086 let mut operation = Operation {
3087 operation_id: Some("getPetById".to_string()),
3088 summary: Some("Find pet by ID".to_string()),
3089 description: Some("Returns a single pet".to_string()),
3090 tags: vec![],
3091 external_docs: None,
3092 parameters: vec![],
3093 request_body: None,
3094 responses: Default::default(),
3095 callbacks: Default::default(),
3096 deprecated: Some(false),
3097 security: vec![],
3098 servers: vec![],
3099 extensions: Default::default(),
3100 };
3101
3102 let param = Parameter {
3104 name: "petId".to_string(),
3105 location: ParameterIn::Path,
3106 description: Some("ID of pet to return".to_string()),
3107 required: Some(true),
3108 deprecated: Some(false),
3109 allow_empty_value: Some(false),
3110 style: None,
3111 explode: None,
3112 allow_reserved: Some(false),
3113 schema: Some(ObjectOrReference::Object(ObjectSchema {
3114 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3115 minimum: Some(serde_json::Number::from(1_i64)),
3116 format: Some("int64".to_string()),
3117 ..Default::default()
3118 })),
3119 example: None,
3120 examples: Default::default(),
3121 content: None,
3122 extensions: Default::default(),
3123 };
3124
3125 operation.parameters.push(ObjectOrReference::Object(param));
3126
3127 let mut responses = BTreeMap::new();
3129 let mut content = BTreeMap::new();
3130 content.insert(
3131 "application/json".to_string(),
3132 MediaType {
3133 extensions: Default::default(),
3134 schema: Some(ObjectOrReference::Object(ObjectSchema {
3135 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3136 properties: {
3137 let mut props = BTreeMap::new();
3138 props.insert(
3139 "id".to_string(),
3140 ObjectOrReference::Object(ObjectSchema {
3141 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3142 format: Some("int64".to_string()),
3143 ..Default::default()
3144 }),
3145 );
3146 props.insert(
3147 "name".to_string(),
3148 ObjectOrReference::Object(ObjectSchema {
3149 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3150 ..Default::default()
3151 }),
3152 );
3153 props.insert(
3154 "status".to_string(),
3155 ObjectOrReference::Object(ObjectSchema {
3156 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3157 ..Default::default()
3158 }),
3159 );
3160 props
3161 },
3162 required: vec!["id".to_string(), "name".to_string()],
3163 ..Default::default()
3164 })),
3165 examples: None,
3166 encoding: Default::default(),
3167 },
3168 );
3169
3170 responses.insert(
3171 "200".to_string(),
3172 ObjectOrReference::Object(Response {
3173 description: Some("successful operation".to_string()),
3174 headers: Default::default(),
3175 content,
3176 links: Default::default(),
3177 extensions: Default::default(),
3178 }),
3179 );
3180 operation.responses = Some(responses);
3181
3182 let spec = create_test_spec();
3183 let metadata = ToolGenerator::generate_tool_metadata(
3184 &operation,
3185 "get".to_string(),
3186 "/pet/{petId}".to_string(),
3187 &spec,
3188 false,
3189 false,
3190 )
3191 .unwrap();
3192
3193 assert_eq!(metadata.name, "getPetById");
3194 assert_eq!(metadata.method, "get");
3195 assert_eq!(metadata.path, "/pet/{petId}");
3196 assert!(
3197 metadata
3198 .description
3199 .clone()
3200 .unwrap()
3201 .contains("Find pet by ID")
3202 );
3203
3204 assert!(metadata.output_schema.is_some());
3206 let output_schema = metadata.output_schema.as_ref().unwrap();
3207
3208 insta::assert_json_snapshot!("test_petstore_get_pet_by_id_output_schema", output_schema);
3210
3211 validate_tool_against_mcp_schema(&metadata);
3213 }
3214
3215 #[test]
3216 fn test_convert_prefix_items_to_draft07_mixed_types() {
3217 let prefix_items = vec![
3220 ObjectOrReference::Object(ObjectSchema {
3221 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3222 format: Some("int32".to_string()),
3223 ..Default::default()
3224 }),
3225 ObjectOrReference::Object(ObjectSchema {
3226 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3227 ..Default::default()
3228 }),
3229 ];
3230
3231 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
3233
3234 let mut result = serde_json::Map::new();
3235 let spec = create_test_spec();
3236 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
3237 .unwrap();
3238
3239 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_mixed_types", result);
3241 }
3242
3243 #[test]
3244 fn test_convert_prefix_items_to_draft07_uniform_types() {
3245 let prefix_items = vec![
3247 ObjectOrReference::Object(ObjectSchema {
3248 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3249 ..Default::default()
3250 }),
3251 ObjectOrReference::Object(ObjectSchema {
3252 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3253 ..Default::default()
3254 }),
3255 ];
3256
3257 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
3259
3260 let mut result = serde_json::Map::new();
3261 let spec = create_test_spec();
3262 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
3263 .unwrap();
3264
3265 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_uniform_types", result);
3267 }
3268
3269 #[test]
3270 fn test_array_with_prefix_items_integration() {
3271 let param = Parameter {
3273 name: "coordinates".to_string(),
3274 location: ParameterIn::Query,
3275 description: Some("X,Y coordinates as tuple".to_string()),
3276 required: Some(true),
3277 deprecated: Some(false),
3278 allow_empty_value: Some(false),
3279 style: None,
3280 explode: None,
3281 allow_reserved: Some(false),
3282 schema: Some(ObjectOrReference::Object(ObjectSchema {
3283 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3284 prefix_items: vec![
3285 ObjectOrReference::Object(ObjectSchema {
3286 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
3287 format: Some("double".to_string()),
3288 ..Default::default()
3289 }),
3290 ObjectOrReference::Object(ObjectSchema {
3291 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
3292 format: Some("double".to_string()),
3293 ..Default::default()
3294 }),
3295 ],
3296 items: Some(Box::new(Schema::Boolean(BooleanSchema(false)))),
3297 ..Default::default()
3298 })),
3299 example: None,
3300 examples: Default::default(),
3301 content: None,
3302 extensions: Default::default(),
3303 };
3304
3305 let spec = create_test_spec();
3306 let (result, _annotations) =
3307 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, false)
3308 .unwrap();
3309
3310 insta::assert_json_snapshot!("test_array_with_prefix_items_integration", result);
3312 }
3313
3314 #[test]
3315 fn test_skip_tool_description() {
3316 let operation = Operation {
3317 operation_id: Some("getPetById".to_string()),
3318 summary: Some("Find pet by ID".to_string()),
3319 description: Some("Returns a single pet".to_string()),
3320 tags: vec![],
3321 external_docs: None,
3322 parameters: vec![],
3323 request_body: None,
3324 responses: Default::default(),
3325 callbacks: Default::default(),
3326 deprecated: Some(false),
3327 security: vec![],
3328 servers: vec![],
3329 extensions: Default::default(),
3330 };
3331
3332 let spec = create_test_spec();
3333 let metadata = ToolGenerator::generate_tool_metadata(
3334 &operation,
3335 "get".to_string(),
3336 "/pet/{petId}".to_string(),
3337 &spec,
3338 true,
3339 false,
3340 )
3341 .unwrap();
3342
3343 assert_eq!(metadata.name, "getPetById");
3344 assert_eq!(metadata.method, "get");
3345 assert_eq!(metadata.path, "/pet/{petId}");
3346 assert!(metadata.description.is_none());
3347
3348 insta::assert_json_snapshot!("test_skip_tool_description", metadata);
3350
3351 validate_tool_against_mcp_schema(&metadata);
3353 }
3354
3355 #[test]
3356 fn test_keep_tool_description() {
3357 let description = Some("Returns a single pet".to_string());
3358 let operation = Operation {
3359 operation_id: Some("getPetById".to_string()),
3360 summary: Some("Find pet by ID".to_string()),
3361 description: description.clone(),
3362 tags: vec![],
3363 external_docs: None,
3364 parameters: vec![],
3365 request_body: None,
3366 responses: Default::default(),
3367 callbacks: Default::default(),
3368 deprecated: Some(false),
3369 security: vec![],
3370 servers: vec![],
3371 extensions: Default::default(),
3372 };
3373
3374 let spec = create_test_spec();
3375 let metadata = ToolGenerator::generate_tool_metadata(
3376 &operation,
3377 "get".to_string(),
3378 "/pet/{petId}".to_string(),
3379 &spec,
3380 false,
3381 false,
3382 )
3383 .unwrap();
3384
3385 assert_eq!(metadata.name, "getPetById");
3386 assert_eq!(metadata.method, "get");
3387 assert_eq!(metadata.path, "/pet/{petId}");
3388 assert!(metadata.description.is_some());
3389
3390 insta::assert_json_snapshot!("test_keep_tool_description", metadata);
3392
3393 validate_tool_against_mcp_schema(&metadata);
3395 }
3396
3397 #[test]
3398 fn test_skip_parameter_descriptions() {
3399 let param = Parameter {
3400 name: "status".to_string(),
3401 location: ParameterIn::Query,
3402 description: Some("Filter by status".to_string()),
3403 required: Some(false),
3404 deprecated: Some(false),
3405 allow_empty_value: Some(false),
3406 style: None,
3407 explode: None,
3408 allow_reserved: Some(false),
3409 schema: Some(ObjectOrReference::Object(ObjectSchema {
3410 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3411 enum_values: vec![json!("available"), json!("pending"), json!("sold")],
3412 ..Default::default()
3413 })),
3414 example: Some(json!("available")),
3415 examples: Default::default(),
3416 content: None,
3417 extensions: Default::default(),
3418 };
3419
3420 let spec = create_test_spec();
3421 let (schema, _) =
3422 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, true)
3423 .unwrap();
3424
3425 assert!(schema.get("description").is_none());
3427
3428 assert_eq!(schema.get("type").unwrap(), "string");
3430 assert_eq!(schema.get("example").unwrap(), "available");
3431
3432 insta::assert_json_snapshot!("test_skip_parameter_descriptions", schema);
3433 }
3434
3435 #[test]
3436 fn test_keep_parameter_descriptions() {
3437 let param = Parameter {
3438 name: "status".to_string(),
3439 location: ParameterIn::Query,
3440 description: Some("Filter by status".to_string()),
3441 required: Some(false),
3442 deprecated: Some(false),
3443 allow_empty_value: Some(false),
3444 style: None,
3445 explode: None,
3446 allow_reserved: Some(false),
3447 schema: Some(ObjectOrReference::Object(ObjectSchema {
3448 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3449 enum_values: vec![json!("available"), json!("pending"), json!("sold")],
3450 ..Default::default()
3451 })),
3452 example: Some(json!("available")),
3453 examples: Default::default(),
3454 content: None,
3455 extensions: Default::default(),
3456 };
3457
3458 let spec = create_test_spec();
3459 let (schema, _) =
3460 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, false)
3461 .unwrap();
3462
3463 assert!(schema.get("description").is_some());
3465 let description = schema.get("description").unwrap().as_str().unwrap();
3466 assert!(description.contains("Filter by status"));
3467 assert!(description.contains("Example: `\"available\"`"));
3468
3469 assert_eq!(schema.get("type").unwrap(), "string");
3471 assert_eq!(schema.get("example").unwrap(), "available");
3472
3473 insta::assert_json_snapshot!("test_keep_parameter_descriptions", schema);
3474 }
3475
3476 #[test]
3477 fn test_array_with_regular_items_schema() {
3478 let param = Parameter {
3480 name: "tags".to_string(),
3481 location: ParameterIn::Query,
3482 description: Some("List of tags".to_string()),
3483 required: Some(false),
3484 deprecated: Some(false),
3485 allow_empty_value: Some(false),
3486 style: None,
3487 explode: None,
3488 allow_reserved: Some(false),
3489 schema: Some(ObjectOrReference::Object(ObjectSchema {
3490 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3491 items: Some(Box::new(Schema::Object(Box::new(
3492 ObjectOrReference::Object(ObjectSchema {
3493 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3494 min_length: Some(1),
3495 max_length: Some(50),
3496 ..Default::default()
3497 }),
3498 )))),
3499 ..Default::default()
3500 })),
3501 example: None,
3502 examples: Default::default(),
3503 content: None,
3504 extensions: Default::default(),
3505 };
3506
3507 let spec = create_test_spec();
3508 let (result, _annotations) =
3509 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, false)
3510 .unwrap();
3511
3512 insta::assert_json_snapshot!("test_array_with_regular_items_schema", result);
3514 }
3515
3516 #[test]
3517 fn test_request_body_object_schema() {
3518 let operation = Operation {
3520 operation_id: Some("createPet".to_string()),
3521 summary: Some("Create a new pet".to_string()),
3522 description: Some("Creates a new pet in the store".to_string()),
3523 tags: vec![],
3524 external_docs: None,
3525 parameters: vec![],
3526 request_body: Some(ObjectOrReference::Object(RequestBody {
3527 description: Some("Pet object that needs to be added to the store".to_string()),
3528 content: {
3529 let mut content = BTreeMap::new();
3530 content.insert(
3531 "application/json".to_string(),
3532 MediaType {
3533 extensions: Default::default(),
3534 schema: Some(ObjectOrReference::Object(ObjectSchema {
3535 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3536 ..Default::default()
3537 })),
3538 examples: None,
3539 encoding: Default::default(),
3540 },
3541 );
3542 content
3543 },
3544 required: Some(true),
3545 })),
3546 responses: Default::default(),
3547 callbacks: Default::default(),
3548 deprecated: Some(false),
3549 security: vec![],
3550 servers: vec![],
3551 extensions: Default::default(),
3552 };
3553
3554 let spec = create_test_spec();
3555 let metadata = ToolGenerator::generate_tool_metadata(
3556 &operation,
3557 "post".to_string(),
3558 "/pets".to_string(),
3559 &spec,
3560 false,
3561 false,
3562 )
3563 .unwrap();
3564
3565 let properties = metadata
3567 .parameters
3568 .get("properties")
3569 .unwrap()
3570 .as_object()
3571 .unwrap();
3572 assert!(properties.contains_key("request_body"));
3573
3574 let required = metadata
3576 .parameters
3577 .get("required")
3578 .unwrap()
3579 .as_array()
3580 .unwrap();
3581 assert!(required.contains(&json!("request_body")));
3582
3583 let request_body_schema = properties.get("request_body").unwrap();
3585 insta::assert_json_snapshot!("test_request_body_object_schema", request_body_schema);
3586
3587 validate_tool_against_mcp_schema(&metadata);
3589 }
3590
3591 #[test]
3592 fn test_request_body_array_schema() {
3593 let operation = Operation {
3595 operation_id: Some("createPets".to_string()),
3596 summary: Some("Create multiple pets".to_string()),
3597 description: None,
3598 tags: vec![],
3599 external_docs: None,
3600 parameters: vec![],
3601 request_body: Some(ObjectOrReference::Object(RequestBody {
3602 description: Some("Array of pet objects".to_string()),
3603 content: {
3604 let mut content = BTreeMap::new();
3605 content.insert(
3606 "application/json".to_string(),
3607 MediaType {
3608 extensions: Default::default(),
3609 schema: Some(ObjectOrReference::Object(ObjectSchema {
3610 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3611 items: Some(Box::new(Schema::Object(Box::new(
3612 ObjectOrReference::Object(ObjectSchema {
3613 schema_type: Some(SchemaTypeSet::Single(
3614 SchemaType::Object,
3615 )),
3616 ..Default::default()
3617 }),
3618 )))),
3619 ..Default::default()
3620 })),
3621 examples: None,
3622 encoding: Default::default(),
3623 },
3624 );
3625 content
3626 },
3627 required: Some(false),
3628 })),
3629 responses: Default::default(),
3630 callbacks: Default::default(),
3631 deprecated: Some(false),
3632 security: vec![],
3633 servers: vec![],
3634 extensions: Default::default(),
3635 };
3636
3637 let spec = create_test_spec();
3638 let metadata = ToolGenerator::generate_tool_metadata(
3639 &operation,
3640 "post".to_string(),
3641 "/pets/batch".to_string(),
3642 &spec,
3643 false,
3644 false,
3645 )
3646 .unwrap();
3647
3648 let properties = metadata
3650 .parameters
3651 .get("properties")
3652 .unwrap()
3653 .as_object()
3654 .unwrap();
3655 assert!(properties.contains_key("request_body"));
3656
3657 let required = metadata
3659 .parameters
3660 .get("required")
3661 .unwrap()
3662 .as_array()
3663 .unwrap();
3664 assert!(!required.contains(&json!("request_body")));
3665
3666 let request_body_schema = properties.get("request_body").unwrap();
3668 insta::assert_json_snapshot!("test_request_body_array_schema", request_body_schema);
3669
3670 validate_tool_against_mcp_schema(&metadata);
3672 }
3673
3674 #[test]
3675 fn test_request_body_string_schema() {
3676 let operation = Operation {
3678 operation_id: Some("updatePetName".to_string()),
3679 summary: Some("Update pet name".to_string()),
3680 description: None,
3681 tags: vec![],
3682 external_docs: None,
3683 parameters: vec![],
3684 request_body: Some(ObjectOrReference::Object(RequestBody {
3685 description: None,
3686 content: {
3687 let mut content = BTreeMap::new();
3688 content.insert(
3689 "text/plain".to_string(),
3690 MediaType {
3691 extensions: Default::default(),
3692 schema: Some(ObjectOrReference::Object(ObjectSchema {
3693 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3694 min_length: Some(1),
3695 max_length: Some(100),
3696 ..Default::default()
3697 })),
3698 examples: None,
3699 encoding: Default::default(),
3700 },
3701 );
3702 content
3703 },
3704 required: Some(true),
3705 })),
3706 responses: Default::default(),
3707 callbacks: Default::default(),
3708 deprecated: Some(false),
3709 security: vec![],
3710 servers: vec![],
3711 extensions: Default::default(),
3712 };
3713
3714 let spec = create_test_spec();
3715 let metadata = ToolGenerator::generate_tool_metadata(
3716 &operation,
3717 "put".to_string(),
3718 "/pets/{petId}/name".to_string(),
3719 &spec,
3720 false,
3721 false,
3722 )
3723 .unwrap();
3724
3725 let properties = metadata
3727 .parameters
3728 .get("properties")
3729 .unwrap()
3730 .as_object()
3731 .unwrap();
3732 let request_body_schema = properties.get("request_body").unwrap();
3733 insta::assert_json_snapshot!("test_request_body_string_schema", request_body_schema);
3734
3735 validate_tool_against_mcp_schema(&metadata);
3737 }
3738
3739 #[test]
3740 fn test_request_body_ref_schema() {
3741 let operation = Operation {
3743 operation_id: Some("updatePet".to_string()),
3744 summary: Some("Update existing pet".to_string()),
3745 description: None,
3746 tags: vec![],
3747 external_docs: None,
3748 parameters: vec![],
3749 request_body: Some(ObjectOrReference::Ref {
3750 ref_path: "#/components/requestBodies/PetBody".to_string(),
3751 summary: None,
3752 description: None,
3753 }),
3754 responses: Default::default(),
3755 callbacks: Default::default(),
3756 deprecated: Some(false),
3757 security: vec![],
3758 servers: vec![],
3759 extensions: Default::default(),
3760 };
3761
3762 let spec = create_test_spec();
3763 let metadata = ToolGenerator::generate_tool_metadata(
3764 &operation,
3765 "put".to_string(),
3766 "/pets/{petId}".to_string(),
3767 &spec,
3768 false,
3769 false,
3770 )
3771 .unwrap();
3772
3773 let properties = metadata
3775 .parameters
3776 .get("properties")
3777 .unwrap()
3778 .as_object()
3779 .unwrap();
3780 let request_body_schema = properties.get("request_body").unwrap();
3781 insta::assert_json_snapshot!("test_request_body_ref_schema", request_body_schema);
3782
3783 validate_tool_against_mcp_schema(&metadata);
3785 }
3786
3787 #[test]
3788 fn test_no_request_body_for_get() {
3789 let operation = Operation {
3791 operation_id: Some("listPets".to_string()),
3792 summary: Some("List all pets".to_string()),
3793 description: None,
3794 tags: vec![],
3795 external_docs: None,
3796 parameters: vec![],
3797 request_body: None,
3798 responses: Default::default(),
3799 callbacks: Default::default(),
3800 deprecated: Some(false),
3801 security: vec![],
3802 servers: vec![],
3803 extensions: Default::default(),
3804 };
3805
3806 let spec = create_test_spec();
3807 let metadata = ToolGenerator::generate_tool_metadata(
3808 &operation,
3809 "get".to_string(),
3810 "/pets".to_string(),
3811 &spec,
3812 false,
3813 false,
3814 )
3815 .unwrap();
3816
3817 let properties = metadata
3819 .parameters
3820 .get("properties")
3821 .unwrap()
3822 .as_object()
3823 .unwrap();
3824 assert!(!properties.contains_key("request_body"));
3825
3826 validate_tool_against_mcp_schema(&metadata);
3828 }
3829
3830 #[test]
3831 fn test_request_body_simple_object_with_properties() {
3832 let operation = Operation {
3834 operation_id: Some("updatePetStatus".to_string()),
3835 summary: Some("Update pet status".to_string()),
3836 description: None,
3837 tags: vec![],
3838 external_docs: None,
3839 parameters: vec![],
3840 request_body: Some(ObjectOrReference::Object(RequestBody {
3841 description: Some("Pet status update".to_string()),
3842 content: {
3843 let mut content = BTreeMap::new();
3844 content.insert(
3845 "application/json".to_string(),
3846 MediaType {
3847 extensions: Default::default(),
3848 schema: Some(ObjectOrReference::Object(ObjectSchema {
3849 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3850 properties: {
3851 let mut props = BTreeMap::new();
3852 props.insert(
3853 "status".to_string(),
3854 ObjectOrReference::Object(ObjectSchema {
3855 schema_type: Some(SchemaTypeSet::Single(
3856 SchemaType::String,
3857 )),
3858 ..Default::default()
3859 }),
3860 );
3861 props.insert(
3862 "reason".to_string(),
3863 ObjectOrReference::Object(ObjectSchema {
3864 schema_type: Some(SchemaTypeSet::Single(
3865 SchemaType::String,
3866 )),
3867 ..Default::default()
3868 }),
3869 );
3870 props
3871 },
3872 required: vec!["status".to_string()],
3873 ..Default::default()
3874 })),
3875 examples: None,
3876 encoding: Default::default(),
3877 },
3878 );
3879 content
3880 },
3881 required: Some(false),
3882 })),
3883 responses: Default::default(),
3884 callbacks: Default::default(),
3885 deprecated: Some(false),
3886 security: vec![],
3887 servers: vec![],
3888 extensions: Default::default(),
3889 };
3890
3891 let spec = create_test_spec();
3892 let metadata = ToolGenerator::generate_tool_metadata(
3893 &operation,
3894 "patch".to_string(),
3895 "/pets/{petId}/status".to_string(),
3896 &spec,
3897 false,
3898 false,
3899 )
3900 .unwrap();
3901
3902 let properties = metadata
3904 .parameters
3905 .get("properties")
3906 .unwrap()
3907 .as_object()
3908 .unwrap();
3909 let request_body_schema = properties.get("request_body").unwrap();
3910 insta::assert_json_snapshot!(
3911 "test_request_body_simple_object_with_properties",
3912 request_body_schema
3913 );
3914
3915 let required = metadata
3917 .parameters
3918 .get("required")
3919 .unwrap()
3920 .as_array()
3921 .unwrap();
3922 assert!(!required.contains(&json!("request_body")));
3923
3924 validate_tool_against_mcp_schema(&metadata);
3926 }
3927
3928 #[test]
3929 fn test_request_body_with_nested_properties() {
3930 let operation = Operation {
3932 operation_id: Some("createUser".to_string()),
3933 summary: Some("Create a new user".to_string()),
3934 description: None,
3935 tags: vec![],
3936 external_docs: None,
3937 parameters: vec![],
3938 request_body: Some(ObjectOrReference::Object(RequestBody {
3939 description: Some("User creation data".to_string()),
3940 content: {
3941 let mut content = BTreeMap::new();
3942 content.insert(
3943 "application/json".to_string(),
3944 MediaType {
3945 extensions: Default::default(),
3946 schema: Some(ObjectOrReference::Object(ObjectSchema {
3947 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3948 properties: {
3949 let mut props = BTreeMap::new();
3950 props.insert(
3951 "name".to_string(),
3952 ObjectOrReference::Object(ObjectSchema {
3953 schema_type: Some(SchemaTypeSet::Single(
3954 SchemaType::String,
3955 )),
3956 ..Default::default()
3957 }),
3958 );
3959 props.insert(
3960 "age".to_string(),
3961 ObjectOrReference::Object(ObjectSchema {
3962 schema_type: Some(SchemaTypeSet::Single(
3963 SchemaType::Integer,
3964 )),
3965 minimum: Some(serde_json::Number::from(0)),
3966 maximum: Some(serde_json::Number::from(150)),
3967 ..Default::default()
3968 }),
3969 );
3970 props
3971 },
3972 required: vec!["name".to_string()],
3973 ..Default::default()
3974 })),
3975 examples: None,
3976 encoding: Default::default(),
3977 },
3978 );
3979 content
3980 },
3981 required: Some(true),
3982 })),
3983 responses: Default::default(),
3984 callbacks: Default::default(),
3985 deprecated: Some(false),
3986 security: vec![],
3987 servers: vec![],
3988 extensions: Default::default(),
3989 };
3990
3991 let spec = create_test_spec();
3992 let metadata = ToolGenerator::generate_tool_metadata(
3993 &operation,
3994 "post".to_string(),
3995 "/users".to_string(),
3996 &spec,
3997 false,
3998 false,
3999 )
4000 .unwrap();
4001
4002 let properties = metadata
4004 .parameters
4005 .get("properties")
4006 .unwrap()
4007 .as_object()
4008 .unwrap();
4009 let request_body_schema = properties.get("request_body").unwrap();
4010 insta::assert_json_snapshot!(
4011 "test_request_body_with_nested_properties",
4012 request_body_schema
4013 );
4014
4015 validate_tool_against_mcp_schema(&metadata);
4017 }
4018
4019 #[test]
4020 fn test_operation_without_responses_has_no_output_schema() {
4021 let operation = Operation {
4022 operation_id: Some("testOperation".to_string()),
4023 summary: Some("Test operation".to_string()),
4024 description: None,
4025 tags: vec![],
4026 external_docs: None,
4027 parameters: vec![],
4028 request_body: None,
4029 responses: None,
4030 callbacks: Default::default(),
4031 deprecated: Some(false),
4032 security: vec![],
4033 servers: vec![],
4034 extensions: Default::default(),
4035 };
4036
4037 let spec = create_test_spec();
4038 let metadata = ToolGenerator::generate_tool_metadata(
4039 &operation,
4040 "get".to_string(),
4041 "/test".to_string(),
4042 &spec,
4043 false,
4044 false,
4045 )
4046 .unwrap();
4047
4048 assert!(metadata.output_schema.is_none());
4050
4051 validate_tool_against_mcp_schema(&metadata);
4053 }
4054
4055 #[test]
4056 fn test_extract_output_schema_with_200_response() {
4057 use oas3::spec::Response;
4058
4059 let mut responses = BTreeMap::new();
4061 let mut content = BTreeMap::new();
4062 content.insert(
4063 "application/json".to_string(),
4064 MediaType {
4065 extensions: Default::default(),
4066 schema: Some(ObjectOrReference::Object(ObjectSchema {
4067 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4068 properties: {
4069 let mut props = BTreeMap::new();
4070 props.insert(
4071 "id".to_string(),
4072 ObjectOrReference::Object(ObjectSchema {
4073 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4074 ..Default::default()
4075 }),
4076 );
4077 props.insert(
4078 "name".to_string(),
4079 ObjectOrReference::Object(ObjectSchema {
4080 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4081 ..Default::default()
4082 }),
4083 );
4084 props
4085 },
4086 required: vec!["id".to_string(), "name".to_string()],
4087 ..Default::default()
4088 })),
4089 examples: None,
4090 encoding: Default::default(),
4091 },
4092 );
4093
4094 responses.insert(
4095 "200".to_string(),
4096 ObjectOrReference::Object(Response {
4097 description: Some("Successful response".to_string()),
4098 headers: Default::default(),
4099 content,
4100 links: Default::default(),
4101 extensions: Default::default(),
4102 }),
4103 );
4104
4105 let spec = create_test_spec();
4106 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4107
4108 insta::assert_json_snapshot!(result);
4110 }
4111
4112 #[test]
4113 fn test_extract_output_schema_with_201_response() {
4114 use oas3::spec::Response;
4115
4116 let mut responses = BTreeMap::new();
4118 let mut content = BTreeMap::new();
4119 content.insert(
4120 "application/json".to_string(),
4121 MediaType {
4122 extensions: Default::default(),
4123 schema: Some(ObjectOrReference::Object(ObjectSchema {
4124 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4125 properties: {
4126 let mut props = BTreeMap::new();
4127 props.insert(
4128 "created".to_string(),
4129 ObjectOrReference::Object(ObjectSchema {
4130 schema_type: Some(SchemaTypeSet::Single(SchemaType::Boolean)),
4131 ..Default::default()
4132 }),
4133 );
4134 props
4135 },
4136 ..Default::default()
4137 })),
4138 examples: None,
4139 encoding: Default::default(),
4140 },
4141 );
4142
4143 responses.insert(
4144 "201".to_string(),
4145 ObjectOrReference::Object(Response {
4146 description: Some("Created".to_string()),
4147 headers: Default::default(),
4148 content,
4149 links: Default::default(),
4150 extensions: Default::default(),
4151 }),
4152 );
4153
4154 let spec = create_test_spec();
4155 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4156
4157 insta::assert_json_snapshot!(result);
4159 }
4160
4161 #[test]
4162 fn test_extract_output_schema_with_2xx_response() {
4163 use oas3::spec::Response;
4164
4165 let mut responses = BTreeMap::new();
4167 let mut content = BTreeMap::new();
4168 content.insert(
4169 "application/json".to_string(),
4170 MediaType {
4171 extensions: Default::default(),
4172 schema: Some(ObjectOrReference::Object(ObjectSchema {
4173 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
4174 items: Some(Box::new(Schema::Object(Box::new(
4175 ObjectOrReference::Object(ObjectSchema {
4176 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4177 ..Default::default()
4178 }),
4179 )))),
4180 ..Default::default()
4181 })),
4182 examples: None,
4183 encoding: Default::default(),
4184 },
4185 );
4186
4187 responses.insert(
4188 "2XX".to_string(),
4189 ObjectOrReference::Object(Response {
4190 description: Some("Success".to_string()),
4191 headers: Default::default(),
4192 content,
4193 links: Default::default(),
4194 extensions: Default::default(),
4195 }),
4196 );
4197
4198 let spec = create_test_spec();
4199 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4200
4201 insta::assert_json_snapshot!(result);
4203 }
4204
4205 #[test]
4206 fn test_extract_output_schema_no_responses() {
4207 let spec = create_test_spec();
4208 let result = ToolGenerator::extract_output_schema(&None, &spec).unwrap();
4209
4210 insta::assert_json_snapshot!(result);
4212 }
4213
4214 #[test]
4215 fn test_extract_output_schema_only_error_responses() {
4216 use oas3::spec::Response;
4217
4218 let mut responses = BTreeMap::new();
4220 responses.insert(
4221 "404".to_string(),
4222 ObjectOrReference::Object(Response {
4223 description: Some("Not found".to_string()),
4224 headers: Default::default(),
4225 content: Default::default(),
4226 links: Default::default(),
4227 extensions: Default::default(),
4228 }),
4229 );
4230 responses.insert(
4231 "500".to_string(),
4232 ObjectOrReference::Object(Response {
4233 description: Some("Server error".to_string()),
4234 headers: Default::default(),
4235 content: Default::default(),
4236 links: Default::default(),
4237 extensions: Default::default(),
4238 }),
4239 );
4240
4241 let spec = create_test_spec();
4242 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4243
4244 insta::assert_json_snapshot!(result);
4246 }
4247
4248 #[test]
4249 fn test_extract_output_schema_with_ref() {
4250 use oas3::spec::Response;
4251
4252 let mut spec = create_test_spec();
4254 let mut schemas = BTreeMap::new();
4255 schemas.insert(
4256 "Pet".to_string(),
4257 ObjectOrReference::Object(ObjectSchema {
4258 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4259 properties: {
4260 let mut props = BTreeMap::new();
4261 props.insert(
4262 "name".to_string(),
4263 ObjectOrReference::Object(ObjectSchema {
4264 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4265 ..Default::default()
4266 }),
4267 );
4268 props
4269 },
4270 ..Default::default()
4271 }),
4272 );
4273 spec.components.as_mut().unwrap().schemas = schemas;
4274
4275 let mut responses = BTreeMap::new();
4277 let mut content = BTreeMap::new();
4278 content.insert(
4279 "application/json".to_string(),
4280 MediaType {
4281 extensions: Default::default(),
4282 schema: Some(ObjectOrReference::Ref {
4283 ref_path: "#/components/schemas/Pet".to_string(),
4284 summary: None,
4285 description: None,
4286 }),
4287 examples: None,
4288 encoding: Default::default(),
4289 },
4290 );
4291
4292 responses.insert(
4293 "200".to_string(),
4294 ObjectOrReference::Object(Response {
4295 description: Some("Success".to_string()),
4296 headers: Default::default(),
4297 content,
4298 links: Default::default(),
4299 extensions: Default::default(),
4300 }),
4301 );
4302
4303 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4304
4305 insta::assert_json_snapshot!(result);
4307 }
4308
4309 #[test]
4310 fn test_generate_tool_metadata_includes_output_schema() {
4311 use oas3::spec::Response;
4312
4313 let mut operation = Operation {
4314 operation_id: Some("getPet".to_string()),
4315 summary: Some("Get a pet".to_string()),
4316 description: None,
4317 tags: vec![],
4318 external_docs: None,
4319 parameters: vec![],
4320 request_body: None,
4321 responses: Default::default(),
4322 callbacks: Default::default(),
4323 deprecated: Some(false),
4324 security: vec![],
4325 servers: vec![],
4326 extensions: Default::default(),
4327 };
4328
4329 let mut responses = BTreeMap::new();
4331 let mut content = BTreeMap::new();
4332 content.insert(
4333 "application/json".to_string(),
4334 MediaType {
4335 extensions: Default::default(),
4336 schema: Some(ObjectOrReference::Object(ObjectSchema {
4337 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4338 properties: {
4339 let mut props = BTreeMap::new();
4340 props.insert(
4341 "id".to_string(),
4342 ObjectOrReference::Object(ObjectSchema {
4343 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4344 ..Default::default()
4345 }),
4346 );
4347 props
4348 },
4349 ..Default::default()
4350 })),
4351 examples: None,
4352 encoding: Default::default(),
4353 },
4354 );
4355
4356 responses.insert(
4357 "200".to_string(),
4358 ObjectOrReference::Object(Response {
4359 description: Some("Success".to_string()),
4360 headers: Default::default(),
4361 content,
4362 links: Default::default(),
4363 extensions: Default::default(),
4364 }),
4365 );
4366 operation.responses = Some(responses);
4367
4368 let spec = create_test_spec();
4369 let metadata = ToolGenerator::generate_tool_metadata(
4370 &operation,
4371 "get".to_string(),
4372 "/pets/{id}".to_string(),
4373 &spec,
4374 false,
4375 false,
4376 )
4377 .unwrap();
4378
4379 assert!(metadata.output_schema.is_some());
4381 let output_schema = metadata.output_schema.as_ref().unwrap();
4382
4383 insta::assert_json_snapshot!(
4385 "test_generate_tool_metadata_includes_output_schema",
4386 output_schema
4387 );
4388
4389 validate_tool_against_mcp_schema(&metadata);
4391 }
4392
4393 #[test]
4394 fn test_sanitize_property_name() {
4395 assert_eq!(sanitize_property_name("user name"), "user_name");
4397 assert_eq!(
4398 sanitize_property_name("first name last name"),
4399 "first_name_last_name"
4400 );
4401
4402 assert_eq!(sanitize_property_name("user(admin)"), "user_admin");
4404 assert_eq!(sanitize_property_name("user[admin]"), "user_admin");
4405 assert_eq!(sanitize_property_name("price($)"), "price");
4406 assert_eq!(sanitize_property_name("email@address"), "email_address");
4407 assert_eq!(sanitize_property_name("item#1"), "item_1");
4408 assert_eq!(sanitize_property_name("a/b/c"), "a_b_c");
4409
4410 assert_eq!(sanitize_property_name("user_name"), "user_name");
4412 assert_eq!(sanitize_property_name("userName123"), "userName123");
4413 assert_eq!(sanitize_property_name("user.name"), "user.name");
4414 assert_eq!(sanitize_property_name("user-name"), "user-name");
4415
4416 assert_eq!(sanitize_property_name("123name"), "param_123name");
4418 assert_eq!(sanitize_property_name("1st_place"), "param_1st_place");
4419
4420 assert_eq!(sanitize_property_name(""), "param_");
4422
4423 let long_name = "a".repeat(100);
4425 assert_eq!(sanitize_property_name(&long_name).len(), 64);
4426
4427 assert_eq!(sanitize_property_name("!@#$%^&*()"), "param_");
4430 }
4431
4432 #[test]
4433 fn test_sanitize_property_name_trailing_underscores() {
4434 assert_eq!(sanitize_property_name("page[size]"), "page_size");
4436 assert_eq!(sanitize_property_name("user[id]"), "user_id");
4437 assert_eq!(sanitize_property_name("field[]"), "field");
4438
4439 assert_eq!(sanitize_property_name("field___"), "field");
4441 assert_eq!(sanitize_property_name("test[[["), "test");
4442 }
4443
4444 #[test]
4445 fn test_sanitize_property_name_consecutive_underscores() {
4446 assert_eq!(sanitize_property_name("user__name"), "user_name");
4448 assert_eq!(sanitize_property_name("first___last"), "first_last");
4449 assert_eq!(sanitize_property_name("a____b____c"), "a_b_c");
4450
4451 assert_eq!(sanitize_property_name("user[[name]]"), "user_name");
4453 assert_eq!(sanitize_property_name("field@#$value"), "field_value");
4454 }
4455
4456 #[test]
4457 fn test_sanitize_property_name_edge_cases() {
4458 assert_eq!(sanitize_property_name("_private"), "_private");
4460 assert_eq!(sanitize_property_name("__dunder"), "_dunder");
4461
4462 assert_eq!(sanitize_property_name("[[["), "param_");
4464 assert_eq!(sanitize_property_name("@@@"), "param_");
4465
4466 assert_eq!(sanitize_property_name(""), "param_");
4468
4469 assert_eq!(sanitize_property_name("_field[size]"), "_field_size");
4471 assert_eq!(sanitize_property_name("__test__"), "_test");
4472 }
4473
4474 #[test]
4475 fn test_sanitize_property_name_complex_cases() {
4476 assert_eq!(sanitize_property_name("page[size]"), "page_size");
4478 assert_eq!(sanitize_property_name("filter[status]"), "filter_status");
4479 assert_eq!(
4480 sanitize_property_name("sort[-created_at]"),
4481 "sort_-created_at"
4482 );
4483 assert_eq!(
4484 sanitize_property_name("include[author.posts]"),
4485 "include_author.posts"
4486 );
4487
4488 let long_name = "very_long_field_name_with_special[characters]_that_needs_truncation_____";
4490 let expected = "very_long_field_name_with_special_characters_that_needs_truncat";
4491 assert_eq!(sanitize_property_name(long_name), expected);
4492 }
4493
4494 #[test]
4495 fn test_property_sanitization_with_annotations() {
4496 let spec = create_test_spec();
4497 let mut visited = HashSet::new();
4498
4499 let obj_schema = ObjectSchema {
4501 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4502 properties: {
4503 let mut props = BTreeMap::new();
4504 props.insert(
4506 "user name".to_string(),
4507 ObjectOrReference::Object(ObjectSchema {
4508 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4509 ..Default::default()
4510 }),
4511 );
4512 props.insert(
4514 "price($)".to_string(),
4515 ObjectOrReference::Object(ObjectSchema {
4516 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
4517 ..Default::default()
4518 }),
4519 );
4520 props.insert(
4522 "validName".to_string(),
4523 ObjectOrReference::Object(ObjectSchema {
4524 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4525 ..Default::default()
4526 }),
4527 );
4528 props
4529 },
4530 ..Default::default()
4531 };
4532
4533 let result =
4534 ToolGenerator::convert_object_schema_to_json_schema(&obj_schema, &spec, &mut visited)
4535 .unwrap();
4536
4537 insta::assert_json_snapshot!("test_property_sanitization_with_annotations", result);
4539 }
4540
4541 #[test]
4542 fn test_parameter_sanitization_and_extraction() {
4543 let spec = create_test_spec();
4544
4545 let operation = Operation {
4547 operation_id: Some("testOp".to_string()),
4548 parameters: vec![
4549 ObjectOrReference::Object(Parameter {
4551 name: "user(id)".to_string(),
4552 location: ParameterIn::Path,
4553 description: Some("User ID".to_string()),
4554 required: Some(true),
4555 deprecated: Some(false),
4556 allow_empty_value: Some(false),
4557 style: None,
4558 explode: None,
4559 allow_reserved: Some(false),
4560 schema: Some(ObjectOrReference::Object(ObjectSchema {
4561 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4562 ..Default::default()
4563 })),
4564 example: None,
4565 examples: Default::default(),
4566 content: None,
4567 extensions: Default::default(),
4568 }),
4569 ObjectOrReference::Object(Parameter {
4571 name: "page size".to_string(),
4572 location: ParameterIn::Query,
4573 description: Some("Page size".to_string()),
4574 required: Some(false),
4575 deprecated: Some(false),
4576 allow_empty_value: Some(false),
4577 style: None,
4578 explode: None,
4579 allow_reserved: Some(false),
4580 schema: Some(ObjectOrReference::Object(ObjectSchema {
4581 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4582 ..Default::default()
4583 })),
4584 example: None,
4585 examples: Default::default(),
4586 content: None,
4587 extensions: Default::default(),
4588 }),
4589 ObjectOrReference::Object(Parameter {
4591 name: "auth-token!".to_string(),
4592 location: ParameterIn::Header,
4593 description: Some("Auth token".to_string()),
4594 required: Some(false),
4595 deprecated: Some(false),
4596 allow_empty_value: Some(false),
4597 style: None,
4598 explode: None,
4599 allow_reserved: Some(false),
4600 schema: Some(ObjectOrReference::Object(ObjectSchema {
4601 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4602 ..Default::default()
4603 })),
4604 example: None,
4605 examples: Default::default(),
4606 content: None,
4607 extensions: Default::default(),
4608 }),
4609 ],
4610 ..Default::default()
4611 };
4612
4613 let tool_metadata = ToolGenerator::generate_tool_metadata(
4614 &operation,
4615 "get".to_string(),
4616 "/users/{user(id)}".to_string(),
4617 &spec,
4618 false,
4619 false,
4620 )
4621 .unwrap();
4622
4623 let properties = tool_metadata
4625 .parameters
4626 .get("properties")
4627 .unwrap()
4628 .as_object()
4629 .unwrap();
4630
4631 assert!(properties.contains_key("user_id"));
4632 assert!(properties.contains_key("page_size"));
4633 assert!(properties.contains_key("header_auth-token"));
4634
4635 let required = tool_metadata
4637 .parameters
4638 .get("required")
4639 .unwrap()
4640 .as_array()
4641 .unwrap();
4642 assert!(required.contains(&json!("user_id")));
4643
4644 let arguments = json!({
4646 "user_id": "123",
4647 "page_size": 10,
4648 "header_auth-token": "secret"
4649 });
4650
4651 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
4652
4653 assert_eq!(extracted.path.get("user(id)"), Some(&json!("123")));
4655
4656 assert_eq!(
4658 extracted.query.get("page size").map(|q| &q.value),
4659 Some(&json!(10))
4660 );
4661
4662 assert_eq!(extracted.headers.get("auth-token!"), Some(&json!("secret")));
4664 }
4665
4666 #[test]
4667 fn test_check_unknown_parameters() {
4668 let mut properties = serde_json::Map::new();
4670 properties.insert("page_size".to_string(), json!({"type": "integer"}));
4671 properties.insert("user_id".to_string(), json!({"type": "string"}));
4672
4673 let mut args = serde_json::Map::new();
4674 args.insert("page_sixe".to_string(), json!(10)); let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4677 assert!(!result.is_empty());
4678 assert_eq!(result.len(), 1);
4679
4680 match &result[0] {
4681 ValidationError::InvalidParameter {
4682 parameter,
4683 suggestions,
4684 valid_parameters,
4685 } => {
4686 assert_eq!(parameter, "page_sixe");
4687 assert_eq!(suggestions, &vec!["page_size".to_string()]);
4688 assert_eq!(
4689 valid_parameters,
4690 &vec!["page_size".to_string(), "user_id".to_string()]
4691 );
4692 }
4693 _ => panic!("Expected InvalidParameter variant"),
4694 }
4695 }
4696
4697 #[test]
4698 fn test_check_unknown_parameters_no_suggestions() {
4699 let mut properties = serde_json::Map::new();
4701 properties.insert("limit".to_string(), json!({"type": "integer"}));
4702 properties.insert("offset".to_string(), json!({"type": "integer"}));
4703
4704 let mut args = serde_json::Map::new();
4705 args.insert("xyz123".to_string(), json!("value"));
4706
4707 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4708 assert!(!result.is_empty());
4709 assert_eq!(result.len(), 1);
4710
4711 match &result[0] {
4712 ValidationError::InvalidParameter {
4713 parameter,
4714 suggestions,
4715 valid_parameters,
4716 } => {
4717 assert_eq!(parameter, "xyz123");
4718 assert!(suggestions.is_empty());
4719 assert!(valid_parameters.contains(&"limit".to_string()));
4720 assert!(valid_parameters.contains(&"offset".to_string()));
4721 }
4722 _ => panic!("Expected InvalidParameter variant"),
4723 }
4724 }
4725
4726 #[test]
4727 fn test_check_unknown_parameters_multiple_suggestions() {
4728 let mut properties = serde_json::Map::new();
4730 properties.insert("user_id".to_string(), json!({"type": "string"}));
4731 properties.insert("user_iid".to_string(), json!({"type": "string"}));
4732 properties.insert("user_name".to_string(), json!({"type": "string"}));
4733
4734 let mut args = serde_json::Map::new();
4735 args.insert("usr_id".to_string(), json!("123"));
4736
4737 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4738 assert!(!result.is_empty());
4739 assert_eq!(result.len(), 1);
4740
4741 match &result[0] {
4742 ValidationError::InvalidParameter {
4743 parameter,
4744 suggestions,
4745 valid_parameters,
4746 } => {
4747 assert_eq!(parameter, "usr_id");
4748 assert!(!suggestions.is_empty());
4749 assert!(suggestions.contains(&"user_id".to_string()));
4750 assert_eq!(valid_parameters.len(), 3);
4751 }
4752 _ => panic!("Expected InvalidParameter variant"),
4753 }
4754 }
4755
4756 #[test]
4757 fn test_check_unknown_parameters_valid() {
4758 let mut properties = serde_json::Map::new();
4760 properties.insert("name".to_string(), json!({"type": "string"}));
4761 properties.insert("email".to_string(), json!({"type": "string"}));
4762
4763 let mut args = serde_json::Map::new();
4764 args.insert("name".to_string(), json!("John"));
4765 args.insert("email".to_string(), json!("john@example.com"));
4766
4767 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4768 assert!(result.is_empty());
4769 }
4770
4771 #[test]
4772 fn test_check_unknown_parameters_empty() {
4773 let properties = serde_json::Map::new();
4775
4776 let mut args = serde_json::Map::new();
4777 args.insert("any_param".to_string(), json!("value"));
4778
4779 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4780 assert!(!result.is_empty());
4781 assert_eq!(result.len(), 1);
4782
4783 match &result[0] {
4784 ValidationError::InvalidParameter {
4785 parameter,
4786 suggestions,
4787 valid_parameters,
4788 } => {
4789 assert_eq!(parameter, "any_param");
4790 assert!(suggestions.is_empty());
4791 assert!(valid_parameters.is_empty());
4792 }
4793 _ => panic!("Expected InvalidParameter variant"),
4794 }
4795 }
4796
4797 #[test]
4798 fn test_check_unknown_parameters_gltf_pagination() {
4799 let mut properties = serde_json::Map::new();
4801 properties.insert(
4802 "page_number".to_string(),
4803 json!({
4804 "type": "integer",
4805 "x-original-name": "page[number]"
4806 }),
4807 );
4808 properties.insert(
4809 "page_size".to_string(),
4810 json!({
4811 "type": "integer",
4812 "x-original-name": "page[size]"
4813 }),
4814 );
4815
4816 let mut args = serde_json::Map::new();
4818 args.insert("page".to_string(), json!(1));
4819 args.insert("per_page".to_string(), json!(10));
4820
4821 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4822 assert_eq!(result.len(), 2, "Should have 2 unknown parameters");
4823
4824 let page_error = result
4826 .iter()
4827 .find(|e| {
4828 if let ValidationError::InvalidParameter { parameter, .. } = e {
4829 parameter == "page"
4830 } else {
4831 false
4832 }
4833 })
4834 .expect("Should have error for 'page'");
4835
4836 let per_page_error = result
4837 .iter()
4838 .find(|e| {
4839 if let ValidationError::InvalidParameter { parameter, .. } = e {
4840 parameter == "per_page"
4841 } else {
4842 false
4843 }
4844 })
4845 .expect("Should have error for 'per_page'");
4846
4847 match page_error {
4849 ValidationError::InvalidParameter {
4850 suggestions,
4851 valid_parameters,
4852 ..
4853 } => {
4854 assert!(
4855 suggestions.contains(&"page_number".to_string()),
4856 "Should suggest 'page_number' for 'page'"
4857 );
4858 assert_eq!(valid_parameters.len(), 2);
4859 assert!(valid_parameters.contains(&"page_number".to_string()));
4860 assert!(valid_parameters.contains(&"page_size".to_string()));
4861 }
4862 _ => panic!("Expected InvalidParameter"),
4863 }
4864
4865 match per_page_error {
4867 ValidationError::InvalidParameter {
4868 parameter,
4869 suggestions,
4870 valid_parameters,
4871 ..
4872 } => {
4873 assert_eq!(parameter, "per_page");
4874 assert_eq!(valid_parameters.len(), 2);
4875 if !suggestions.is_empty() {
4878 assert!(suggestions.contains(&"page_size".to_string()));
4879 }
4880 }
4881 _ => panic!("Expected InvalidParameter"),
4882 }
4883 }
4884
4885 #[test]
4886 fn test_validate_parameters_with_invalid_params() {
4887 let tool_metadata = ToolMetadata {
4889 name: "listItems".to_string(),
4890 title: None,
4891 description: Some("List items".to_string()),
4892 parameters: json!({
4893 "type": "object",
4894 "properties": {
4895 "page_number": {
4896 "type": "integer",
4897 "x-original-name": "page[number]"
4898 },
4899 "page_size": {
4900 "type": "integer",
4901 "x-original-name": "page[size]"
4902 }
4903 },
4904 "required": []
4905 }),
4906 output_schema: None,
4907 method: "GET".to_string(),
4908 path: "/items".to_string(),
4909 security: None,
4910 parameter_mappings: std::collections::HashMap::new(),
4911 };
4912
4913 let arguments = json!({
4915 "page": 1,
4916 "per_page": 10
4917 });
4918
4919 let result = ToolGenerator::validate_parameters(&tool_metadata, &arguments);
4920 assert!(
4921 result.is_err(),
4922 "Should fail validation with unknown parameters"
4923 );
4924
4925 let error = result.unwrap_err();
4926 match error {
4927 ToolCallValidationError::InvalidParameters { violations } => {
4928 assert_eq!(violations.len(), 2, "Should have 2 validation errors");
4929
4930 let has_page_error = violations.iter().any(|v| {
4932 if let ValidationError::InvalidParameter { parameter, .. } = v {
4933 parameter == "page"
4934 } else {
4935 false
4936 }
4937 });
4938
4939 let has_per_page_error = violations.iter().any(|v| {
4940 if let ValidationError::InvalidParameter { parameter, .. } = v {
4941 parameter == "per_page"
4942 } else {
4943 false
4944 }
4945 });
4946
4947 assert!(has_page_error, "Should have error for 'page' parameter");
4948 assert!(
4949 has_per_page_error,
4950 "Should have error for 'per_page' parameter"
4951 );
4952 }
4953 _ => panic!("Expected InvalidParameters"),
4954 }
4955 }
4956
4957 #[test]
4958 fn test_cookie_parameter_sanitization() {
4959 let spec = create_test_spec();
4960
4961 let operation = Operation {
4962 operation_id: Some("testCookie".to_string()),
4963 parameters: vec![ObjectOrReference::Object(Parameter {
4964 name: "session[id]".to_string(),
4965 location: ParameterIn::Cookie,
4966 description: Some("Session ID".to_string()),
4967 required: Some(false),
4968 deprecated: Some(false),
4969 allow_empty_value: Some(false),
4970 style: None,
4971 explode: None,
4972 allow_reserved: Some(false),
4973 schema: Some(ObjectOrReference::Object(ObjectSchema {
4974 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4975 ..Default::default()
4976 })),
4977 example: None,
4978 examples: Default::default(),
4979 content: None,
4980 extensions: Default::default(),
4981 })],
4982 ..Default::default()
4983 };
4984
4985 let tool_metadata = ToolGenerator::generate_tool_metadata(
4986 &operation,
4987 "get".to_string(),
4988 "/data".to_string(),
4989 &spec,
4990 false,
4991 false,
4992 )
4993 .unwrap();
4994
4995 let properties = tool_metadata
4996 .parameters
4997 .get("properties")
4998 .unwrap()
4999 .as_object()
5000 .unwrap();
5001
5002 assert!(properties.contains_key("cookie_session_id"));
5004
5005 let arguments = json!({
5007 "cookie_session_id": "abc123"
5008 });
5009
5010 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
5011
5012 assert_eq!(extracted.cookies.get("session[id]"), Some(&json!("abc123")));
5014 }
5015
5016 #[test]
5017 fn test_parameter_description_with_examples() {
5018 let spec = create_test_spec();
5019
5020 let param_with_example = Parameter {
5022 name: "status".to_string(),
5023 location: ParameterIn::Query,
5024 description: Some("Filter by status".to_string()),
5025 required: Some(false),
5026 deprecated: Some(false),
5027 allow_empty_value: Some(false),
5028 style: None,
5029 explode: None,
5030 allow_reserved: Some(false),
5031 schema: Some(ObjectOrReference::Object(ObjectSchema {
5032 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5033 ..Default::default()
5034 })),
5035 example: Some(json!("active")),
5036 examples: Default::default(),
5037 content: None,
5038 extensions: Default::default(),
5039 };
5040
5041 let (schema, _) = ToolGenerator::convert_parameter_schema(
5042 ¶m_with_example,
5043 ParameterIn::Query,
5044 &spec,
5045 false,
5046 )
5047 .unwrap();
5048 let description = schema.get("description").unwrap().as_str().unwrap();
5049 assert_eq!(description, "Filter by status. Example: `\"active\"`");
5050
5051 let mut examples_map = std::collections::BTreeMap::new();
5053 examples_map.insert(
5054 "example1".to_string(),
5055 ObjectOrReference::Object(oas3::spec::Example {
5056 value: Some(json!("pending")),
5057 ..Default::default()
5058 }),
5059 );
5060 examples_map.insert(
5061 "example2".to_string(),
5062 ObjectOrReference::Object(oas3::spec::Example {
5063 value: Some(json!("completed")),
5064 ..Default::default()
5065 }),
5066 );
5067
5068 let param_with_examples = Parameter {
5069 name: "status".to_string(),
5070 location: ParameterIn::Query,
5071 description: Some("Filter by status".to_string()),
5072 required: Some(false),
5073 deprecated: Some(false),
5074 allow_empty_value: Some(false),
5075 style: None,
5076 explode: None,
5077 allow_reserved: Some(false),
5078 schema: Some(ObjectOrReference::Object(ObjectSchema {
5079 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5080 ..Default::default()
5081 })),
5082 example: None,
5083 examples: examples_map,
5084 content: None,
5085 extensions: Default::default(),
5086 };
5087
5088 let (schema, _) = ToolGenerator::convert_parameter_schema(
5089 ¶m_with_examples,
5090 ParameterIn::Query,
5091 &spec,
5092 false,
5093 )
5094 .unwrap();
5095 let description = schema.get("description").unwrap().as_str().unwrap();
5096 assert!(description.starts_with("Filter by status. Examples:\n"));
5097 assert!(description.contains("`\"pending\"`"));
5098 assert!(description.contains("`\"completed\"`"));
5099
5100 let param_no_desc = Parameter {
5102 name: "limit".to_string(),
5103 location: ParameterIn::Query,
5104 description: None,
5105 required: Some(false),
5106 deprecated: Some(false),
5107 allow_empty_value: Some(false),
5108 style: None,
5109 explode: None,
5110 allow_reserved: Some(false),
5111 schema: Some(ObjectOrReference::Object(ObjectSchema {
5112 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
5113 ..Default::default()
5114 })),
5115 example: Some(json!(100)),
5116 examples: Default::default(),
5117 content: None,
5118 extensions: Default::default(),
5119 };
5120
5121 let (schema, _) = ToolGenerator::convert_parameter_schema(
5122 ¶m_no_desc,
5123 ParameterIn::Query,
5124 &spec,
5125 false,
5126 )
5127 .unwrap();
5128 let description = schema.get("description").unwrap().as_str().unwrap();
5129 assert_eq!(description, "limit parameter. Example: `100`");
5130 }
5131
5132 #[test]
5133 fn test_format_examples_for_description() {
5134 let examples = vec![json!("active")];
5136 let result = ToolGenerator::format_examples_for_description(&examples);
5137 assert_eq!(result, Some("Example: `\"active\"`".to_string()));
5138
5139 let examples = vec![json!(42)];
5141 let result = ToolGenerator::format_examples_for_description(&examples);
5142 assert_eq!(result, Some("Example: `42`".to_string()));
5143
5144 let examples = vec![json!(true)];
5146 let result = ToolGenerator::format_examples_for_description(&examples);
5147 assert_eq!(result, Some("Example: `true`".to_string()));
5148
5149 let examples = vec![json!("active"), json!("pending"), json!("completed")];
5151 let result = ToolGenerator::format_examples_for_description(&examples);
5152 assert_eq!(
5153 result,
5154 Some("Examples:\n- `\"active\"`\n- `\"pending\"`\n- `\"completed\"`".to_string())
5155 );
5156
5157 let examples = vec![json!(["a", "b", "c"])];
5159 let result = ToolGenerator::format_examples_for_description(&examples);
5160 assert_eq!(result, Some("Example: `[\"a\",\"b\",\"c\"]`".to_string()));
5161
5162 let examples = vec![json!({"key": "value"})];
5164 let result = ToolGenerator::format_examples_for_description(&examples);
5165 assert_eq!(result, Some("Example: `{\"key\":\"value\"}`".to_string()));
5166
5167 let examples = vec![];
5169 let result = ToolGenerator::format_examples_for_description(&examples);
5170 assert_eq!(result, None);
5171
5172 let examples = vec![json!(null)];
5174 let result = ToolGenerator::format_examples_for_description(&examples);
5175 assert_eq!(result, Some("Example: `null`".to_string()));
5176
5177 let examples = vec![json!("text"), json!(123), json!(true)];
5179 let result = ToolGenerator::format_examples_for_description(&examples);
5180 assert_eq!(
5181 result,
5182 Some("Examples:\n- `\"text\"`\n- `123`\n- `true`".to_string())
5183 );
5184
5185 let examples = vec![json!(["a", "b", "c", "d", "e", "f"])];
5187 let result = ToolGenerator::format_examples_for_description(&examples);
5188 assert_eq!(
5189 result,
5190 Some("Example: `[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\"]`".to_string())
5191 );
5192
5193 let examples = vec![json!([1, 2])];
5195 let result = ToolGenerator::format_examples_for_description(&examples);
5196 assert_eq!(result, Some("Example: `[1,2]`".to_string()));
5197
5198 let examples = vec![json!({"user": {"name": "John", "age": 30}})];
5200 let result = ToolGenerator::format_examples_for_description(&examples);
5201 assert_eq!(
5202 result,
5203 Some("Example: `{\"user\":{\"name\":\"John\",\"age\":30}}`".to_string())
5204 );
5205
5206 let examples = vec![json!("a"), json!("b"), json!("c"), json!("d"), json!("e")];
5208 let result = ToolGenerator::format_examples_for_description(&examples);
5209 assert_eq!(
5210 result,
5211 Some("Examples:\n- `\"a\"`\n- `\"b\"`\n- `\"c\"`\n- `\"d\"`\n- `\"e\"`".to_string())
5212 );
5213
5214 let examples = vec![json!(3.5)];
5216 let result = ToolGenerator::format_examples_for_description(&examples);
5217 assert_eq!(result, Some("Example: `3.5`".to_string()));
5218
5219 let examples = vec![json!(-42)];
5221 let result = ToolGenerator::format_examples_for_description(&examples);
5222 assert_eq!(result, Some("Example: `-42`".to_string()));
5223
5224 let examples = vec![json!(false)];
5226 let result = ToolGenerator::format_examples_for_description(&examples);
5227 assert_eq!(result, Some("Example: `false`".to_string()));
5228
5229 let examples = vec![json!("hello \"world\"")];
5231 let result = ToolGenerator::format_examples_for_description(&examples);
5232 assert_eq!(result, Some(r#"Example: `"hello \"world\""`"#.to_string()));
5234
5235 let examples = vec![json!("")];
5237 let result = ToolGenerator::format_examples_for_description(&examples);
5238 assert_eq!(result, Some("Example: `\"\"`".to_string()));
5239
5240 let examples = vec![json!([])];
5242 let result = ToolGenerator::format_examples_for_description(&examples);
5243 assert_eq!(result, Some("Example: `[]`".to_string()));
5244
5245 let examples = vec![json!({})];
5247 let result = ToolGenerator::format_examples_for_description(&examples);
5248 assert_eq!(result, Some("Example: `{}`".to_string()));
5249 }
5250
5251 #[test]
5252 fn test_reference_metadata_functionality() {
5253 let metadata = ReferenceMetadata::new(
5255 Some("User Reference".to_string()),
5256 Some("A reference to user data with additional context".to_string()),
5257 );
5258
5259 assert!(!metadata.is_empty());
5260 assert_eq!(metadata.summary(), Some("User Reference"));
5261 assert_eq!(
5262 metadata.best_description(),
5263 Some("A reference to user data with additional context")
5264 );
5265
5266 let summary_only = ReferenceMetadata::new(Some("Pet Summary".to_string()), None);
5268 assert_eq!(summary_only.best_description(), Some("Pet Summary"));
5269
5270 let empty_metadata = ReferenceMetadata::new(None, None);
5272 assert!(empty_metadata.is_empty());
5273 assert_eq!(empty_metadata.best_description(), None);
5274
5275 let metadata = ReferenceMetadata::new(
5277 Some("Reference Summary".to_string()),
5278 Some("Reference Description".to_string()),
5279 );
5280
5281 let result = metadata.merge_with_description(None, false);
5283 assert_eq!(result, Some("Reference Description".to_string()));
5284
5285 let result = metadata.merge_with_description(Some("Existing desc"), false);
5287 assert_eq!(result, Some("Reference Description".to_string()));
5288
5289 let result = metadata.merge_with_description(Some("Existing desc"), true);
5291 assert_eq!(result, Some("Reference Description".to_string()));
5292
5293 let result = metadata.enhance_parameter_description("userId", Some("User ID parameter"));
5295 assert_eq!(result, Some("userId: Reference Description".to_string()));
5296
5297 let result = metadata.enhance_parameter_description("userId", None);
5298 assert_eq!(result, Some("userId: Reference Description".to_string()));
5299
5300 let summary_only = ReferenceMetadata::new(Some("API Token".to_string()), None);
5302
5303 let result = summary_only.merge_with_description(Some("Generic token"), false);
5304 assert_eq!(result, Some("API Token".to_string()));
5305
5306 let result = summary_only.merge_with_description(Some("Different desc"), true);
5307 assert_eq!(result, Some("API Token".to_string())); let result = summary_only.enhance_parameter_description("token", Some("Token field"));
5310 assert_eq!(result, Some("token: API Token".to_string()));
5311
5312 let empty_meta = ReferenceMetadata::new(None, None);
5314
5315 let result = empty_meta.merge_with_description(Some("Schema description"), false);
5316 assert_eq!(result, Some("Schema description".to_string()));
5317
5318 let result = empty_meta.enhance_parameter_description("param", Some("Schema param"));
5319 assert_eq!(result, Some("Schema param".to_string()));
5320
5321 let result = empty_meta.enhance_parameter_description("param", None);
5322 assert_eq!(result, Some("param parameter".to_string()));
5323 }
5324
5325 #[test]
5326 fn test_parameter_schema_with_reference_metadata() {
5327 let mut spec = create_test_spec();
5328
5329 spec.components.as_mut().unwrap().schemas.insert(
5331 "Pet".to_string(),
5332 ObjectOrReference::Object(ObjectSchema {
5333 description: None, schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5335 ..Default::default()
5336 }),
5337 );
5338
5339 let param_with_ref = Parameter {
5341 name: "user".to_string(),
5342 location: ParameterIn::Query,
5343 description: None,
5344 required: Some(true),
5345 deprecated: Some(false),
5346 allow_empty_value: Some(false),
5347 style: None,
5348 explode: None,
5349 allow_reserved: Some(false),
5350 schema: Some(ObjectOrReference::Ref {
5351 ref_path: "#/components/schemas/Pet".to_string(),
5352 summary: Some("Pet Reference".to_string()),
5353 description: Some("A reference to pet schema with additional context".to_string()),
5354 }),
5355 example: None,
5356 examples: BTreeMap::new(),
5357 content: None,
5358 extensions: Default::default(),
5359 };
5360
5361 let result = ToolGenerator::convert_parameter_schema(
5363 ¶m_with_ref,
5364 ParameterIn::Query,
5365 &spec,
5366 false,
5367 );
5368
5369 assert!(result.is_ok());
5370 let (schema, _annotations) = result.unwrap();
5371
5372 let description = schema.get("description").and_then(|v| v.as_str());
5374 assert!(description.is_some());
5375 assert!(
5377 description.unwrap().contains("Pet Reference")
5378 || description
5379 .unwrap()
5380 .contains("A reference to pet schema with additional context")
5381 );
5382 }
5383
5384 #[test]
5385 fn test_request_body_with_reference_metadata() {
5386 let spec = create_test_spec();
5387
5388 let request_body_ref = ObjectOrReference::Ref {
5390 ref_path: "#/components/requestBodies/PetBody".to_string(),
5391 summary: Some("Pet Request Body".to_string()),
5392 description: Some(
5393 "Request body containing pet information for API operations".to_string(),
5394 ),
5395 };
5396
5397 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body_ref, &spec);
5398
5399 assert!(result.is_ok());
5400 let schema_result = result.unwrap();
5401 assert!(schema_result.is_some());
5402
5403 let (schema, _annotations, _required) = schema_result.unwrap();
5404 let description = schema.get("description").and_then(|v| v.as_str());
5405
5406 assert!(description.is_some());
5407 assert_eq!(
5409 description.unwrap(),
5410 "Request body containing pet information for API operations"
5411 );
5412 }
5413
5414 #[test]
5415 fn test_response_schema_with_reference_metadata() {
5416 let spec = create_test_spec();
5417
5418 let mut responses = BTreeMap::new();
5420 responses.insert(
5421 "200".to_string(),
5422 ObjectOrReference::Ref {
5423 ref_path: "#/components/responses/PetResponse".to_string(),
5424 summary: Some("Successful Pet Response".to_string()),
5425 description: Some(
5426 "Response containing pet data on successful operation".to_string(),
5427 ),
5428 },
5429 );
5430 let responses_option = Some(responses);
5431
5432 let result = ToolGenerator::extract_output_schema(&responses_option, &spec);
5433
5434 assert!(result.is_ok());
5435 let schema = result.unwrap();
5436 assert!(schema.is_some());
5437
5438 let schema_value = schema.unwrap();
5439 let body_desc = schema_value
5440 .get("properties")
5441 .and_then(|props| props.get("body"))
5442 .and_then(|body| body.get("description"))
5443 .and_then(|desc| desc.as_str());
5444
5445 assert!(body_desc.is_some());
5446 assert_eq!(
5448 body_desc.unwrap(),
5449 "Response containing pet data on successful operation"
5450 );
5451 }
5452
5453 #[test]
5454 fn test_self_referencing_schema_does_not_overflow() {
5455 let mut spec = create_test_spec();
5458
5459 let node_schema = ObjectSchema {
5461 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5462 properties: {
5463 let mut props = BTreeMap::new();
5464 props.insert(
5465 "name".to_string(),
5466 ObjectOrReference::Object(ObjectSchema {
5467 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5468 ..Default::default()
5469 }),
5470 );
5471 props.insert(
5473 "children".to_string(),
5474 ObjectOrReference::Object(ObjectSchema {
5475 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
5476 items: Some(Box::new(Schema::Object(Box::new(ObjectOrReference::Ref {
5477 ref_path: "#/components/schemas/Node".to_string(),
5478 summary: None,
5479 description: None,
5480 })))),
5481 ..Default::default()
5482 }),
5483 );
5484 props
5485 },
5486 ..Default::default()
5487 };
5488
5489 if let Some(ref mut components) = spec.components {
5491 components
5492 .schemas
5493 .insert("Node".to_string(), ObjectOrReference::Object(node_schema));
5494 }
5495
5496 let mut visited = HashSet::new();
5498 let result = ToolGenerator::convert_schema_to_json_schema(
5499 &Schema::Object(Box::new(ObjectOrReference::Ref {
5500 ref_path: "#/components/schemas/Node".to_string(),
5501 summary: None,
5502 description: None,
5503 })),
5504 &spec,
5505 &mut visited,
5506 );
5507
5508 assert!(
5510 result.is_err(),
5511 "Expected circular reference error, got: {result:?}"
5512 );
5513 let error = result.unwrap_err();
5514 assert!(
5515 error.to_string().contains("Circular reference"),
5516 "Expected circular reference error message, got: {error}"
5517 );
5518 }
5519
5520 #[test]
5523 fn test_multipart_form_data_with_single_file() {
5524 let request_body = ObjectOrReference::Object(RequestBody {
5527 description: Some("File upload request".to_string()),
5528 content: {
5529 let mut content = BTreeMap::new();
5530 content.insert(
5531 "multipart/form-data".to_string(),
5532 MediaType {
5533 extensions: Default::default(),
5534 schema: Some(ObjectOrReference::Object(ObjectSchema {
5535 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5536 properties: {
5537 let mut props = BTreeMap::new();
5538 props.insert(
5539 "file".to_string(),
5540 ObjectOrReference::Object(ObjectSchema {
5541 schema_type: Some(SchemaTypeSet::Single(
5542 SchemaType::String,
5543 )),
5544 format: Some("binary".to_string()),
5545 description: Some("The file to upload".to_string()),
5546 ..Default::default()
5547 }),
5548 );
5549 props
5550 },
5551 required: vec!["file".to_string()],
5552 ..Default::default()
5553 })),
5554 examples: None,
5555 encoding: Default::default(),
5556 },
5557 );
5558 content
5559 },
5560 required: Some(true),
5561 });
5562
5563 let spec = create_test_spec();
5564 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
5565 .unwrap()
5566 .unwrap();
5567
5568 let (schema, annotations, is_required) = result;
5569
5570 let schema_obj = schema.as_object().unwrap();
5572 assert_eq!(schema_obj.get("type").unwrap(), "object");
5573
5574 let file_schema = schema_obj.get("properties").unwrap().get("file").unwrap();
5576
5577 assert_eq!(file_schema.get("type").unwrap(), "object");
5579 assert!(
5580 file_schema
5581 .get("properties")
5582 .unwrap()
5583 .get("content")
5584 .is_some()
5585 );
5586 assert!(
5587 file_schema
5588 .get("properties")
5589 .unwrap()
5590 .get("filename")
5591 .is_some()
5592 );
5593 assert!(
5594 file_schema
5595 .get("required")
5596 .unwrap()
5597 .as_array()
5598 .unwrap()
5599 .contains(&json!("content"))
5600 );
5601
5602 let annotations_value = serde_json::to_value(&annotations).unwrap();
5604 let annotations_obj = annotations_value.as_object().unwrap();
5605
5606 assert_eq!(
5608 annotations_obj.get("x-content-type").unwrap(),
5609 "multipart/form-data"
5610 );
5611
5612 let x_file_fields = annotations_obj
5614 .get("x-file-fields")
5615 .unwrap()
5616 .as_array()
5617 .unwrap();
5618 assert_eq!(x_file_fields.len(), 1);
5619 assert!(x_file_fields.contains(&json!("file")));
5620
5621 assert!(is_required);
5623
5624 insta::assert_json_snapshot!("test_multipart_form_data_with_single_file", schema);
5626 }
5627
5628 #[test]
5629 fn test_multipart_form_data_with_multiple_files() {
5630 let request_body = ObjectOrReference::Object(RequestBody {
5632 description: Some("Multiple file upload request".to_string()),
5633 content: {
5634 let mut content = BTreeMap::new();
5635 content.insert(
5636 "multipart/form-data".to_string(),
5637 MediaType {
5638 extensions: Default::default(),
5639 schema: Some(ObjectOrReference::Object(ObjectSchema {
5640 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5641 properties: {
5642 let mut props = BTreeMap::new();
5643 props.insert(
5644 "avatar".to_string(),
5645 ObjectOrReference::Object(ObjectSchema {
5646 schema_type: Some(SchemaTypeSet::Single(
5647 SchemaType::String,
5648 )),
5649 format: Some("binary".to_string()),
5650 description: Some("Profile avatar image".to_string()),
5651 ..Default::default()
5652 }),
5653 );
5654 props.insert(
5655 "document".to_string(),
5656 ObjectOrReference::Object(ObjectSchema {
5657 schema_type: Some(SchemaTypeSet::Single(
5658 SchemaType::String,
5659 )),
5660 format: Some("binary".to_string()),
5661 description: Some("Supporting document".to_string()),
5662 ..Default::default()
5663 }),
5664 );
5665 props.insert(
5666 "resume".to_string(),
5667 ObjectOrReference::Object(ObjectSchema {
5668 schema_type: Some(SchemaTypeSet::Single(
5669 SchemaType::String,
5670 )),
5671 format: Some("binary".to_string()),
5672 description: Some("Resume file".to_string()),
5673 ..Default::default()
5674 }),
5675 );
5676 props
5677 },
5678 required: vec!["avatar".to_string(), "resume".to_string()],
5679 ..Default::default()
5680 })),
5681 examples: None,
5682 encoding: Default::default(),
5683 },
5684 );
5685 content
5686 },
5687 required: Some(true),
5688 });
5689
5690 let spec = create_test_spec();
5691 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
5692 .unwrap()
5693 .unwrap();
5694
5695 let (schema, annotations, _is_required) = result;
5696
5697 let body_properties = schema.get("properties").unwrap();
5699 for field_name in ["avatar", "document", "resume"] {
5700 let field_schema = body_properties.get(field_name).unwrap();
5701 assert_eq!(
5702 field_schema.get("type").unwrap(),
5703 "object",
5704 "Field {field_name} should be transformed to object type"
5705 );
5706 assert!(
5707 field_schema
5708 .get("properties")
5709 .unwrap()
5710 .get("content")
5711 .is_some(),
5712 "Field {field_name} should have content property"
5713 );
5714 }
5715
5716 let annotations_value = serde_json::to_value(&annotations).unwrap();
5718 let annotations_obj = annotations_value.as_object().unwrap();
5719
5720 let x_file_fields = annotations_obj
5721 .get("x-file-fields")
5722 .unwrap()
5723 .as_array()
5724 .unwrap();
5725 assert_eq!(x_file_fields.len(), 3);
5726 assert!(x_file_fields.contains(&json!("avatar")));
5727 assert!(x_file_fields.contains(&json!("document")));
5728 assert!(x_file_fields.contains(&json!("resume")));
5729
5730 insta::assert_json_snapshot!("test_multipart_form_data_with_multiple_files", schema);
5732 }
5733
5734 #[test]
5735 fn test_multipart_form_data_mixed_fields() {
5736 let request_body = ObjectOrReference::Object(RequestBody {
5738 description: Some("Profile creation with file upload".to_string()),
5739 content: {
5740 let mut content = BTreeMap::new();
5741 content.insert(
5742 "multipart/form-data".to_string(),
5743 MediaType {
5744 extensions: Default::default(),
5745 schema: Some(ObjectOrReference::Object(ObjectSchema {
5746 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5747 properties: {
5748 let mut props = BTreeMap::new();
5749 props.insert(
5751 "avatar".to_string(),
5752 ObjectOrReference::Object(ObjectSchema {
5753 schema_type: Some(SchemaTypeSet::Single(
5754 SchemaType::String,
5755 )),
5756 format: Some("binary".to_string()),
5757 description: Some("Profile avatar image".to_string()),
5758 ..Default::default()
5759 }),
5760 );
5761 props.insert(
5763 "name".to_string(),
5764 ObjectOrReference::Object(ObjectSchema {
5765 schema_type: Some(SchemaTypeSet::Single(
5766 SchemaType::String,
5767 )),
5768 description: Some("User's display name".to_string()),
5769 ..Default::default()
5770 }),
5771 );
5772 props.insert(
5774 "age".to_string(),
5775 ObjectOrReference::Object(ObjectSchema {
5776 schema_type: Some(SchemaTypeSet::Single(
5777 SchemaType::Integer,
5778 )),
5779 description: Some("User's age".to_string()),
5780 ..Default::default()
5781 }),
5782 );
5783 props.insert(
5785 "email".to_string(),
5786 ObjectOrReference::Object(ObjectSchema {
5787 schema_type: Some(SchemaTypeSet::Single(
5788 SchemaType::String,
5789 )),
5790 format: Some("email".to_string()),
5791 description: Some("User's email address".to_string()),
5792 ..Default::default()
5793 }),
5794 );
5795 props
5796 },
5797 required: vec!["name".to_string(), "avatar".to_string()],
5798 ..Default::default()
5799 })),
5800 examples: None,
5801 encoding: Default::default(),
5802 },
5803 );
5804 content
5805 },
5806 required: Some(true),
5807 });
5808
5809 let spec = create_test_spec();
5810 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
5811 .unwrap()
5812 .unwrap();
5813
5814 let (schema, annotations, _is_required) = result;
5815 let body_properties = schema.get("properties").unwrap();
5816
5817 let avatar_schema = body_properties.get("avatar").unwrap();
5819 assert_eq!(avatar_schema.get("type").unwrap(), "object");
5820 assert!(
5821 avatar_schema
5822 .get("properties")
5823 .unwrap()
5824 .get("content")
5825 .is_some()
5826 );
5827 assert!(
5828 avatar_schema
5829 .get("properties")
5830 .unwrap()
5831 .get("filename")
5832 .is_some()
5833 );
5834
5835 let name_schema = body_properties.get("name").unwrap();
5837 assert_eq!(name_schema.get("type").unwrap(), "string");
5838 assert!(name_schema.get("properties").is_none()); let age_schema = body_properties.get("age").unwrap();
5842 assert_eq!(age_schema.get("type").unwrap(), "integer");
5843
5844 let email_schema = body_properties.get("email").unwrap();
5846 assert_eq!(email_schema.get("type").unwrap(), "string");
5847 assert_eq!(email_schema.get("format").unwrap(), "email");
5848
5849 let annotations_value = serde_json::to_value(&annotations).unwrap();
5851 let annotations_obj = annotations_value.as_object().unwrap();
5852
5853 let x_file_fields = annotations_obj
5854 .get("x-file-fields")
5855 .unwrap()
5856 .as_array()
5857 .unwrap();
5858 assert_eq!(x_file_fields.len(), 1);
5859 assert!(x_file_fields.contains(&json!("avatar")));
5860
5861 insta::assert_json_snapshot!("test_multipart_form_data_mixed_fields", schema);
5863 }
5864
5865 #[test]
5866 fn test_multipart_format_byte_detection() {
5867 let request_body = ObjectOrReference::Object(RequestBody {
5869 description: Some("Base64 encoded file upload".to_string()),
5870 content: {
5871 let mut content = BTreeMap::new();
5872 content.insert(
5873 "multipart/form-data".to_string(),
5874 MediaType {
5875 extensions: Default::default(),
5876 schema: Some(ObjectOrReference::Object(ObjectSchema {
5877 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5878 properties: {
5879 let mut props = BTreeMap::new();
5880 props.insert(
5882 "data".to_string(),
5883 ObjectOrReference::Object(ObjectSchema {
5884 schema_type: Some(SchemaTypeSet::Single(
5885 SchemaType::String,
5886 )),
5887 format: Some("byte".to_string()),
5888 description: Some(
5889 "Base64 encoded file content".to_string(),
5890 ),
5891 ..Default::default()
5892 }),
5893 );
5894 props.insert(
5896 "attachment".to_string(),
5897 ObjectOrReference::Object(ObjectSchema {
5898 schema_type: Some(SchemaTypeSet::Single(
5899 SchemaType::String,
5900 )),
5901 format: Some("binary".to_string()),
5902 description: Some("Binary file attachment".to_string()),
5903 ..Default::default()
5904 }),
5905 );
5906 props
5907 },
5908 required: vec!["data".to_string()],
5909 ..Default::default()
5910 })),
5911 examples: None,
5912 encoding: Default::default(),
5913 },
5914 );
5915 content
5916 },
5917 required: Some(true),
5918 });
5919
5920 let spec = create_test_spec();
5921 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
5922 .unwrap()
5923 .unwrap();
5924
5925 let (schema, annotations, _is_required) = result;
5926 let body_properties = schema.get("properties").unwrap();
5927
5928 let data_schema = body_properties.get("data").unwrap();
5930 assert_eq!(data_schema.get("type").unwrap(), "object");
5931 assert!(
5932 data_schema
5933 .get("properties")
5934 .unwrap()
5935 .get("content")
5936 .is_some()
5937 );
5938
5939 let attachment_schema = body_properties.get("attachment").unwrap();
5940 assert_eq!(attachment_schema.get("type").unwrap(), "object");
5941 assert!(
5942 attachment_schema
5943 .get("properties")
5944 .unwrap()
5945 .get("content")
5946 .is_some()
5947 );
5948
5949 let annotations_value = serde_json::to_value(&annotations).unwrap();
5951 let annotations_obj = annotations_value.as_object().unwrap();
5952
5953 let x_file_fields = annotations_obj
5954 .get("x-file-fields")
5955 .unwrap()
5956 .as_array()
5957 .unwrap();
5958 assert_eq!(x_file_fields.len(), 2);
5959 assert!(x_file_fields.contains(&json!("data")));
5960 assert!(x_file_fields.contains(&json!("attachment")));
5961
5962 insta::assert_json_snapshot!("test_multipart_format_byte_detection", schema);
5964 }
5965
5966 #[test]
5967 fn test_multipart_non_file_fields_unchanged() {
5968 let request_body = ObjectOrReference::Object(RequestBody {
5970 description: Some("Form submission".to_string()),
5971 content: {
5972 let mut content = BTreeMap::new();
5973 content.insert(
5974 "multipart/form-data".to_string(),
5975 MediaType {
5976 extensions: Default::default(),
5977 schema: Some(ObjectOrReference::Object(ObjectSchema {
5978 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5979 properties: {
5980 let mut props = BTreeMap::new();
5981 props.insert(
5983 "title".to_string(),
5984 ObjectOrReference::Object(ObjectSchema {
5985 schema_type: Some(SchemaTypeSet::Single(
5986 SchemaType::String,
5987 )),
5988 description: Some("Form title".to_string()),
5989 ..Default::default()
5990 }),
5991 );
5992 props.insert(
5993 "count".to_string(),
5994 ObjectOrReference::Object(ObjectSchema {
5995 schema_type: Some(SchemaTypeSet::Single(
5996 SchemaType::Integer,
5997 )),
5998 description: Some("Item count".to_string()),
5999 ..Default::default()
6000 }),
6001 );
6002 props.insert(
6003 "enabled".to_string(),
6004 ObjectOrReference::Object(ObjectSchema {
6005 schema_type: Some(SchemaTypeSet::Single(
6006 SchemaType::Boolean,
6007 )),
6008 description: Some("Enable flag".to_string()),
6009 ..Default::default()
6010 }),
6011 );
6012 props.insert(
6013 "price".to_string(),
6014 ObjectOrReference::Object(ObjectSchema {
6015 schema_type: Some(SchemaTypeSet::Single(
6016 SchemaType::Number,
6017 )),
6018 description: Some("Price value".to_string()),
6019 ..Default::default()
6020 }),
6021 );
6022 props.insert(
6023 "uuid".to_string(),
6024 ObjectOrReference::Object(ObjectSchema {
6025 schema_type: Some(SchemaTypeSet::Single(
6026 SchemaType::String,
6027 )),
6028 format: Some("uuid".to_string()),
6029 description: Some("UUID field".to_string()),
6030 ..Default::default()
6031 }),
6032 );
6033 props.insert(
6034 "date".to_string(),
6035 ObjectOrReference::Object(ObjectSchema {
6036 schema_type: Some(SchemaTypeSet::Single(
6037 SchemaType::String,
6038 )),
6039 format: Some("date".to_string()),
6040 description: Some("Date field".to_string()),
6041 ..Default::default()
6042 }),
6043 );
6044 props
6045 },
6046 required: vec!["title".to_string()],
6047 ..Default::default()
6048 })),
6049 examples: None,
6050 encoding: Default::default(),
6051 },
6052 );
6053 content
6054 },
6055 required: Some(true),
6056 });
6057
6058 let spec = create_test_spec();
6059 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
6060 .unwrap()
6061 .unwrap();
6062
6063 let (schema, annotations, _is_required) = result;
6064 let body_properties = schema.get("properties").unwrap();
6065
6066 let title_schema = body_properties.get("title").unwrap();
6068 assert_eq!(title_schema.get("type").unwrap(), "string");
6069 assert!(title_schema.get("properties").is_none());
6070
6071 let count_schema = body_properties.get("count").unwrap();
6073 assert_eq!(count_schema.get("type").unwrap(), "integer");
6074
6075 let enabled_schema = body_properties.get("enabled").unwrap();
6077 assert_eq!(enabled_schema.get("type").unwrap(), "boolean");
6078
6079 let price_schema = body_properties.get("price").unwrap();
6081 assert_eq!(price_schema.get("type").unwrap(), "number");
6082
6083 let uuid_schema = body_properties.get("uuid").unwrap();
6085 assert_eq!(uuid_schema.get("type").unwrap(), "string");
6086 assert_eq!(uuid_schema.get("format").unwrap(), "uuid");
6087
6088 let date_schema = body_properties.get("date").unwrap();
6090 assert_eq!(date_schema.get("type").unwrap(), "string");
6091 assert_eq!(date_schema.get("format").unwrap(), "date");
6092
6093 let annotations_value = serde_json::to_value(&annotations).unwrap();
6095 let annotations_obj = annotations_value.as_object().unwrap();
6096
6097 assert!(
6098 annotations_obj.get("x-file-fields").is_none(),
6099 "x-file-fields should not be present when there are no file fields"
6100 );
6101
6102 assert_eq!(
6104 annotations_obj.get("x-content-type").unwrap(),
6105 "multipart/form-data"
6106 );
6107
6108 insta::assert_json_snapshot!("test_multipart_non_file_fields_unchanged", schema);
6110 }
6111}