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