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