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 lifted_item_examples: Option<Vec<Value>> = (result.get("type")
1702 == Some(&json!("array")))
1703 .then(|| {
1704 result
1705 .get("items")
1706 .and_then(|items| items.get("examples"))
1707 .and_then(Value::as_array)
1708 .cloned()
1709 })
1710 .flatten();
1711 if let Some(item_examples) = lifted_item_examples {
1712 for item_example in &item_examples {
1713 collected_examples.push(json!([item_example]));
1714 }
1715 if let Some(Value::Object(items)) = result.get_mut("items") {
1716 items.remove("examples");
1717 }
1718 }
1719 let mut deduped: Vec<Value> = Vec::with_capacity(collected_examples.len());
1721 for example in collected_examples {
1722 if !deduped.contains(&example) {
1723 deduped.push(example);
1724 }
1725 }
1726 let collected_examples = deduped;
1727
1728 result.remove("example");
1735 result.remove("examples");
1736
1737 let base_description = param
1738 .description
1739 .as_ref()
1740 .map(|d| d.to_string())
1741 .or_else(|| {
1742 result
1743 .get("description")
1744 .and_then(|d| d.as_str())
1745 .map(|d| d.to_string())
1746 })
1747 .unwrap_or_else(|| format!("{} parameter", param.name));
1748
1749 let description = if parameter_examples_in_description {
1750 match Self::format_examples_for_description(&collected_examples) {
1751 Some(examples_str) => format!("{base_description}. {examples_str}"),
1752 None => base_description,
1753 }
1754 } else {
1755 base_description
1756 };
1757
1758 if !skip_parameter_descriptions {
1759 result.insert("description".to_string(), json!(description));
1760 }
1761
1762 if !parameter_examples_in_description && !collected_examples.is_empty() {
1763 result.insert("examples".to_string(), json!(collected_examples));
1764 }
1765
1766 let mut annotations = Annotations::new()
1768 .with_location(Location::Parameter(location))
1769 .with_required(param.required.unwrap_or(false));
1770
1771 if let Some(explode) = param.explode {
1773 annotations = annotations.with_explode(explode);
1774 } else {
1775 let default_explode = match ¶m.style {
1779 Some(ParameterStyle::Form) | None => true, _ => false,
1781 };
1782 annotations = annotations.with_explode(default_explode);
1783 }
1784
1785 Ok((Value::Object(result), annotations))
1786 }
1787
1788 fn format_examples_for_description(examples: &[Value]) -> Option<String> {
1790 if examples.is_empty() {
1791 return None;
1792 }
1793
1794 if examples.len() == 1 {
1795 let example_str =
1796 serde_json::to_string(&examples[0]).unwrap_or_else(|_| "null".to_string());
1797 Some(format!("Example: `{example_str}`"))
1798 } else {
1799 let mut result = String::from("Examples:\n");
1800 for ex in examples {
1801 let json_str = serde_json::to_string(ex).unwrap_or_else(|_| "null".to_string());
1802 result.push_str(&format!("- `{json_str}`\n"));
1803 }
1804 result.pop();
1806 Some(result)
1807 }
1808 }
1809
1810 fn convert_prefix_items_to_draft07(
1821 prefix_items: &[ObjectOrReference<ObjectSchema>],
1822 items: &Option<Box<Schema>>,
1823 result: &mut serde_json::Map<String, Value>,
1824 spec: &Spec,
1825 ) -> Result<(), Error> {
1826 let prefix_count = prefix_items.len();
1827
1828 let mut item_types = Vec::new();
1830 for prefix_item in prefix_items {
1831 match prefix_item {
1832 ObjectOrReference::Object(obj_schema) => {
1833 if let Some(schema_type) = &obj_schema.schema_type {
1834 match schema_type {
1835 SchemaTypeSet::Single(SchemaType::String) => item_types.push("string"),
1836 SchemaTypeSet::Single(SchemaType::Integer) => {
1837 item_types.push("integer")
1838 }
1839 SchemaTypeSet::Single(SchemaType::Number) => item_types.push("number"),
1840 SchemaTypeSet::Single(SchemaType::Boolean) => {
1841 item_types.push("boolean")
1842 }
1843 SchemaTypeSet::Single(SchemaType::Array) => item_types.push("array"),
1844 SchemaTypeSet::Single(SchemaType::Object) => item_types.push("object"),
1845 _ => item_types.push("string"), }
1847 } else {
1848 item_types.push("string"); }
1850 }
1851 ObjectOrReference::Ref { ref_path, .. } => {
1852 let mut visited = HashSet::new();
1854 match Self::resolve_reference(ref_path, spec, &mut visited) {
1855 Ok(resolved_schema) => {
1856 if let Some(schema_type_set) = &resolved_schema.schema_type {
1858 match schema_type_set {
1859 SchemaTypeSet::Single(SchemaType::String) => {
1860 item_types.push("string")
1861 }
1862 SchemaTypeSet::Single(SchemaType::Integer) => {
1863 item_types.push("integer")
1864 }
1865 SchemaTypeSet::Single(SchemaType::Number) => {
1866 item_types.push("number")
1867 }
1868 SchemaTypeSet::Single(SchemaType::Boolean) => {
1869 item_types.push("boolean")
1870 }
1871 SchemaTypeSet::Single(SchemaType::Array) => {
1872 item_types.push("array")
1873 }
1874 SchemaTypeSet::Single(SchemaType::Object) => {
1875 item_types.push("object")
1876 }
1877 _ => item_types.push("string"), }
1879 } else {
1880 item_types.push("string"); }
1882 }
1883 Err(_) => {
1884 item_types.push("string");
1886 }
1887 }
1888 }
1889 }
1890 }
1891
1892 let items_is_false =
1894 matches!(items.as_ref().map(|i| i.as_ref()), Some(Schema::Boolean(b)) if !b.0);
1895
1896 if items_is_false {
1897 result.insert("minItems".to_string(), json!(prefix_count));
1899 result.insert("maxItems".to_string(), json!(prefix_count));
1900 }
1901
1902 let unique_types: std::collections::BTreeSet<_> = item_types.into_iter().collect();
1904
1905 if unique_types.len() == 1 {
1906 let item_type = unique_types.into_iter().next().unwrap();
1908 result.insert("items".to_string(), json!({"type": item_type}));
1909 } else if unique_types.len() > 1 {
1910 let one_of: Vec<Value> = unique_types
1912 .into_iter()
1913 .map(|t| json!({"type": t}))
1914 .collect();
1915 result.insert("items".to_string(), json!({"oneOf": one_of}));
1916 }
1917
1918 Ok(())
1919 }
1920
1921 fn convert_request_body_to_json_schema(
1933 request_body_ref: &ObjectOrReference<RequestBody>,
1934 spec: &Spec,
1935 ) -> Result<Option<(Value, Annotations, bool)>, Error> {
1936 match request_body_ref {
1937 ObjectOrReference::Object(request_body) => {
1938 if let Some(media_type) = request_body.content.get("multipart/form-data") {
1940 return Self::convert_multipart_request_body(request_body, media_type, spec);
1941 }
1942
1943 let schema_info = request_body
1946 .content
1947 .get(mime::APPLICATION_JSON.as_ref())
1948 .or_else(|| request_body.content.get("application/json"))
1949 .or_else(|| {
1950 request_body.content.values().next()
1952 });
1953
1954 if let Some(media_type) = schema_info {
1955 if let Some(schema_ref) = &media_type.schema {
1956 let schema = Schema::Object(Box::new(schema_ref.clone()));
1958
1959 let mut visited = HashSet::new();
1961 let converted_schema =
1962 Self::convert_schema_to_json_schema(&schema, spec, &mut visited)?;
1963
1964 let mut schema_obj = match converted_schema {
1966 Value::Object(obj) => obj,
1967 _ => {
1968 let mut obj = serde_json::Map::new();
1970 obj.insert("type".to_string(), json!("object"));
1971 obj.insert("additionalProperties".to_string(), json!(true));
1972 obj
1973 }
1974 };
1975
1976 if !schema_obj.contains_key("description") {
1978 let description = request_body
1979 .description
1980 .clone()
1981 .unwrap_or_else(|| "Request body data".to_string());
1982 schema_obj.insert("description".to_string(), json!(description));
1983 }
1984
1985 let annotations = Annotations::new()
1987 .with_location(Location::Body)
1988 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
1989
1990 let required = request_body.required.unwrap_or(false);
1991 Ok(Some((Value::Object(schema_obj), annotations, required)))
1992 } else {
1993 Ok(None)
1994 }
1995 } else {
1996 Ok(None)
1997 }
1998 }
1999 ObjectOrReference::Ref {
2000 ref_path: _,
2001 summary,
2002 description,
2003 } => {
2004 let ref_metadata = ReferenceMetadata::new(summary.clone(), description.clone());
2006 let enhanced_description = ref_metadata
2007 .best_description()
2008 .map(|desc| desc.to_string())
2009 .unwrap_or_else(|| "Request body data".to_string());
2010
2011 let mut result = serde_json::Map::new();
2012 result.insert("type".to_string(), json!("object"));
2013 result.insert("additionalProperties".to_string(), json!(true));
2014 result.insert("description".to_string(), json!(enhanced_description));
2015
2016 let annotations = Annotations::new()
2018 .with_location(Location::Body)
2019 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
2020
2021 Ok(Some((Value::Object(result), annotations, false)))
2022 }
2023 }
2024 }
2025
2026 fn convert_multipart_request_body(
2035 request_body: &RequestBody,
2036 media_type: &oas3::spec::MediaType,
2037 spec: &Spec,
2038 ) -> Result<Option<(Value, Annotations, bool)>, Error> {
2039 let Some(schema_ref) = &media_type.schema else {
2040 return Ok(None);
2041 };
2042
2043 let obj_schema = match schema_ref {
2045 ObjectOrReference::Object(obj) => obj.clone(),
2046 ObjectOrReference::Ref { ref_path, .. } => {
2047 let mut visited = HashSet::new();
2049 Self::resolve_reference(ref_path, spec, &mut visited)?
2050 }
2051 };
2052
2053 let mut props_map = serde_json::Map::new();
2055 let mut file_fields = Vec::new();
2056
2057 for (prop_name, prop_schema_or_ref) in &obj_schema.properties {
2058 let sanitized_name = sanitize_property_name(prop_name);
2059
2060 let prop_schema = if Self::is_file_field_property(prop_schema_or_ref) {
2061 file_fields.push(sanitized_name.clone());
2063
2064 let description = match prop_schema_or_ref {
2066 ObjectOrReference::Object(obj) => obj.description.as_deref(),
2067 ObjectOrReference::Ref { .. } => None,
2068 };
2069
2070 Self::convert_file_field_to_schema(description)
2072 } else {
2073 let schema = Schema::Object(Box::new(prop_schema_or_ref.clone()));
2075 let mut visited = HashSet::new();
2076 Self::convert_schema_to_json_schema(&schema, spec, &mut visited)?
2077 };
2078
2079 props_map.insert(sanitized_name, prop_schema);
2080 }
2081
2082 let mut schema_obj = serde_json::Map::new();
2084 schema_obj.insert("type".to_string(), json!("object"));
2085
2086 if !props_map.is_empty() {
2087 schema_obj.insert("properties".to_string(), Value::Object(props_map));
2088 }
2089
2090 if !obj_schema.required.is_empty() {
2092 let sanitized_required: Vec<String> = obj_schema
2094 .required
2095 .iter()
2096 .map(|name| sanitize_property_name(name))
2097 .collect();
2098 schema_obj.insert("required".to_string(), json!(sanitized_required));
2099 }
2100
2101 let description = obj_schema
2103 .description
2104 .clone()
2105 .or_else(|| request_body.description.clone())
2106 .unwrap_or_else(|| "Request body data".to_string());
2107 schema_obj.insert("description".to_string(), json!(description));
2108
2109 let mut annotations = Annotations::new()
2111 .with_location(Location::Body)
2112 .with_content_type("multipart/form-data".to_string());
2113
2114 if !file_fields.is_empty() {
2115 annotations = annotations.with_file_fields(file_fields);
2116 }
2117
2118 let required = request_body.required.unwrap_or(false);
2119 Ok(Some((Value::Object(schema_obj), annotations, required)))
2120 }
2121
2122 pub fn extract_parameters(
2128 tool_metadata: &ToolMetadata,
2129 arguments: &Value,
2130 ) -> Result<ExtractedParameters, ToolCallValidationError> {
2131 let args = arguments.as_object().ok_or_else(|| {
2132 ToolCallValidationError::RequestConstructionError {
2133 reason: "Arguments must be an object".to_string(),
2134 }
2135 })?;
2136
2137 trace!(
2138 tool_name = %tool_metadata.name,
2139 raw_arguments = ?arguments,
2140 "Starting parameter extraction"
2141 );
2142
2143 let mut path_params = HashMap::new();
2144 let mut query_params = HashMap::new();
2145 let mut header_params = HashMap::new();
2146 let mut cookie_params = HashMap::new();
2147 let mut body_params = HashMap::new();
2148 let mut config = RequestConfig::default();
2149
2150 if let Some(timeout) = args.get("timeout_seconds").and_then(Value::as_u64) {
2152 config.timeout_seconds = u32::try_from(timeout).unwrap_or(u32::MAX);
2153 }
2154
2155 for (key, value) in args {
2157 if key == "timeout_seconds" {
2158 continue; }
2160
2161 if key == "request_body" {
2163 body_params.insert("request_body".to_string(), value.clone());
2164 continue;
2165 }
2166
2167 let mapping = tool_metadata.parameter_mappings.get(key);
2169
2170 if let Some(mapping) = mapping {
2171 match mapping.location.as_str() {
2173 "path" => {
2174 path_params.insert(mapping.original_name.clone(), value.clone());
2175 }
2176 "query" => {
2177 query_params.insert(
2178 mapping.original_name.clone(),
2179 QueryParameter::new(value.clone(), mapping.explode),
2180 );
2181 }
2182 "header" => {
2183 header_params.insert(mapping.original_name.clone(), value.clone());
2184 }
2185 "cookie" => {
2186 cookie_params.insert(mapping.original_name.clone(), value.clone());
2187 }
2188 "body" => {
2189 body_params.insert(mapping.original_name.clone(), value.clone());
2190 }
2191 _ => {
2192 return Err(ToolCallValidationError::RequestConstructionError {
2193 reason: format!("Unknown parameter location for parameter: {key}"),
2194 });
2195 }
2196 }
2197 } else {
2198 let location = Self::get_parameter_location(tool_metadata, key).map_err(|e| {
2200 ToolCallValidationError::RequestConstructionError {
2201 reason: e.to_string(),
2202 }
2203 })?;
2204
2205 let original_name = Self::get_original_parameter_name(tool_metadata, key);
2206
2207 match location.as_str() {
2208 "path" => {
2209 path_params
2210 .insert(original_name.unwrap_or_else(|| key.clone()), value.clone());
2211 }
2212 "query" => {
2213 let param_name = original_name.unwrap_or_else(|| key.clone());
2214 let explode = Self::get_parameter_explode(tool_metadata, key);
2215 query_params
2216 .insert(param_name, QueryParameter::new(value.clone(), explode));
2217 }
2218 "header" => {
2219 let header_name = if let Some(orig) = original_name {
2220 orig
2221 } else if key.starts_with("header_") {
2222 key.strip_prefix("header_").unwrap_or(key).to_string()
2223 } else {
2224 key.clone()
2225 };
2226 header_params.insert(header_name, value.clone());
2227 }
2228 "cookie" => {
2229 let cookie_name = if let Some(orig) = original_name {
2230 orig
2231 } else if key.starts_with("cookie_") {
2232 key.strip_prefix("cookie_").unwrap_or(key).to_string()
2233 } else {
2234 key.clone()
2235 };
2236 cookie_params.insert(cookie_name, value.clone());
2237 }
2238 "body" => {
2239 let body_name = if key.starts_with("body_") {
2240 key.strip_prefix("body_").unwrap_or(key).to_string()
2241 } else {
2242 key.clone()
2243 };
2244 body_params.insert(body_name, value.clone());
2245 }
2246 _ => {
2247 return Err(ToolCallValidationError::RequestConstructionError {
2248 reason: format!("Unknown parameter location for parameter: {key}"),
2249 });
2250 }
2251 }
2252 }
2253 }
2254
2255 let extracted = ExtractedParameters {
2256 path: path_params,
2257 query: query_params,
2258 headers: header_params,
2259 cookies: cookie_params,
2260 body: body_params,
2261 config,
2262 };
2263
2264 trace!(
2265 tool_name = %tool_metadata.name,
2266 extracted_parameters = ?extracted,
2267 "Parameter extraction completed"
2268 );
2269
2270 Self::validate_parameters(tool_metadata, arguments)?;
2272
2273 Ok(extracted)
2274 }
2275
2276 fn get_original_parameter_name(
2278 tool_metadata: &ToolMetadata,
2279 param_name: &str,
2280 ) -> Option<String> {
2281 tool_metadata
2282 .parameters
2283 .get("properties")
2284 .and_then(|p| p.as_object())
2285 .and_then(|props| props.get(param_name))
2286 .and_then(|schema| schema.get(X_ORIGINAL_NAME))
2287 .and_then(|v| v.as_str())
2288 .map(|s| s.to_string())
2289 }
2290
2291 fn get_parameter_explode(tool_metadata: &ToolMetadata, param_name: &str) -> bool {
2293 tool_metadata
2294 .parameters
2295 .get("properties")
2296 .and_then(|p| p.as_object())
2297 .and_then(|props| props.get(param_name))
2298 .and_then(|schema| schema.get(X_PARAMETER_EXPLODE))
2299 .and_then(|v| v.as_bool())
2300 .unwrap_or(true) }
2302
2303 fn get_parameter_location(
2305 tool_metadata: &ToolMetadata,
2306 param_name: &str,
2307 ) -> Result<String, Error> {
2308 let properties = tool_metadata
2309 .parameters
2310 .get("properties")
2311 .and_then(|p| p.as_object())
2312 .ok_or_else(|| Error::ToolGeneration("Invalid tool parameters schema".to_string()))?;
2313
2314 if let Some(param_schema) = properties.get(param_name)
2315 && let Some(location) = param_schema
2316 .get(X_PARAMETER_LOCATION)
2317 .and_then(|v| v.as_str())
2318 {
2319 return Ok(location.to_string());
2320 }
2321
2322 if param_name.starts_with("header_") {
2324 Ok("header".to_string())
2325 } else if param_name.starts_with("cookie_") {
2326 Ok("cookie".to_string())
2327 } else if param_name.starts_with("body_") {
2328 Ok("body".to_string())
2329 } else {
2330 Ok("query".to_string())
2332 }
2333 }
2334
2335 fn validate_parameters(
2337 tool_metadata: &ToolMetadata,
2338 arguments: &Value,
2339 ) -> Result<(), ToolCallValidationError> {
2340 let schema = &tool_metadata.parameters;
2341
2342 let required_params = schema
2344 .get("required")
2345 .and_then(|r| r.as_array())
2346 .map(|arr| {
2347 arr.iter()
2348 .filter_map(|v| v.as_str())
2349 .collect::<std::collections::HashSet<_>>()
2350 })
2351 .unwrap_or_default();
2352
2353 let properties = schema
2354 .get("properties")
2355 .and_then(|p| p.as_object())
2356 .ok_or_else(|| ToolCallValidationError::RequestConstructionError {
2357 reason: "Tool schema missing properties".to_string(),
2358 })?;
2359
2360 let args = arguments.as_object().ok_or_else(|| {
2361 ToolCallValidationError::RequestConstructionError {
2362 reason: "Arguments must be an object".to_string(),
2363 }
2364 })?;
2365
2366 let mut all_errors = Vec::new();
2368
2369 all_errors.extend(Self::check_unknown_parameters(args, properties));
2371
2372 all_errors.extend(Self::check_missing_required(
2374 args,
2375 properties,
2376 &required_params,
2377 ));
2378
2379 all_errors.extend(Self::validate_parameter_values(
2381 args,
2382 properties,
2383 &required_params,
2384 ));
2385
2386 if !all_errors.is_empty() {
2388 return Err(ToolCallValidationError::InvalidParameters {
2389 violations: all_errors,
2390 });
2391 }
2392
2393 Ok(())
2394 }
2395
2396 fn check_unknown_parameters(
2398 args: &serde_json::Map<String, Value>,
2399 properties: &serde_json::Map<String, Value>,
2400 ) -> Vec<ValidationError> {
2401 let mut errors = Vec::new();
2402
2403 let valid_params: Vec<String> = properties.keys().map(|s| s.to_string()).collect();
2405
2406 for (arg_name, _) in args.iter() {
2408 if !properties.contains_key(arg_name) {
2409 errors.push(ValidationError::invalid_parameter(
2411 arg_name.clone(),
2412 &valid_params,
2413 ));
2414 }
2415 }
2416
2417 errors
2418 }
2419
2420 fn check_missing_required(
2422 args: &serde_json::Map<String, Value>,
2423 properties: &serde_json::Map<String, Value>,
2424 required_params: &HashSet<&str>,
2425 ) -> Vec<ValidationError> {
2426 let mut errors = Vec::new();
2427
2428 for required_param in required_params {
2429 if !args.contains_key(*required_param) {
2430 let param_schema = properties.get(*required_param);
2432
2433 let description = param_schema
2434 .and_then(|schema| schema.get("description"))
2435 .and_then(|d| d.as_str())
2436 .map(|s| s.to_string());
2437
2438 let expected_type = param_schema
2439 .and_then(Self::get_expected_type)
2440 .unwrap_or_else(|| "unknown".to_string());
2441
2442 errors.push(ValidationError::MissingRequiredParameter {
2443 parameter: (*required_param).to_string(),
2444 description,
2445 expected_type,
2446 });
2447 }
2448 }
2449
2450 errors
2451 }
2452
2453 fn validate_parameter_values(
2455 args: &serde_json::Map<String, Value>,
2456 properties: &serde_json::Map<String, Value>,
2457 required_params: &std::collections::HashSet<&str>,
2458 ) -> Vec<ValidationError> {
2459 let mut errors = Vec::new();
2460
2461 for (param_name, param_value) in args {
2462 if let Some(param_schema) = properties.get(param_name) {
2463 let is_null_value = param_value.is_null();
2465 let is_required = required_params.contains(param_name.as_str());
2466
2467 let schema = json!({
2469 "type": "object",
2470 "properties": {
2471 param_name: param_schema
2472 }
2473 });
2474
2475 let compiled = match jsonschema::validator_for(&schema) {
2477 Ok(compiled) => compiled,
2478 Err(e) => {
2479 errors.push(ValidationError::ConstraintViolation {
2480 parameter: param_name.clone(),
2481 message: format!(
2482 "Failed to compile schema for parameter '{param_name}': {e}"
2483 ),
2484 field_path: None,
2485 actual_value: None,
2486 expected_type: None,
2487 constraints: vec![],
2488 });
2489 continue;
2490 }
2491 };
2492
2493 let instance = json!({ param_name: param_value });
2495
2496 let validation_errors: Vec<_> =
2498 compiled.validate(&instance).err().into_iter().collect();
2499
2500 for validation_error in validation_errors {
2501 let error_message = validation_error.to_string();
2503 let instance_path_str = validation_error.instance_path().to_string();
2504 let field_path = if instance_path_str.is_empty() || instance_path_str == "/" {
2505 Some(param_name.clone())
2506 } else {
2507 Some(instance_path_str.trim_start_matches('/').to_string())
2508 };
2509
2510 let constraints = Self::extract_constraints_from_schema(param_schema);
2512
2513 let expected_type = Self::get_expected_type(param_schema);
2515
2516 let maybe_type_error = match &validation_error.kind() {
2520 ValidationErrorKind::Type { kind } => Some(kind),
2521 _ => None,
2522 };
2523 let is_type_error = maybe_type_error.is_some();
2524 let is_null_error = is_null_value
2525 || (is_type_error && validation_error.instance().as_null().is_some());
2526 let message = if is_null_error && let Some(type_error) = maybe_type_error {
2527 let field_name = field_path.as_ref().unwrap_or(param_name);
2529
2530 let final_expected_type =
2532 expected_type.clone().unwrap_or_else(|| match type_error {
2533 TypeKind::Single(json_type) => json_type.to_string(),
2534 TypeKind::Multiple(json_type_set) => json_type_set
2535 .iter()
2536 .map(|t| t.to_string())
2537 .collect::<Vec<_>>()
2538 .join(", "),
2539 });
2540
2541 let actual_field_name = field_path
2544 .as_ref()
2545 .and_then(|path| path.split('/').next_back())
2546 .unwrap_or(param_name);
2547
2548 let is_nested_field = field_path.as_ref().is_some_and(|p| p.contains('/'));
2551
2552 let field_is_required = if is_nested_field {
2553 constraints.iter().any(|c| {
2554 if let ValidationConstraint::Required { properties } = c {
2555 properties.contains(&actual_field_name.to_string())
2556 } else {
2557 false
2558 }
2559 })
2560 } else {
2561 is_required
2562 };
2563
2564 if field_is_required {
2565 format!(
2566 "Parameter '{field_name}' is required and must not be null (expected: {final_expected_type})"
2567 )
2568 } else {
2569 format!(
2570 "Parameter '{field_name}' is optional but must not be null (expected: {final_expected_type})"
2571 )
2572 }
2573 } else {
2574 error_message
2575 };
2576
2577 errors.push(ValidationError::ConstraintViolation {
2578 parameter: param_name.clone(),
2579 message,
2580 field_path,
2581 actual_value: Some(Box::new(param_value.clone())),
2582 expected_type,
2583 constraints,
2584 });
2585 }
2586 }
2587 }
2588
2589 errors
2590 }
2591
2592 fn extract_constraints_from_schema(schema: &Value) -> Vec<ValidationConstraint> {
2594 let mut constraints = Vec::new();
2595
2596 if let Some(min_value) = schema.get("minimum").and_then(|v| v.as_f64()) {
2598 let exclusive = schema
2599 .get("exclusiveMinimum")
2600 .and_then(|v| v.as_bool())
2601 .unwrap_or(false);
2602 constraints.push(ValidationConstraint::Minimum {
2603 value: min_value,
2604 exclusive,
2605 });
2606 }
2607
2608 if let Some(max_value) = schema.get("maximum").and_then(|v| v.as_f64()) {
2610 let exclusive = schema
2611 .get("exclusiveMaximum")
2612 .and_then(|v| v.as_bool())
2613 .unwrap_or(false);
2614 constraints.push(ValidationConstraint::Maximum {
2615 value: max_value,
2616 exclusive,
2617 });
2618 }
2619
2620 if let Some(min_len) = schema
2622 .get("minLength")
2623 .and_then(|v| v.as_u64())
2624 .map(|v| v as usize)
2625 {
2626 constraints.push(ValidationConstraint::MinLength { value: min_len });
2627 }
2628
2629 if let Some(max_len) = schema
2631 .get("maxLength")
2632 .and_then(|v| v.as_u64())
2633 .map(|v| v as usize)
2634 {
2635 constraints.push(ValidationConstraint::MaxLength { value: max_len });
2636 }
2637
2638 if let Some(pattern) = schema
2640 .get("pattern")
2641 .and_then(|v| v.as_str())
2642 .map(|s| s.to_string())
2643 {
2644 constraints.push(ValidationConstraint::Pattern { pattern });
2645 }
2646
2647 if let Some(enum_values) = schema.get("enum").and_then(|v| v.as_array()).cloned() {
2649 constraints.push(ValidationConstraint::EnumValues {
2650 values: enum_values,
2651 });
2652 }
2653
2654 if let Some(format) = schema
2656 .get("format")
2657 .and_then(|v| v.as_str())
2658 .map(|s| s.to_string())
2659 {
2660 constraints.push(ValidationConstraint::Format { format });
2661 }
2662
2663 if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
2665 constraints.push(ValidationConstraint::MultipleOf { value: multiple_of });
2666 }
2667
2668 if let Some(min_items) = schema
2670 .get("minItems")
2671 .and_then(|v| v.as_u64())
2672 .map(|v| v as usize)
2673 {
2674 constraints.push(ValidationConstraint::MinItems { value: min_items });
2675 }
2676
2677 if let Some(max_items) = schema
2679 .get("maxItems")
2680 .and_then(|v| v.as_u64())
2681 .map(|v| v as usize)
2682 {
2683 constraints.push(ValidationConstraint::MaxItems { value: max_items });
2684 }
2685
2686 if let Some(true) = schema.get("uniqueItems").and_then(|v| v.as_bool()) {
2688 constraints.push(ValidationConstraint::UniqueItems);
2689 }
2690
2691 if let Some(min_props) = schema
2693 .get("minProperties")
2694 .and_then(|v| v.as_u64())
2695 .map(|v| v as usize)
2696 {
2697 constraints.push(ValidationConstraint::MinProperties { value: min_props });
2698 }
2699
2700 if let Some(max_props) = schema
2702 .get("maxProperties")
2703 .and_then(|v| v.as_u64())
2704 .map(|v| v as usize)
2705 {
2706 constraints.push(ValidationConstraint::MaxProperties { value: max_props });
2707 }
2708
2709 if let Some(const_value) = schema.get("const").cloned() {
2711 constraints.push(ValidationConstraint::ConstValue { value: const_value });
2712 }
2713
2714 if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
2716 let properties: Vec<String> = required
2717 .iter()
2718 .filter_map(|v| v.as_str().map(|s| s.to_string()))
2719 .collect();
2720 if !properties.is_empty() {
2721 constraints.push(ValidationConstraint::Required { properties });
2722 }
2723 }
2724
2725 constraints
2726 }
2727
2728 fn get_expected_type(schema: &Value) -> Option<String> {
2730 if let Some(type_value) = schema.get("type") {
2731 if let Some(type_str) = type_value.as_str() {
2732 return Some(type_str.to_string());
2733 } else if let Some(type_array) = type_value.as_array() {
2734 let types: Vec<String> = type_array
2736 .iter()
2737 .filter_map(|v| v.as_str())
2738 .map(|s| s.to_string())
2739 .collect();
2740 if !types.is_empty() {
2741 return Some(types.join(" | "));
2742 }
2743 }
2744 }
2745 None
2746 }
2747
2748 fn wrap_output_schema(
2772 body_schema: &ObjectOrReference<ObjectSchema>,
2773 spec: &Spec,
2774 ) -> Result<Value, Error> {
2775 let mut visited = HashSet::new();
2777 let body_schema_json = match body_schema {
2778 ObjectOrReference::Object(obj_schema) => {
2779 Self::convert_object_schema_to_json_schema(obj_schema, spec, &mut visited)?
2780 }
2781 ObjectOrReference::Ref { ref_path, .. } => {
2782 let resolved = Self::resolve_reference(ref_path, spec, &mut visited)?;
2783 let result =
2784 Self::convert_object_schema_to_json_schema(&resolved, spec, &mut visited)?;
2785 visited.remove(ref_path);
2787 result
2788 }
2789 };
2790
2791 let error_schema = create_error_response_schema();
2792
2793 Ok(json!({
2794 "type": "object",
2795 "description": "Unified response structure with success and error variants",
2796 "required": ["status", "body"],
2797 "additionalProperties": false,
2798 "properties": {
2799 "status": {
2800 "type": "integer",
2801 "description": "HTTP status code",
2802 "minimum": 100,
2803 "maximum": 599
2804 },
2805 "body": {
2806 "description": "Response body - either success data or error information",
2807 "oneOf": [
2808 body_schema_json,
2809 error_schema
2810 ]
2811 }
2812 }
2813 }))
2814 }
2815
2816 #[must_use]
2827 pub fn is_file_field(schema: &Schema) -> bool {
2828 match schema {
2829 Schema::Object(obj_or_ref) => match obj_or_ref.as_ref() {
2830 ObjectOrReference::Object(obj_schema) => {
2831 Self::is_file_field_object_schema(obj_schema)
2832 }
2833 ObjectOrReference::Ref { .. } => {
2834 false
2836 }
2837 },
2838 Schema::Boolean(_) => false,
2839 }
2840 }
2841
2842 fn is_file_field_object_schema(obj_schema: &ObjectSchema) -> bool {
2847 if let Some(format) = &obj_schema.format {
2848 format == "binary" || format == "byte"
2849 } else {
2850 false
2851 }
2852 }
2853
2854 fn is_file_field_property(prop_schema: &ObjectOrReference<ObjectSchema>) -> bool {
2859 match prop_schema {
2860 ObjectOrReference::Object(obj_schema) => Self::is_file_field_object_schema(obj_schema),
2861 ObjectOrReference::Ref { .. } => {
2862 false
2864 }
2865 }
2866 }
2867
2868 fn convert_file_field_to_schema(original_description: Option<&str>) -> Value {
2880 let description = original_description.unwrap_or("File upload");
2881 json!({
2882 "type": "object",
2883 "description": description,
2884 "properties": {
2885 "content": {
2886 "type": "string",
2887 "description": "File content as data URI (e.g., data:image/png;base64,...)"
2888 },
2889 "filename": {
2890 "type": "string",
2891 "description": "Optional filename for the upload"
2892 }
2893 },
2894 "required": ["content"]
2895 })
2896 }
2897}
2898
2899fn create_error_response_schema() -> Value {
2901 let root_schema = schema_for!(ErrorResponse);
2902 let schema_json = serde_json::to_value(root_schema).expect("Valid error schema");
2903
2904 let definitions = schema_json
2906 .get("$defs")
2907 .or_else(|| schema_json.get("definitions"))
2908 .cloned()
2909 .unwrap_or_else(|| json!({}));
2910
2911 let mut result = schema_json.clone();
2913 if let Some(obj) = result.as_object_mut() {
2914 obj.remove("$schema");
2915 obj.remove("$defs");
2916 obj.remove("definitions");
2917 obj.remove("title");
2918 }
2919
2920 inline_refs(&mut result, &definitions);
2922
2923 result
2924}
2925
2926fn inline_refs(schema: &mut Value, definitions: &Value) {
2928 match schema {
2929 Value::Object(obj) => {
2930 if let Some(ref_value) = obj.get("$ref").cloned()
2932 && let Some(ref_str) = ref_value.as_str()
2933 {
2934 let def_name = ref_str
2936 .strip_prefix("#/$defs/")
2937 .or_else(|| ref_str.strip_prefix("#/definitions/"));
2938
2939 if let Some(name) = def_name
2940 && let Some(definition) = definitions.get(name)
2941 {
2942 *schema = definition.clone();
2944 inline_refs(schema, definitions);
2946 return;
2947 }
2948 }
2949
2950 for (_, value) in obj.iter_mut() {
2952 inline_refs(value, definitions);
2953 }
2954 }
2955 Value::Array(arr) => {
2956 for item in arr.iter_mut() {
2958 inline_refs(item, definitions);
2959 }
2960 }
2961 _ => {} }
2963}
2964
2965#[derive(Debug, Clone)]
2967pub struct QueryParameter {
2968 pub value: Value,
2969 pub explode: bool,
2970}
2971
2972impl QueryParameter {
2973 pub fn new(value: Value, explode: bool) -> Self {
2974 Self { value, explode }
2975 }
2976}
2977
2978#[derive(Debug, Clone)]
2980pub struct ExtractedParameters {
2981 pub path: HashMap<String, Value>,
2982 pub query: HashMap<String, QueryParameter>,
2983 pub headers: HashMap<String, Value>,
2984 pub cookies: HashMap<String, Value>,
2985 pub body: HashMap<String, Value>,
2986 pub config: RequestConfig,
2987}
2988
2989#[derive(Debug, Clone)]
2991pub struct RequestConfig {
2992 pub timeout_seconds: u32,
2993 pub content_type: String,
2994}
2995
2996impl Default for RequestConfig {
2997 fn default() -> Self {
2998 Self {
2999 timeout_seconds: 30,
3000 content_type: mime::APPLICATION_JSON.to_string(),
3001 }
3002 }
3003}
3004
3005#[cfg(test)]
3006mod tests {
3007 use super::*;
3008
3009 use insta::assert_json_snapshot;
3010 use oas3::spec::{
3011 BooleanSchema, Components, MediaType, ObjectOrReference, ObjectSchema, Operation,
3012 Parameter, ParameterIn, RequestBody, Schema, SchemaType, SchemaTypeSet, Spec,
3013 };
3014 use rmcp::model::Tool;
3015 use serde_json::{Value, json};
3016 use std::collections::BTreeMap;
3017
3018 #[test]
3019 fn converter_preserves_schema_level_examples_plural() {
3020 let spec = create_test_spec();
3021 let schema: ObjectSchema = serde_json::from_value(json!({
3022 "type": "string",
3023 "examples": ["a", "a.b", "a.b.c"],
3024 }))
3025 .expect("valid object schema");
3026 let mut visited = std::collections::HashSet::new();
3027 let result =
3028 ToolGenerator::convert_object_schema_to_json_schema(&schema, &spec, &mut visited)
3029 .expect("conversion succeeds");
3030 assert_eq!(result["type"], json!("string"));
3031 assert_eq!(
3032 result["examples"],
3033 json!(["a", "a.b", "a.b.c"]),
3034 "schema-level plural `examples` must be preserved: {result}"
3035 );
3036 }
3037
3038 fn parameter_with_singular_and_named_map_examples() -> Parameter {
3039 serde_json::from_value(json!({
3040 "name": "q",
3041 "in": "query",
3042 "schema": { "type": "string" },
3043 "example": "alpha",
3044 "examples": {
3045 "beta": { "value": "beta" },
3046 "gamma": { "value": "gamma" },
3047 },
3048 }))
3049 .expect("valid parameter")
3050 }
3051
3052 #[test]
3053 fn parameter_examples_default_to_structured_field() {
3054 let spec = create_test_spec();
3055 let param = parameter_with_singular_and_named_map_examples();
3056 let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3059 ¶m,
3060 ParameterIn::Query,
3061 &spec,
3062 false,
3063 false,
3064 )
3065 .expect("conversion succeeds");
3066 let values: Vec<String> = result["examples"]
3067 .as_array()
3068 .expect("structured `examples` present")
3069 .iter()
3070 .filter_map(|value| value.as_str().map(ToString::to_string))
3071 .collect();
3072 assert!(
3073 values.iter().any(|v| v == "alpha")
3074 && values.iter().any(|v| v == "beta")
3075 && values.iter().any(|v| v == "gamma"),
3076 "all sources chained into structured `examples`: {result}"
3077 );
3078 let description = result["description"].as_str().unwrap_or_default();
3079 assert!(
3080 !description.contains("alpha") && !description.contains("beta"),
3081 "examples must not be duplicated into the description by default: {description}"
3082 );
3083 }
3084
3085 #[test]
3086 fn parameter_examples_in_description_when_flag_set() {
3087 let spec = create_test_spec();
3088 let param = parameter_with_singular_and_named_map_examples();
3089 let (result, _annotations) =
3091 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, false, true)
3092 .expect("conversion succeeds");
3093 let description = result["description"].as_str().unwrap_or_default();
3094 assert!(
3095 description.contains("alpha")
3096 && description.contains("beta")
3097 && description.contains("gamma"),
3098 "examples folded into description: {description}"
3099 );
3100 assert!(
3101 result.get("examples").is_none(),
3102 "structured `examples` omitted when folding into the description: {result}"
3103 );
3104 }
3105
3106 #[test]
3107 fn array_parameter_lifts_item_examples_to_parameter_level() {
3108 let spec = create_test_spec();
3109 let param: Parameter = serde_json::from_value(json!({
3113 "name": "include",
3114 "in": "query",
3115 "schema": {
3116 "type": "array",
3117 "items": { "type": "string", "examples": ["camera", "mesh.primitives"] },
3118 },
3119 }))
3120 .expect("valid parameter");
3121 let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3122 ¶m,
3123 ParameterIn::Query,
3124 &spec,
3125 false,
3126 false,
3127 )
3128 .expect("conversion succeeds");
3129 assert_eq!(
3132 result["examples"],
3133 json!([["camera"], ["mesh.primitives"]]),
3134 "array element examples must be lifted to parameter-level examples: {result}"
3135 );
3136 }
3137
3138 #[test]
3139 fn lifting_array_item_examples_clears_them_from_items() {
3140 let spec = create_test_spec();
3141 let param: Parameter = serde_json::from_value(json!({
3142 "name": "include",
3143 "in": "query",
3144 "schema": {
3145 "type": "array",
3146 "items": { "type": "string", "examples": ["camera", "mesh.primitives"] },
3147 },
3148 }))
3149 .expect("valid parameter");
3150 let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3151 ¶m,
3152 ParameterIn::Query,
3153 &spec,
3154 false,
3155 false,
3156 )
3157 .expect("conversion succeeds");
3158 assert!(
3161 result["items"].get("examples").is_none(),
3162 "item-level examples must be cleared once lifted to the parameter level: {result}"
3163 );
3164 }
3165
3166 #[test]
3167 fn array_parameter_examples_lift_snapshot() {
3168 let spec = create_test_spec();
3169 let param: Parameter = serde_json::from_value(json!({
3173 "name": "include",
3174 "in": "query",
3175 "description": "Relationship paths to include.",
3176 "schema": {
3177 "type": "array",
3178 "items": {
3179 "type": "string",
3180 "description": "A relationship path: a dot-separated chain of relationship names.",
3181 "examples": ["camera", "mesh.primitives.material", "mesh.primitives.indices"],
3182 },
3183 },
3184 }))
3185 .expect("valid parameter");
3186 let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3187 ¶m,
3188 ParameterIn::Query,
3189 &spec,
3190 false,
3191 false,
3192 )
3193 .expect("conversion succeeds");
3194 assert_json_snapshot!("array_parameter_examples_lift", result);
3195 }
3196
3197 fn create_test_spec() -> Spec {
3199 Spec {
3200 openapi: "3.0.0".to_string(),
3201 info: oas3::spec::Info {
3202 title: "Test API".to_string(),
3203 version: "1.0.0".to_string(),
3204 summary: None,
3205 description: Some("Test API for unit tests".to_string()),
3206 terms_of_service: None,
3207 contact: None,
3208 license: None,
3209 extensions: Default::default(),
3210 },
3211 components: Some(Components {
3212 schemas: BTreeMap::new(),
3213 responses: BTreeMap::new(),
3214 parameters: BTreeMap::new(),
3215 examples: BTreeMap::new(),
3216 request_bodies: BTreeMap::new(),
3217 headers: BTreeMap::new(),
3218 security_schemes: BTreeMap::new(),
3219 links: BTreeMap::new(),
3220 callbacks: BTreeMap::new(),
3221 path_items: BTreeMap::new(),
3222 extensions: Default::default(),
3223 }),
3224 servers: vec![],
3225 paths: None,
3226 external_docs: None,
3227 tags: vec![],
3228 security: vec![],
3229 webhooks: BTreeMap::new(),
3230 extensions: Default::default(),
3231 }
3232 }
3233
3234 fn validate_tool_against_mcp_schema(metadata: &ToolMetadata) {
3235 let schema_content = std::fs::read_to_string("schema/2025-06-18/schema.json")
3236 .expect("Failed to read MCP schema file");
3237 let full_schema: Value =
3238 serde_json::from_str(&schema_content).expect("Failed to parse MCP schema JSON");
3239
3240 let tool_schema = json!({
3242 "$schema": "http://json-schema.org/draft-07/schema#",
3243 "definitions": full_schema.get("definitions"),
3244 "$ref": "#/definitions/Tool"
3245 });
3246
3247 let validator =
3248 jsonschema::validator_for(&tool_schema).expect("Failed to compile MCP Tool schema");
3249
3250 let tool = Tool::from(metadata);
3252
3253 let mcp_tool_json = serde_json::to_value(&tool).expect("Failed to serialize Tool to JSON");
3255
3256 let errors: Vec<String> = validator
3258 .iter_errors(&mcp_tool_json)
3259 .map(|e| e.to_string())
3260 .collect();
3261
3262 if !errors.is_empty() {
3263 panic!("Generated tool failed MCP schema validation: {errors:?}");
3264 }
3265 }
3266
3267 #[test]
3268 fn test_error_schema_structure() {
3269 let error_schema = create_error_response_schema();
3270
3271 assert!(error_schema.get("$schema").is_none());
3273 assert!(error_schema.get("definitions").is_none());
3274
3275 assert_json_snapshot!(error_schema);
3277 }
3278
3279 #[test]
3280 fn test_petstore_get_pet_by_id() {
3281 use oas3::spec::Response;
3282
3283 let mut operation = Operation {
3284 operation_id: Some("getPetById".to_string()),
3285 summary: Some("Find pet by ID".to_string()),
3286 description: Some("Returns a single pet".to_string()),
3287 tags: vec![],
3288 external_docs: None,
3289 parameters: vec![],
3290 request_body: None,
3291 responses: Default::default(),
3292 callbacks: Default::default(),
3293 deprecated: Some(false),
3294 security: vec![],
3295 servers: vec![],
3296 extensions: Default::default(),
3297 };
3298
3299 let param = Parameter {
3301 name: "petId".to_string(),
3302 location: ParameterIn::Path,
3303 description: Some("ID of pet to return".to_string()),
3304 required: Some(true),
3305 deprecated: Some(false),
3306 allow_empty_value: Some(false),
3307 style: None,
3308 explode: None,
3309 allow_reserved: Some(false),
3310 schema: Some(ObjectOrReference::Object(ObjectSchema {
3311 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3312 minimum: Some(serde_json::Number::from(1_i64)),
3313 format: Some("int64".to_string()),
3314 ..Default::default()
3315 })),
3316 example: None,
3317 examples: Default::default(),
3318 content: None,
3319 extensions: Default::default(),
3320 };
3321
3322 operation.parameters.push(ObjectOrReference::Object(param));
3323
3324 let mut responses = BTreeMap::new();
3326 let mut content = BTreeMap::new();
3327 content.insert(
3328 "application/json".to_string(),
3329 MediaType {
3330 extensions: Default::default(),
3331 schema: Some(ObjectOrReference::Object(ObjectSchema {
3332 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3333 properties: {
3334 let mut props = BTreeMap::new();
3335 props.insert(
3336 "id".to_string(),
3337 ObjectOrReference::Object(ObjectSchema {
3338 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3339 format: Some("int64".to_string()),
3340 ..Default::default()
3341 }),
3342 );
3343 props.insert(
3344 "name".to_string(),
3345 ObjectOrReference::Object(ObjectSchema {
3346 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3347 ..Default::default()
3348 }),
3349 );
3350 props.insert(
3351 "status".to_string(),
3352 ObjectOrReference::Object(ObjectSchema {
3353 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3354 ..Default::default()
3355 }),
3356 );
3357 props
3358 },
3359 required: vec!["id".to_string(), "name".to_string()],
3360 ..Default::default()
3361 })),
3362 examples: None,
3363 encoding: Default::default(),
3364 },
3365 );
3366
3367 responses.insert(
3368 "200".to_string(),
3369 ObjectOrReference::Object(Response {
3370 description: Some("successful operation".to_string()),
3371 headers: Default::default(),
3372 content,
3373 links: Default::default(),
3374 extensions: Default::default(),
3375 }),
3376 );
3377 operation.responses = Some(responses);
3378
3379 let spec = create_test_spec();
3380 let metadata = ToolGenerator::generate_tool_metadata(
3381 &operation,
3382 "get".to_string(),
3383 "/pet/{petId}".to_string(),
3384 &spec,
3385 false,
3386 false,
3387 false,
3388 )
3389 .unwrap();
3390
3391 assert_eq!(metadata.name, "getPetById");
3392 assert_eq!(metadata.method, "get");
3393 assert_eq!(metadata.path, "/pet/{petId}");
3394 assert!(
3395 metadata
3396 .description
3397 .clone()
3398 .unwrap()
3399 .contains("Find pet by ID")
3400 );
3401
3402 assert!(metadata.output_schema.is_some());
3404 let output_schema = metadata.output_schema.as_ref().unwrap();
3405
3406 insta::assert_json_snapshot!("test_petstore_get_pet_by_id_output_schema", output_schema);
3408
3409 validate_tool_against_mcp_schema(&metadata);
3411 }
3412
3413 #[test]
3414 fn test_convert_prefix_items_to_draft07_mixed_types() {
3415 let prefix_items = vec![
3418 ObjectOrReference::Object(ObjectSchema {
3419 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3420 format: Some("int32".to_string()),
3421 ..Default::default()
3422 }),
3423 ObjectOrReference::Object(ObjectSchema {
3424 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3425 ..Default::default()
3426 }),
3427 ];
3428
3429 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
3431
3432 let mut result = serde_json::Map::new();
3433 let spec = create_test_spec();
3434 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
3435 .unwrap();
3436
3437 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_mixed_types", result);
3439 }
3440
3441 #[test]
3442 fn test_convert_prefix_items_to_draft07_uniform_types() {
3443 let prefix_items = vec![
3445 ObjectOrReference::Object(ObjectSchema {
3446 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3447 ..Default::default()
3448 }),
3449 ObjectOrReference::Object(ObjectSchema {
3450 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3451 ..Default::default()
3452 }),
3453 ];
3454
3455 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
3457
3458 let mut result = serde_json::Map::new();
3459 let spec = create_test_spec();
3460 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
3461 .unwrap();
3462
3463 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_uniform_types", result);
3465 }
3466
3467 #[test]
3468 fn test_array_with_prefix_items_integration() {
3469 let param = Parameter {
3471 name: "coordinates".to_string(),
3472 location: ParameterIn::Query,
3473 description: Some("X,Y coordinates as tuple".to_string()),
3474 required: Some(true),
3475 deprecated: Some(false),
3476 allow_empty_value: Some(false),
3477 style: None,
3478 explode: None,
3479 allow_reserved: Some(false),
3480 schema: Some(ObjectOrReference::Object(ObjectSchema {
3481 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3482 prefix_items: vec![
3483 ObjectOrReference::Object(ObjectSchema {
3484 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
3485 format: Some("double".to_string()),
3486 ..Default::default()
3487 }),
3488 ObjectOrReference::Object(ObjectSchema {
3489 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
3490 format: Some("double".to_string()),
3491 ..Default::default()
3492 }),
3493 ],
3494 items: Some(Box::new(Schema::Boolean(BooleanSchema(false)))),
3495 ..Default::default()
3496 })),
3497 example: None,
3498 examples: Default::default(),
3499 content: None,
3500 extensions: Default::default(),
3501 };
3502
3503 let spec = create_test_spec();
3504 let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3505 ¶m,
3506 ParameterIn::Query,
3507 &spec,
3508 false,
3509 false,
3510 )
3511 .unwrap();
3512
3513 insta::assert_json_snapshot!("test_array_with_prefix_items_integration", result);
3515 }
3516
3517 #[test]
3518 fn test_skip_tool_description() {
3519 let operation = Operation {
3520 operation_id: Some("getPetById".to_string()),
3521 summary: Some("Find pet by ID".to_string()),
3522 description: Some("Returns a single pet".to_string()),
3523 tags: vec![],
3524 external_docs: None,
3525 parameters: vec![],
3526 request_body: None,
3527 responses: Default::default(),
3528 callbacks: Default::default(),
3529 deprecated: Some(false),
3530 security: vec![],
3531 servers: vec![],
3532 extensions: Default::default(),
3533 };
3534
3535 let spec = create_test_spec();
3536 let metadata = ToolGenerator::generate_tool_metadata(
3537 &operation,
3538 "get".to_string(),
3539 "/pet/{petId}".to_string(),
3540 &spec,
3541 true,
3542 false,
3543 false,
3544 )
3545 .unwrap();
3546
3547 assert_eq!(metadata.name, "getPetById");
3548 assert_eq!(metadata.method, "get");
3549 assert_eq!(metadata.path, "/pet/{petId}");
3550 assert!(metadata.description.is_none());
3551
3552 insta::assert_json_snapshot!("test_skip_tool_description", metadata);
3554
3555 validate_tool_against_mcp_schema(&metadata);
3557 }
3558
3559 #[test]
3560 fn test_keep_tool_description() {
3561 let description = Some("Returns a single pet".to_string());
3562 let operation = Operation {
3563 operation_id: Some("getPetById".to_string()),
3564 summary: Some("Find pet by ID".to_string()),
3565 description: description.clone(),
3566 tags: vec![],
3567 external_docs: None,
3568 parameters: vec![],
3569 request_body: None,
3570 responses: Default::default(),
3571 callbacks: Default::default(),
3572 deprecated: Some(false),
3573 security: vec![],
3574 servers: vec![],
3575 extensions: Default::default(),
3576 };
3577
3578 let spec = create_test_spec();
3579 let metadata = ToolGenerator::generate_tool_metadata(
3580 &operation,
3581 "get".to_string(),
3582 "/pet/{petId}".to_string(),
3583 &spec,
3584 false,
3585 false,
3586 false,
3587 )
3588 .unwrap();
3589
3590 assert_eq!(metadata.name, "getPetById");
3591 assert_eq!(metadata.method, "get");
3592 assert_eq!(metadata.path, "/pet/{petId}");
3593 assert!(metadata.description.is_some());
3594
3595 insta::assert_json_snapshot!("test_keep_tool_description", metadata);
3597
3598 validate_tool_against_mcp_schema(&metadata);
3600 }
3601
3602 #[test]
3603 fn test_skip_parameter_descriptions() {
3604 let param = Parameter {
3605 name: "status".to_string(),
3606 location: ParameterIn::Query,
3607 description: Some("Filter by status".to_string()),
3608 required: Some(false),
3609 deprecated: Some(false),
3610 allow_empty_value: Some(false),
3611 style: None,
3612 explode: None,
3613 allow_reserved: Some(false),
3614 schema: Some(ObjectOrReference::Object(ObjectSchema {
3615 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3616 enum_values: vec![json!("available"), json!("pending"), json!("sold")],
3617 ..Default::default()
3618 })),
3619 example: Some(json!("available")),
3620 examples: Default::default(),
3621 content: None,
3622 extensions: Default::default(),
3623 };
3624
3625 let spec = create_test_spec();
3626 let (schema, _) =
3627 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, true, false)
3628 .unwrap();
3629
3630 assert!(schema.get("description").is_none());
3632
3633 assert_eq!(schema.get("type").unwrap(), "string");
3636 assert!(schema.get("example").is_none());
3637 assert_eq!(schema.get("examples").unwrap(), &json!(["available"]));
3638
3639 insta::assert_json_snapshot!("test_skip_parameter_descriptions", schema);
3640 }
3641
3642 #[test]
3643 fn test_keep_parameter_descriptions() {
3644 let param = Parameter {
3645 name: "status".to_string(),
3646 location: ParameterIn::Query,
3647 description: Some("Filter by status".to_string()),
3648 required: Some(false),
3649 deprecated: Some(false),
3650 allow_empty_value: Some(false),
3651 style: None,
3652 explode: None,
3653 allow_reserved: Some(false),
3654 schema: Some(ObjectOrReference::Object(ObjectSchema {
3655 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3656 enum_values: vec![json!("available"), json!("pending"), json!("sold")],
3657 ..Default::default()
3658 })),
3659 example: Some(json!("available")),
3660 examples: Default::default(),
3661 content: None,
3662 extensions: Default::default(),
3663 };
3664
3665 let spec = create_test_spec();
3666 let (schema, _) = ToolGenerator::convert_parameter_schema(
3667 ¶m,
3668 ParameterIn::Query,
3669 &spec,
3670 false,
3671 false,
3672 )
3673 .unwrap();
3674
3675 assert!(schema.get("description").is_some());
3678 let description = schema.get("description").unwrap().as_str().unwrap();
3679 assert!(description.contains("Filter by status"));
3680 assert!(!description.contains("Example:"));
3681
3682 assert_eq!(schema.get("type").unwrap(), "string");
3684 assert!(schema.get("example").is_none());
3685 assert_eq!(schema.get("examples").unwrap(), &json!(["available"]));
3686
3687 insta::assert_json_snapshot!("test_keep_parameter_descriptions", schema);
3688 }
3689
3690 #[test]
3691 fn test_array_with_regular_items_schema() {
3692 let param = Parameter {
3694 name: "tags".to_string(),
3695 location: ParameterIn::Query,
3696 description: Some("List of tags".to_string()),
3697 required: Some(false),
3698 deprecated: Some(false),
3699 allow_empty_value: Some(false),
3700 style: None,
3701 explode: None,
3702 allow_reserved: Some(false),
3703 schema: Some(ObjectOrReference::Object(ObjectSchema {
3704 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3705 items: Some(Box::new(Schema::Object(Box::new(
3706 ObjectOrReference::Object(ObjectSchema {
3707 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3708 min_length: Some(1),
3709 max_length: Some(50),
3710 ..Default::default()
3711 }),
3712 )))),
3713 ..Default::default()
3714 })),
3715 example: None,
3716 examples: Default::default(),
3717 content: None,
3718 extensions: Default::default(),
3719 };
3720
3721 let spec = create_test_spec();
3722 let (result, _annotations) = ToolGenerator::convert_parameter_schema(
3723 ¶m,
3724 ParameterIn::Query,
3725 &spec,
3726 false,
3727 false,
3728 )
3729 .unwrap();
3730
3731 insta::assert_json_snapshot!("test_array_with_regular_items_schema", result);
3733 }
3734
3735 #[test]
3736 fn test_request_body_object_schema() {
3737 let operation = Operation {
3739 operation_id: Some("createPet".to_string()),
3740 summary: Some("Create a new pet".to_string()),
3741 description: Some("Creates a new pet in the store".to_string()),
3742 tags: vec![],
3743 external_docs: None,
3744 parameters: vec![],
3745 request_body: Some(ObjectOrReference::Object(RequestBody {
3746 description: Some("Pet object that needs to be added to the store".to_string()),
3747 content: {
3748 let mut content = BTreeMap::new();
3749 content.insert(
3750 "application/json".to_string(),
3751 MediaType {
3752 extensions: Default::default(),
3753 schema: Some(ObjectOrReference::Object(ObjectSchema {
3754 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3755 ..Default::default()
3756 })),
3757 examples: None,
3758 encoding: Default::default(),
3759 },
3760 );
3761 content
3762 },
3763 required: Some(true),
3764 })),
3765 responses: Default::default(),
3766 callbacks: Default::default(),
3767 deprecated: Some(false),
3768 security: vec![],
3769 servers: vec![],
3770 extensions: Default::default(),
3771 };
3772
3773 let spec = create_test_spec();
3774 let metadata = ToolGenerator::generate_tool_metadata(
3775 &operation,
3776 "post".to_string(),
3777 "/pets".to_string(),
3778 &spec,
3779 false,
3780 false,
3781 false,
3782 )
3783 .unwrap();
3784
3785 let properties = metadata
3787 .parameters
3788 .get("properties")
3789 .unwrap()
3790 .as_object()
3791 .unwrap();
3792 assert!(properties.contains_key("request_body"));
3793
3794 let required = metadata
3796 .parameters
3797 .get("required")
3798 .unwrap()
3799 .as_array()
3800 .unwrap();
3801 assert!(required.contains(&json!("request_body")));
3802
3803 let request_body_schema = properties.get("request_body").unwrap();
3805 insta::assert_json_snapshot!("test_request_body_object_schema", request_body_schema);
3806
3807 validate_tool_against_mcp_schema(&metadata);
3809 }
3810
3811 #[test]
3812 fn test_request_body_array_schema() {
3813 let operation = Operation {
3815 operation_id: Some("createPets".to_string()),
3816 summary: Some("Create multiple pets".to_string()),
3817 description: None,
3818 tags: vec![],
3819 external_docs: None,
3820 parameters: vec![],
3821 request_body: Some(ObjectOrReference::Object(RequestBody {
3822 description: Some("Array of pet objects".to_string()),
3823 content: {
3824 let mut content = BTreeMap::new();
3825 content.insert(
3826 "application/json".to_string(),
3827 MediaType {
3828 extensions: Default::default(),
3829 schema: Some(ObjectOrReference::Object(ObjectSchema {
3830 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3831 items: Some(Box::new(Schema::Object(Box::new(
3832 ObjectOrReference::Object(ObjectSchema {
3833 schema_type: Some(SchemaTypeSet::Single(
3834 SchemaType::Object,
3835 )),
3836 ..Default::default()
3837 }),
3838 )))),
3839 ..Default::default()
3840 })),
3841 examples: None,
3842 encoding: Default::default(),
3843 },
3844 );
3845 content
3846 },
3847 required: Some(false),
3848 })),
3849 responses: Default::default(),
3850 callbacks: Default::default(),
3851 deprecated: Some(false),
3852 security: vec![],
3853 servers: vec![],
3854 extensions: Default::default(),
3855 };
3856
3857 let spec = create_test_spec();
3858 let metadata = ToolGenerator::generate_tool_metadata(
3859 &operation,
3860 "post".to_string(),
3861 "/pets/batch".to_string(),
3862 &spec,
3863 false,
3864 false,
3865 false,
3866 )
3867 .unwrap();
3868
3869 let properties = metadata
3871 .parameters
3872 .get("properties")
3873 .unwrap()
3874 .as_object()
3875 .unwrap();
3876 assert!(properties.contains_key("request_body"));
3877
3878 let required = metadata
3880 .parameters
3881 .get("required")
3882 .unwrap()
3883 .as_array()
3884 .unwrap();
3885 assert!(!required.contains(&json!("request_body")));
3886
3887 let request_body_schema = properties.get("request_body").unwrap();
3889 insta::assert_json_snapshot!("test_request_body_array_schema", request_body_schema);
3890
3891 validate_tool_against_mcp_schema(&metadata);
3893 }
3894
3895 #[test]
3896 fn test_request_body_string_schema() {
3897 let operation = Operation {
3899 operation_id: Some("updatePetName".to_string()),
3900 summary: Some("Update pet name".to_string()),
3901 description: None,
3902 tags: vec![],
3903 external_docs: None,
3904 parameters: vec![],
3905 request_body: Some(ObjectOrReference::Object(RequestBody {
3906 description: None,
3907 content: {
3908 let mut content = BTreeMap::new();
3909 content.insert(
3910 "text/plain".to_string(),
3911 MediaType {
3912 extensions: Default::default(),
3913 schema: Some(ObjectOrReference::Object(ObjectSchema {
3914 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3915 min_length: Some(1),
3916 max_length: Some(100),
3917 ..Default::default()
3918 })),
3919 examples: None,
3920 encoding: Default::default(),
3921 },
3922 );
3923 content
3924 },
3925 required: Some(true),
3926 })),
3927 responses: Default::default(),
3928 callbacks: Default::default(),
3929 deprecated: Some(false),
3930 security: vec![],
3931 servers: vec![],
3932 extensions: Default::default(),
3933 };
3934
3935 let spec = create_test_spec();
3936 let metadata = ToolGenerator::generate_tool_metadata(
3937 &operation,
3938 "put".to_string(),
3939 "/pets/{petId}/name".to_string(),
3940 &spec,
3941 false,
3942 false,
3943 false,
3944 )
3945 .unwrap();
3946
3947 let properties = metadata
3949 .parameters
3950 .get("properties")
3951 .unwrap()
3952 .as_object()
3953 .unwrap();
3954 let request_body_schema = properties.get("request_body").unwrap();
3955 insta::assert_json_snapshot!("test_request_body_string_schema", request_body_schema);
3956
3957 validate_tool_against_mcp_schema(&metadata);
3959 }
3960
3961 #[test]
3962 fn test_request_body_ref_schema() {
3963 let operation = Operation {
3965 operation_id: Some("updatePet".to_string()),
3966 summary: Some("Update existing pet".to_string()),
3967 description: None,
3968 tags: vec![],
3969 external_docs: None,
3970 parameters: vec![],
3971 request_body: Some(ObjectOrReference::Ref {
3972 ref_path: "#/components/requestBodies/PetBody".to_string(),
3973 summary: None,
3974 description: None,
3975 }),
3976 responses: Default::default(),
3977 callbacks: Default::default(),
3978 deprecated: Some(false),
3979 security: vec![],
3980 servers: vec![],
3981 extensions: Default::default(),
3982 };
3983
3984 let spec = create_test_spec();
3985 let metadata = ToolGenerator::generate_tool_metadata(
3986 &operation,
3987 "put".to_string(),
3988 "/pets/{petId}".to_string(),
3989 &spec,
3990 false,
3991 false,
3992 false,
3993 )
3994 .unwrap();
3995
3996 let properties = metadata
3998 .parameters
3999 .get("properties")
4000 .unwrap()
4001 .as_object()
4002 .unwrap();
4003 let request_body_schema = properties.get("request_body").unwrap();
4004 insta::assert_json_snapshot!("test_request_body_ref_schema", request_body_schema);
4005
4006 validate_tool_against_mcp_schema(&metadata);
4008 }
4009
4010 #[test]
4011 fn test_no_request_body_for_get() {
4012 let operation = Operation {
4014 operation_id: Some("listPets".to_string()),
4015 summary: Some("List all pets".to_string()),
4016 description: None,
4017 tags: vec![],
4018 external_docs: None,
4019 parameters: vec![],
4020 request_body: None,
4021 responses: Default::default(),
4022 callbacks: Default::default(),
4023 deprecated: Some(false),
4024 security: vec![],
4025 servers: vec![],
4026 extensions: Default::default(),
4027 };
4028
4029 let spec = create_test_spec();
4030 let metadata = ToolGenerator::generate_tool_metadata(
4031 &operation,
4032 "get".to_string(),
4033 "/pets".to_string(),
4034 &spec,
4035 false,
4036 false,
4037 false,
4038 )
4039 .unwrap();
4040
4041 let properties = metadata
4043 .parameters
4044 .get("properties")
4045 .unwrap()
4046 .as_object()
4047 .unwrap();
4048 assert!(!properties.contains_key("request_body"));
4049
4050 validate_tool_against_mcp_schema(&metadata);
4052 }
4053
4054 #[test]
4055 fn test_request_body_simple_object_with_properties() {
4056 let operation = Operation {
4058 operation_id: Some("updatePetStatus".to_string()),
4059 summary: Some("Update pet status".to_string()),
4060 description: None,
4061 tags: vec![],
4062 external_docs: None,
4063 parameters: vec![],
4064 request_body: Some(ObjectOrReference::Object(RequestBody {
4065 description: Some("Pet status update".to_string()),
4066 content: {
4067 let mut content = BTreeMap::new();
4068 content.insert(
4069 "application/json".to_string(),
4070 MediaType {
4071 extensions: Default::default(),
4072 schema: Some(ObjectOrReference::Object(ObjectSchema {
4073 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4074 properties: {
4075 let mut props = BTreeMap::new();
4076 props.insert(
4077 "status".to_string(),
4078 ObjectOrReference::Object(ObjectSchema {
4079 schema_type: Some(SchemaTypeSet::Single(
4080 SchemaType::String,
4081 )),
4082 ..Default::default()
4083 }),
4084 );
4085 props.insert(
4086 "reason".to_string(),
4087 ObjectOrReference::Object(ObjectSchema {
4088 schema_type: Some(SchemaTypeSet::Single(
4089 SchemaType::String,
4090 )),
4091 ..Default::default()
4092 }),
4093 );
4094 props
4095 },
4096 required: vec!["status".to_string()],
4097 ..Default::default()
4098 })),
4099 examples: None,
4100 encoding: Default::default(),
4101 },
4102 );
4103 content
4104 },
4105 required: Some(false),
4106 })),
4107 responses: Default::default(),
4108 callbacks: Default::default(),
4109 deprecated: Some(false),
4110 security: vec![],
4111 servers: vec![],
4112 extensions: Default::default(),
4113 };
4114
4115 let spec = create_test_spec();
4116 let metadata = ToolGenerator::generate_tool_metadata(
4117 &operation,
4118 "patch".to_string(),
4119 "/pets/{petId}/status".to_string(),
4120 &spec,
4121 false,
4122 false,
4123 false,
4124 )
4125 .unwrap();
4126
4127 let properties = metadata
4129 .parameters
4130 .get("properties")
4131 .unwrap()
4132 .as_object()
4133 .unwrap();
4134 let request_body_schema = properties.get("request_body").unwrap();
4135 insta::assert_json_snapshot!(
4136 "test_request_body_simple_object_with_properties",
4137 request_body_schema
4138 );
4139
4140 let required = metadata
4142 .parameters
4143 .get("required")
4144 .unwrap()
4145 .as_array()
4146 .unwrap();
4147 assert!(!required.contains(&json!("request_body")));
4148
4149 validate_tool_against_mcp_schema(&metadata);
4151 }
4152
4153 #[test]
4154 fn test_request_body_with_nested_properties() {
4155 let operation = Operation {
4157 operation_id: Some("createUser".to_string()),
4158 summary: Some("Create a new user".to_string()),
4159 description: None,
4160 tags: vec![],
4161 external_docs: None,
4162 parameters: vec![],
4163 request_body: Some(ObjectOrReference::Object(RequestBody {
4164 description: Some("User creation data".to_string()),
4165 content: {
4166 let mut content = BTreeMap::new();
4167 content.insert(
4168 "application/json".to_string(),
4169 MediaType {
4170 extensions: Default::default(),
4171 schema: Some(ObjectOrReference::Object(ObjectSchema {
4172 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4173 properties: {
4174 let mut props = BTreeMap::new();
4175 props.insert(
4176 "name".to_string(),
4177 ObjectOrReference::Object(ObjectSchema {
4178 schema_type: Some(SchemaTypeSet::Single(
4179 SchemaType::String,
4180 )),
4181 ..Default::default()
4182 }),
4183 );
4184 props.insert(
4185 "age".to_string(),
4186 ObjectOrReference::Object(ObjectSchema {
4187 schema_type: Some(SchemaTypeSet::Single(
4188 SchemaType::Integer,
4189 )),
4190 minimum: Some(serde_json::Number::from(0)),
4191 maximum: Some(serde_json::Number::from(150)),
4192 ..Default::default()
4193 }),
4194 );
4195 props
4196 },
4197 required: vec!["name".to_string()],
4198 ..Default::default()
4199 })),
4200 examples: None,
4201 encoding: Default::default(),
4202 },
4203 );
4204 content
4205 },
4206 required: Some(true),
4207 })),
4208 responses: Default::default(),
4209 callbacks: Default::default(),
4210 deprecated: Some(false),
4211 security: vec![],
4212 servers: vec![],
4213 extensions: Default::default(),
4214 };
4215
4216 let spec = create_test_spec();
4217 let metadata = ToolGenerator::generate_tool_metadata(
4218 &operation,
4219 "post".to_string(),
4220 "/users".to_string(),
4221 &spec,
4222 false,
4223 false,
4224 false,
4225 )
4226 .unwrap();
4227
4228 let properties = metadata
4230 .parameters
4231 .get("properties")
4232 .unwrap()
4233 .as_object()
4234 .unwrap();
4235 let request_body_schema = properties.get("request_body").unwrap();
4236 insta::assert_json_snapshot!(
4237 "test_request_body_with_nested_properties",
4238 request_body_schema
4239 );
4240
4241 validate_tool_against_mcp_schema(&metadata);
4243 }
4244
4245 #[test]
4246 fn test_operation_without_responses_has_no_output_schema() {
4247 let operation = Operation {
4248 operation_id: Some("testOperation".to_string()),
4249 summary: Some("Test operation".to_string()),
4250 description: None,
4251 tags: vec![],
4252 external_docs: None,
4253 parameters: vec![],
4254 request_body: None,
4255 responses: None,
4256 callbacks: Default::default(),
4257 deprecated: Some(false),
4258 security: vec![],
4259 servers: vec![],
4260 extensions: Default::default(),
4261 };
4262
4263 let spec = create_test_spec();
4264 let metadata = ToolGenerator::generate_tool_metadata(
4265 &operation,
4266 "get".to_string(),
4267 "/test".to_string(),
4268 &spec,
4269 false,
4270 false,
4271 false,
4272 )
4273 .unwrap();
4274
4275 assert!(metadata.output_schema.is_none());
4277
4278 validate_tool_against_mcp_schema(&metadata);
4280 }
4281
4282 #[test]
4283 fn test_extract_output_schema_with_200_response() {
4284 use oas3::spec::Response;
4285
4286 let mut responses = BTreeMap::new();
4288 let mut content = BTreeMap::new();
4289 content.insert(
4290 "application/json".to_string(),
4291 MediaType {
4292 extensions: Default::default(),
4293 schema: Some(ObjectOrReference::Object(ObjectSchema {
4294 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4295 properties: {
4296 let mut props = BTreeMap::new();
4297 props.insert(
4298 "id".to_string(),
4299 ObjectOrReference::Object(ObjectSchema {
4300 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4301 ..Default::default()
4302 }),
4303 );
4304 props.insert(
4305 "name".to_string(),
4306 ObjectOrReference::Object(ObjectSchema {
4307 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4308 ..Default::default()
4309 }),
4310 );
4311 props
4312 },
4313 required: vec!["id".to_string(), "name".to_string()],
4314 ..Default::default()
4315 })),
4316 examples: None,
4317 encoding: Default::default(),
4318 },
4319 );
4320
4321 responses.insert(
4322 "200".to_string(),
4323 ObjectOrReference::Object(Response {
4324 description: Some("Successful response".to_string()),
4325 headers: Default::default(),
4326 content,
4327 links: Default::default(),
4328 extensions: Default::default(),
4329 }),
4330 );
4331
4332 let spec = create_test_spec();
4333 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4334
4335 insta::assert_json_snapshot!(result);
4337 }
4338
4339 #[test]
4340 fn test_extract_output_schema_with_201_response() {
4341 use oas3::spec::Response;
4342
4343 let mut responses = BTreeMap::new();
4345 let mut content = BTreeMap::new();
4346 content.insert(
4347 "application/json".to_string(),
4348 MediaType {
4349 extensions: Default::default(),
4350 schema: Some(ObjectOrReference::Object(ObjectSchema {
4351 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4352 properties: {
4353 let mut props = BTreeMap::new();
4354 props.insert(
4355 "created".to_string(),
4356 ObjectOrReference::Object(ObjectSchema {
4357 schema_type: Some(SchemaTypeSet::Single(SchemaType::Boolean)),
4358 ..Default::default()
4359 }),
4360 );
4361 props
4362 },
4363 ..Default::default()
4364 })),
4365 examples: None,
4366 encoding: Default::default(),
4367 },
4368 );
4369
4370 responses.insert(
4371 "201".to_string(),
4372 ObjectOrReference::Object(Response {
4373 description: Some("Created".to_string()),
4374 headers: Default::default(),
4375 content,
4376 links: Default::default(),
4377 extensions: Default::default(),
4378 }),
4379 );
4380
4381 let spec = create_test_spec();
4382 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4383
4384 insta::assert_json_snapshot!(result);
4386 }
4387
4388 #[test]
4389 fn test_extract_output_schema_with_2xx_response() {
4390 use oas3::spec::Response;
4391
4392 let mut responses = BTreeMap::new();
4394 let mut content = BTreeMap::new();
4395 content.insert(
4396 "application/json".to_string(),
4397 MediaType {
4398 extensions: Default::default(),
4399 schema: Some(ObjectOrReference::Object(ObjectSchema {
4400 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
4401 items: Some(Box::new(Schema::Object(Box::new(
4402 ObjectOrReference::Object(ObjectSchema {
4403 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4404 ..Default::default()
4405 }),
4406 )))),
4407 ..Default::default()
4408 })),
4409 examples: None,
4410 encoding: Default::default(),
4411 },
4412 );
4413
4414 responses.insert(
4415 "2XX".to_string(),
4416 ObjectOrReference::Object(Response {
4417 description: Some("Success".to_string()),
4418 headers: Default::default(),
4419 content,
4420 links: Default::default(),
4421 extensions: Default::default(),
4422 }),
4423 );
4424
4425 let spec = create_test_spec();
4426 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4427
4428 insta::assert_json_snapshot!(result);
4430 }
4431
4432 #[test]
4433 fn test_extract_output_schema_no_responses() {
4434 let spec = create_test_spec();
4435 let result = ToolGenerator::extract_output_schema(&None, &spec).unwrap();
4436
4437 insta::assert_json_snapshot!(result);
4439 }
4440
4441 #[test]
4442 fn test_extract_output_schema_only_error_responses() {
4443 use oas3::spec::Response;
4444
4445 let mut responses = BTreeMap::new();
4447 responses.insert(
4448 "404".to_string(),
4449 ObjectOrReference::Object(Response {
4450 description: Some("Not found".to_string()),
4451 headers: Default::default(),
4452 content: Default::default(),
4453 links: Default::default(),
4454 extensions: Default::default(),
4455 }),
4456 );
4457 responses.insert(
4458 "500".to_string(),
4459 ObjectOrReference::Object(Response {
4460 description: Some("Server error".to_string()),
4461 headers: Default::default(),
4462 content: Default::default(),
4463 links: Default::default(),
4464 extensions: Default::default(),
4465 }),
4466 );
4467
4468 let spec = create_test_spec();
4469 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4470
4471 insta::assert_json_snapshot!(result);
4473 }
4474
4475 #[test]
4476 fn test_extract_output_schema_with_ref() {
4477 use oas3::spec::Response;
4478
4479 let mut spec = create_test_spec();
4481 let mut schemas = BTreeMap::new();
4482 schemas.insert(
4483 "Pet".to_string(),
4484 ObjectOrReference::Object(ObjectSchema {
4485 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4486 properties: {
4487 let mut props = BTreeMap::new();
4488 props.insert(
4489 "name".to_string(),
4490 ObjectOrReference::Object(ObjectSchema {
4491 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4492 ..Default::default()
4493 }),
4494 );
4495 props
4496 },
4497 ..Default::default()
4498 }),
4499 );
4500 spec.components.as_mut().unwrap().schemas = schemas;
4501
4502 let mut responses = BTreeMap::new();
4504 let mut content = BTreeMap::new();
4505 content.insert(
4506 "application/json".to_string(),
4507 MediaType {
4508 extensions: Default::default(),
4509 schema: Some(ObjectOrReference::Ref {
4510 ref_path: "#/components/schemas/Pet".to_string(),
4511 summary: None,
4512 description: None,
4513 }),
4514 examples: None,
4515 encoding: Default::default(),
4516 },
4517 );
4518
4519 responses.insert(
4520 "200".to_string(),
4521 ObjectOrReference::Object(Response {
4522 description: Some("Success".to_string()),
4523 headers: Default::default(),
4524 content,
4525 links: Default::default(),
4526 extensions: Default::default(),
4527 }),
4528 );
4529
4530 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4531
4532 insta::assert_json_snapshot!(result);
4534 }
4535
4536 #[test]
4537 fn test_generate_tool_metadata_includes_output_schema() {
4538 use oas3::spec::Response;
4539
4540 let mut operation = Operation {
4541 operation_id: Some("getPet".to_string()),
4542 summary: Some("Get a pet".to_string()),
4543 description: None,
4544 tags: vec![],
4545 external_docs: None,
4546 parameters: vec![],
4547 request_body: None,
4548 responses: Default::default(),
4549 callbacks: Default::default(),
4550 deprecated: Some(false),
4551 security: vec![],
4552 servers: vec![],
4553 extensions: Default::default(),
4554 };
4555
4556 let mut responses = BTreeMap::new();
4558 let mut content = BTreeMap::new();
4559 content.insert(
4560 "application/json".to_string(),
4561 MediaType {
4562 extensions: Default::default(),
4563 schema: Some(ObjectOrReference::Object(ObjectSchema {
4564 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4565 properties: {
4566 let mut props = BTreeMap::new();
4567 props.insert(
4568 "id".to_string(),
4569 ObjectOrReference::Object(ObjectSchema {
4570 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4571 ..Default::default()
4572 }),
4573 );
4574 props
4575 },
4576 ..Default::default()
4577 })),
4578 examples: None,
4579 encoding: Default::default(),
4580 },
4581 );
4582
4583 responses.insert(
4584 "200".to_string(),
4585 ObjectOrReference::Object(Response {
4586 description: Some("Success".to_string()),
4587 headers: Default::default(),
4588 content,
4589 links: Default::default(),
4590 extensions: Default::default(),
4591 }),
4592 );
4593 operation.responses = Some(responses);
4594
4595 let spec = create_test_spec();
4596 let metadata = ToolGenerator::generate_tool_metadata(
4597 &operation,
4598 "get".to_string(),
4599 "/pets/{id}".to_string(),
4600 &spec,
4601 false,
4602 false,
4603 false,
4604 )
4605 .unwrap();
4606
4607 assert!(metadata.output_schema.is_some());
4609 let output_schema = metadata.output_schema.as_ref().unwrap();
4610
4611 insta::assert_json_snapshot!(
4613 "test_generate_tool_metadata_includes_output_schema",
4614 output_schema
4615 );
4616
4617 validate_tool_against_mcp_schema(&metadata);
4619 }
4620
4621 #[test]
4622 fn test_sanitize_property_name() {
4623 assert_eq!(sanitize_property_name("user name"), "user_name");
4625 assert_eq!(
4626 sanitize_property_name("first name last name"),
4627 "first_name_last_name"
4628 );
4629
4630 assert_eq!(sanitize_property_name("user(admin)"), "user_admin");
4632 assert_eq!(sanitize_property_name("user[admin]"), "user_admin");
4633 assert_eq!(sanitize_property_name("price($)"), "price");
4634 assert_eq!(sanitize_property_name("email@address"), "email_address");
4635 assert_eq!(sanitize_property_name("item#1"), "item_1");
4636 assert_eq!(sanitize_property_name("a/b/c"), "a_b_c");
4637
4638 assert_eq!(sanitize_property_name("user_name"), "user_name");
4640 assert_eq!(sanitize_property_name("userName123"), "userName123");
4641 assert_eq!(sanitize_property_name("user.name"), "user.name");
4642 assert_eq!(sanitize_property_name("user-name"), "user-name");
4643
4644 assert_eq!(sanitize_property_name("123name"), "param_123name");
4646 assert_eq!(sanitize_property_name("1st_place"), "param_1st_place");
4647
4648 assert_eq!(sanitize_property_name(""), "param_");
4650
4651 let long_name = "a".repeat(100);
4653 assert_eq!(sanitize_property_name(&long_name).len(), 64);
4654
4655 assert_eq!(sanitize_property_name("!@#$%^&*()"), "param_");
4658 }
4659
4660 #[test]
4661 fn test_sanitize_property_name_trailing_underscores() {
4662 assert_eq!(sanitize_property_name("page[size]"), "page_size");
4664 assert_eq!(sanitize_property_name("user[id]"), "user_id");
4665 assert_eq!(sanitize_property_name("field[]"), "field");
4666
4667 assert_eq!(sanitize_property_name("field___"), "field");
4669 assert_eq!(sanitize_property_name("test[[["), "test");
4670 }
4671
4672 #[test]
4673 fn test_sanitize_property_name_consecutive_underscores() {
4674 assert_eq!(sanitize_property_name("user__name"), "user_name");
4676 assert_eq!(sanitize_property_name("first___last"), "first_last");
4677 assert_eq!(sanitize_property_name("a____b____c"), "a_b_c");
4678
4679 assert_eq!(sanitize_property_name("user[[name]]"), "user_name");
4681 assert_eq!(sanitize_property_name("field@#$value"), "field_value");
4682 }
4683
4684 #[test]
4685 fn test_sanitize_property_name_edge_cases() {
4686 assert_eq!(sanitize_property_name("_private"), "_private");
4688 assert_eq!(sanitize_property_name("__dunder"), "_dunder");
4689
4690 assert_eq!(sanitize_property_name("[[["), "param_");
4692 assert_eq!(sanitize_property_name("@@@"), "param_");
4693
4694 assert_eq!(sanitize_property_name(""), "param_");
4696
4697 assert_eq!(sanitize_property_name("_field[size]"), "_field_size");
4699 assert_eq!(sanitize_property_name("__test__"), "_test");
4700 }
4701
4702 #[test]
4703 fn test_sanitize_property_name_complex_cases() {
4704 assert_eq!(sanitize_property_name("page[size]"), "page_size");
4706 assert_eq!(sanitize_property_name("filter[status]"), "filter_status");
4707 assert_eq!(
4708 sanitize_property_name("sort[-created_at]"),
4709 "sort_-created_at"
4710 );
4711 assert_eq!(
4712 sanitize_property_name("include[author.posts]"),
4713 "include_author.posts"
4714 );
4715
4716 let long_name = "very_long_field_name_with_special[characters]_that_needs_truncation_____";
4718 let expected = "very_long_field_name_with_special_characters_that_needs_truncat";
4719 assert_eq!(sanitize_property_name(long_name), expected);
4720 }
4721
4722 #[test]
4723 fn test_property_sanitization_with_annotations() {
4724 let spec = create_test_spec();
4725 let mut visited = HashSet::new();
4726
4727 let obj_schema = ObjectSchema {
4729 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4730 properties: {
4731 let mut props = BTreeMap::new();
4732 props.insert(
4734 "user name".to_string(),
4735 ObjectOrReference::Object(ObjectSchema {
4736 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4737 ..Default::default()
4738 }),
4739 );
4740 props.insert(
4742 "price($)".to_string(),
4743 ObjectOrReference::Object(ObjectSchema {
4744 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
4745 ..Default::default()
4746 }),
4747 );
4748 props.insert(
4750 "validName".to_string(),
4751 ObjectOrReference::Object(ObjectSchema {
4752 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4753 ..Default::default()
4754 }),
4755 );
4756 props
4757 },
4758 ..Default::default()
4759 };
4760
4761 let result =
4762 ToolGenerator::convert_object_schema_to_json_schema(&obj_schema, &spec, &mut visited)
4763 .unwrap();
4764
4765 insta::assert_json_snapshot!("test_property_sanitization_with_annotations", result);
4767 }
4768
4769 #[test]
4770 fn test_parameter_sanitization_and_extraction() {
4771 let spec = create_test_spec();
4772
4773 let operation = Operation {
4775 operation_id: Some("testOp".to_string()),
4776 parameters: vec![
4777 ObjectOrReference::Object(Parameter {
4779 name: "user(id)".to_string(),
4780 location: ParameterIn::Path,
4781 description: Some("User ID".to_string()),
4782 required: Some(true),
4783 deprecated: Some(false),
4784 allow_empty_value: Some(false),
4785 style: None,
4786 explode: None,
4787 allow_reserved: Some(false),
4788 schema: Some(ObjectOrReference::Object(ObjectSchema {
4789 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4790 ..Default::default()
4791 })),
4792 example: None,
4793 examples: Default::default(),
4794 content: None,
4795 extensions: Default::default(),
4796 }),
4797 ObjectOrReference::Object(Parameter {
4799 name: "page size".to_string(),
4800 location: ParameterIn::Query,
4801 description: Some("Page size".to_string()),
4802 required: Some(false),
4803 deprecated: Some(false),
4804 allow_empty_value: Some(false),
4805 style: None,
4806 explode: None,
4807 allow_reserved: Some(false),
4808 schema: Some(ObjectOrReference::Object(ObjectSchema {
4809 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4810 ..Default::default()
4811 })),
4812 example: None,
4813 examples: Default::default(),
4814 content: None,
4815 extensions: Default::default(),
4816 }),
4817 ObjectOrReference::Object(Parameter {
4819 name: "auth-token!".to_string(),
4820 location: ParameterIn::Header,
4821 description: Some("Auth token".to_string()),
4822 required: Some(false),
4823 deprecated: Some(false),
4824 allow_empty_value: Some(false),
4825 style: None,
4826 explode: None,
4827 allow_reserved: Some(false),
4828 schema: Some(ObjectOrReference::Object(ObjectSchema {
4829 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4830 ..Default::default()
4831 })),
4832 example: None,
4833 examples: Default::default(),
4834 content: None,
4835 extensions: Default::default(),
4836 }),
4837 ],
4838 ..Default::default()
4839 };
4840
4841 let tool_metadata = ToolGenerator::generate_tool_metadata(
4842 &operation,
4843 "get".to_string(),
4844 "/users/{user(id)}".to_string(),
4845 &spec,
4846 false,
4847 false,
4848 false,
4849 )
4850 .unwrap();
4851
4852 let properties = tool_metadata
4854 .parameters
4855 .get("properties")
4856 .unwrap()
4857 .as_object()
4858 .unwrap();
4859
4860 assert!(properties.contains_key("user_id"));
4861 assert!(properties.contains_key("page_size"));
4862 assert!(properties.contains_key("header_auth-token"));
4863
4864 let required = tool_metadata
4866 .parameters
4867 .get("required")
4868 .unwrap()
4869 .as_array()
4870 .unwrap();
4871 assert!(required.contains(&json!("user_id")));
4872
4873 let arguments = json!({
4875 "user_id": "123",
4876 "page_size": 10,
4877 "header_auth-token": "secret"
4878 });
4879
4880 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
4881
4882 assert_eq!(extracted.path.get("user(id)"), Some(&json!("123")));
4884
4885 assert_eq!(
4887 extracted.query.get("page size").map(|q| &q.value),
4888 Some(&json!(10))
4889 );
4890
4891 assert_eq!(extracted.headers.get("auth-token!"), Some(&json!("secret")));
4893 }
4894
4895 #[test]
4896 fn test_check_unknown_parameters() {
4897 let mut properties = serde_json::Map::new();
4899 properties.insert("page_size".to_string(), json!({"type": "integer"}));
4900 properties.insert("user_id".to_string(), json!({"type": "string"}));
4901
4902 let mut args = serde_json::Map::new();
4903 args.insert("page_sixe".to_string(), json!(10)); let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4906 assert!(!result.is_empty());
4907 assert_eq!(result.len(), 1);
4908
4909 match &result[0] {
4910 ValidationError::InvalidParameter {
4911 parameter,
4912 suggestions,
4913 valid_parameters,
4914 } => {
4915 assert_eq!(parameter, "page_sixe");
4916 assert_eq!(suggestions, &vec!["page_size".to_string()]);
4917 assert_eq!(
4918 valid_parameters,
4919 &vec!["page_size".to_string(), "user_id".to_string()]
4920 );
4921 }
4922 _ => panic!("Expected InvalidParameter variant"),
4923 }
4924 }
4925
4926 #[test]
4927 fn test_check_unknown_parameters_no_suggestions() {
4928 let mut properties = serde_json::Map::new();
4930 properties.insert("limit".to_string(), json!({"type": "integer"}));
4931 properties.insert("offset".to_string(), json!({"type": "integer"}));
4932
4933 let mut args = serde_json::Map::new();
4934 args.insert("xyz123".to_string(), json!("value"));
4935
4936 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4937 assert!(!result.is_empty());
4938 assert_eq!(result.len(), 1);
4939
4940 match &result[0] {
4941 ValidationError::InvalidParameter {
4942 parameter,
4943 suggestions,
4944 valid_parameters,
4945 } => {
4946 assert_eq!(parameter, "xyz123");
4947 assert!(suggestions.is_empty());
4948 assert!(valid_parameters.contains(&"limit".to_string()));
4949 assert!(valid_parameters.contains(&"offset".to_string()));
4950 }
4951 _ => panic!("Expected InvalidParameter variant"),
4952 }
4953 }
4954
4955 #[test]
4956 fn test_check_unknown_parameters_multiple_suggestions() {
4957 let mut properties = serde_json::Map::new();
4959 properties.insert("user_id".to_string(), json!({"type": "string"}));
4960 properties.insert("user_iid".to_string(), json!({"type": "string"}));
4961 properties.insert("user_name".to_string(), json!({"type": "string"}));
4962
4963 let mut args = serde_json::Map::new();
4964 args.insert("usr_id".to_string(), json!("123"));
4965
4966 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4967 assert!(!result.is_empty());
4968 assert_eq!(result.len(), 1);
4969
4970 match &result[0] {
4971 ValidationError::InvalidParameter {
4972 parameter,
4973 suggestions,
4974 valid_parameters,
4975 } => {
4976 assert_eq!(parameter, "usr_id");
4977 assert!(!suggestions.is_empty());
4978 assert!(suggestions.contains(&"user_id".to_string()));
4979 assert_eq!(valid_parameters.len(), 3);
4980 }
4981 _ => panic!("Expected InvalidParameter variant"),
4982 }
4983 }
4984
4985 #[test]
4986 fn test_check_unknown_parameters_valid() {
4987 let mut properties = serde_json::Map::new();
4989 properties.insert("name".to_string(), json!({"type": "string"}));
4990 properties.insert("email".to_string(), json!({"type": "string"}));
4991
4992 let mut args = serde_json::Map::new();
4993 args.insert("name".to_string(), json!("John"));
4994 args.insert("email".to_string(), json!("john@example.com"));
4995
4996 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4997 assert!(result.is_empty());
4998 }
4999
5000 #[test]
5001 fn test_check_unknown_parameters_empty() {
5002 let properties = serde_json::Map::new();
5004
5005 let mut args = serde_json::Map::new();
5006 args.insert("any_param".to_string(), json!("value"));
5007
5008 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
5009 assert!(!result.is_empty());
5010 assert_eq!(result.len(), 1);
5011
5012 match &result[0] {
5013 ValidationError::InvalidParameter {
5014 parameter,
5015 suggestions,
5016 valid_parameters,
5017 } => {
5018 assert_eq!(parameter, "any_param");
5019 assert!(suggestions.is_empty());
5020 assert!(valid_parameters.is_empty());
5021 }
5022 _ => panic!("Expected InvalidParameter variant"),
5023 }
5024 }
5025
5026 #[test]
5027 fn test_check_unknown_parameters_gltf_pagination() {
5028 let mut properties = serde_json::Map::new();
5030 properties.insert(
5031 "page_number".to_string(),
5032 json!({
5033 "type": "integer",
5034 "x-original-name": "page[number]"
5035 }),
5036 );
5037 properties.insert(
5038 "page_size".to_string(),
5039 json!({
5040 "type": "integer",
5041 "x-original-name": "page[size]"
5042 }),
5043 );
5044
5045 let mut args = serde_json::Map::new();
5047 args.insert("page".to_string(), json!(1));
5048 args.insert("per_page".to_string(), json!(10));
5049
5050 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
5051 assert_eq!(result.len(), 2, "Should have 2 unknown parameters");
5052
5053 let page_error = result
5055 .iter()
5056 .find(|e| {
5057 if let ValidationError::InvalidParameter { parameter, .. } = e {
5058 parameter == "page"
5059 } else {
5060 false
5061 }
5062 })
5063 .expect("Should have error for 'page'");
5064
5065 let per_page_error = result
5066 .iter()
5067 .find(|e| {
5068 if let ValidationError::InvalidParameter { parameter, .. } = e {
5069 parameter == "per_page"
5070 } else {
5071 false
5072 }
5073 })
5074 .expect("Should have error for 'per_page'");
5075
5076 match page_error {
5078 ValidationError::InvalidParameter {
5079 suggestions,
5080 valid_parameters,
5081 ..
5082 } => {
5083 assert!(
5084 suggestions.contains(&"page_number".to_string()),
5085 "Should suggest 'page_number' for 'page'"
5086 );
5087 assert_eq!(valid_parameters.len(), 2);
5088 assert!(valid_parameters.contains(&"page_number".to_string()));
5089 assert!(valid_parameters.contains(&"page_size".to_string()));
5090 }
5091 _ => panic!("Expected InvalidParameter"),
5092 }
5093
5094 match per_page_error {
5096 ValidationError::InvalidParameter {
5097 parameter,
5098 suggestions,
5099 valid_parameters,
5100 ..
5101 } => {
5102 assert_eq!(parameter, "per_page");
5103 assert_eq!(valid_parameters.len(), 2);
5104 if !suggestions.is_empty() {
5107 assert!(suggestions.contains(&"page_size".to_string()));
5108 }
5109 }
5110 _ => panic!("Expected InvalidParameter"),
5111 }
5112 }
5113
5114 #[test]
5115 fn test_validate_parameters_with_invalid_params() {
5116 let tool_metadata = ToolMetadata {
5118 name: "listItems".to_string(),
5119 title: None,
5120 description: Some("List items".to_string()),
5121 parameters: json!({
5122 "type": "object",
5123 "properties": {
5124 "page_number": {
5125 "type": "integer",
5126 "x-original-name": "page[number]"
5127 },
5128 "page_size": {
5129 "type": "integer",
5130 "x-original-name": "page[size]"
5131 }
5132 },
5133 "required": []
5134 }),
5135 output_schema: None,
5136 method: "GET".to_string(),
5137 path: "/items".to_string(),
5138 security: None,
5139 parameter_mappings: std::collections::HashMap::new(),
5140 };
5141
5142 let arguments = json!({
5144 "page": 1,
5145 "per_page": 10
5146 });
5147
5148 let result = ToolGenerator::validate_parameters(&tool_metadata, &arguments);
5149 assert!(
5150 result.is_err(),
5151 "Should fail validation with unknown parameters"
5152 );
5153
5154 let error = result.unwrap_err();
5155 match error {
5156 ToolCallValidationError::InvalidParameters { violations } => {
5157 assert_eq!(violations.len(), 2, "Should have 2 validation errors");
5158
5159 let has_page_error = violations.iter().any(|v| {
5161 if let ValidationError::InvalidParameter { parameter, .. } = v {
5162 parameter == "page"
5163 } else {
5164 false
5165 }
5166 });
5167
5168 let has_per_page_error = violations.iter().any(|v| {
5169 if let ValidationError::InvalidParameter { parameter, .. } = v {
5170 parameter == "per_page"
5171 } else {
5172 false
5173 }
5174 });
5175
5176 assert!(has_page_error, "Should have error for 'page' parameter");
5177 assert!(
5178 has_per_page_error,
5179 "Should have error for 'per_page' parameter"
5180 );
5181 }
5182 _ => panic!("Expected InvalidParameters"),
5183 }
5184 }
5185
5186 #[test]
5187 fn test_cookie_parameter_sanitization() {
5188 let spec = create_test_spec();
5189
5190 let operation = Operation {
5191 operation_id: Some("testCookie".to_string()),
5192 parameters: vec![ObjectOrReference::Object(Parameter {
5193 name: "session[id]".to_string(),
5194 location: ParameterIn::Cookie,
5195 description: Some("Session ID".to_string()),
5196 required: Some(false),
5197 deprecated: Some(false),
5198 allow_empty_value: Some(false),
5199 style: None,
5200 explode: None,
5201 allow_reserved: Some(false),
5202 schema: Some(ObjectOrReference::Object(ObjectSchema {
5203 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5204 ..Default::default()
5205 })),
5206 example: None,
5207 examples: Default::default(),
5208 content: None,
5209 extensions: Default::default(),
5210 })],
5211 ..Default::default()
5212 };
5213
5214 let tool_metadata = ToolGenerator::generate_tool_metadata(
5215 &operation,
5216 "get".to_string(),
5217 "/data".to_string(),
5218 &spec,
5219 false,
5220 false,
5221 false,
5222 )
5223 .unwrap();
5224
5225 let properties = tool_metadata
5226 .parameters
5227 .get("properties")
5228 .unwrap()
5229 .as_object()
5230 .unwrap();
5231
5232 assert!(properties.contains_key("cookie_session_id"));
5234
5235 let arguments = json!({
5237 "cookie_session_id": "abc123"
5238 });
5239
5240 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
5241
5242 assert_eq!(extracted.cookies.get("session[id]"), Some(&json!("abc123")));
5244 }
5245
5246 #[test]
5247 fn test_parameter_description_with_examples() {
5248 let spec = create_test_spec();
5249
5250 let param_with_example = Parameter {
5252 name: "status".to_string(),
5253 location: ParameterIn::Query,
5254 description: Some("Filter by status".to_string()),
5255 required: Some(false),
5256 deprecated: Some(false),
5257 allow_empty_value: Some(false),
5258 style: None,
5259 explode: None,
5260 allow_reserved: Some(false),
5261 schema: Some(ObjectOrReference::Object(ObjectSchema {
5262 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5263 ..Default::default()
5264 })),
5265 example: Some(json!("active")),
5266 examples: Default::default(),
5267 content: None,
5268 extensions: Default::default(),
5269 };
5270
5271 let (schema, _) = ToolGenerator::convert_parameter_schema(
5272 ¶m_with_example,
5273 ParameterIn::Query,
5274 &spec,
5275 false,
5276 true,
5277 )
5278 .unwrap();
5279 let description = schema.get("description").unwrap().as_str().unwrap();
5280 assert_eq!(description, "Filter by status. Example: `\"active\"`");
5281
5282 let mut examples_map = std::collections::BTreeMap::new();
5284 examples_map.insert(
5285 "example1".to_string(),
5286 ObjectOrReference::Object(oas3::spec::Example {
5287 value: Some(json!("pending")),
5288 ..Default::default()
5289 }),
5290 );
5291 examples_map.insert(
5292 "example2".to_string(),
5293 ObjectOrReference::Object(oas3::spec::Example {
5294 value: Some(json!("completed")),
5295 ..Default::default()
5296 }),
5297 );
5298
5299 let param_with_examples = Parameter {
5300 name: "status".to_string(),
5301 location: ParameterIn::Query,
5302 description: Some("Filter by status".to_string()),
5303 required: Some(false),
5304 deprecated: Some(false),
5305 allow_empty_value: Some(false),
5306 style: None,
5307 explode: None,
5308 allow_reserved: Some(false),
5309 schema: Some(ObjectOrReference::Object(ObjectSchema {
5310 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5311 ..Default::default()
5312 })),
5313 example: None,
5314 examples: examples_map,
5315 content: None,
5316 extensions: Default::default(),
5317 };
5318
5319 let (schema, _) = ToolGenerator::convert_parameter_schema(
5320 ¶m_with_examples,
5321 ParameterIn::Query,
5322 &spec,
5323 false,
5324 true,
5325 )
5326 .unwrap();
5327 let description = schema.get("description").unwrap().as_str().unwrap();
5328 assert!(description.starts_with("Filter by status. Examples:\n"));
5329 assert!(description.contains("`\"pending\"`"));
5330 assert!(description.contains("`\"completed\"`"));
5331
5332 let param_no_desc = Parameter {
5334 name: "limit".to_string(),
5335 location: ParameterIn::Query,
5336 description: None,
5337 required: Some(false),
5338 deprecated: Some(false),
5339 allow_empty_value: Some(false),
5340 style: None,
5341 explode: None,
5342 allow_reserved: Some(false),
5343 schema: Some(ObjectOrReference::Object(ObjectSchema {
5344 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
5345 ..Default::default()
5346 })),
5347 example: Some(json!(100)),
5348 examples: Default::default(),
5349 content: None,
5350 extensions: Default::default(),
5351 };
5352
5353 let (schema, _) = ToolGenerator::convert_parameter_schema(
5354 ¶m_no_desc,
5355 ParameterIn::Query,
5356 &spec,
5357 false,
5358 true,
5359 )
5360 .unwrap();
5361 let description = schema.get("description").unwrap().as_str().unwrap();
5362 assert_eq!(description, "limit parameter. Example: `100`");
5363 }
5364
5365 #[test]
5366 fn test_format_examples_for_description() {
5367 let examples = vec![json!("active")];
5369 let result = ToolGenerator::format_examples_for_description(&examples);
5370 assert_eq!(result, Some("Example: `\"active\"`".to_string()));
5371
5372 let examples = vec![json!(42)];
5374 let result = ToolGenerator::format_examples_for_description(&examples);
5375 assert_eq!(result, Some("Example: `42`".to_string()));
5376
5377 let examples = vec![json!(true)];
5379 let result = ToolGenerator::format_examples_for_description(&examples);
5380 assert_eq!(result, Some("Example: `true`".to_string()));
5381
5382 let examples = vec![json!("active"), json!("pending"), json!("completed")];
5384 let result = ToolGenerator::format_examples_for_description(&examples);
5385 assert_eq!(
5386 result,
5387 Some("Examples:\n- `\"active\"`\n- `\"pending\"`\n- `\"completed\"`".to_string())
5388 );
5389
5390 let examples = vec![json!(["a", "b", "c"])];
5392 let result = ToolGenerator::format_examples_for_description(&examples);
5393 assert_eq!(result, Some("Example: `[\"a\",\"b\",\"c\"]`".to_string()));
5394
5395 let examples = vec![json!({"key": "value"})];
5397 let result = ToolGenerator::format_examples_for_description(&examples);
5398 assert_eq!(result, Some("Example: `{\"key\":\"value\"}`".to_string()));
5399
5400 let examples = vec![];
5402 let result = ToolGenerator::format_examples_for_description(&examples);
5403 assert_eq!(result, None);
5404
5405 let examples = vec![json!(null)];
5407 let result = ToolGenerator::format_examples_for_description(&examples);
5408 assert_eq!(result, Some("Example: `null`".to_string()));
5409
5410 let examples = vec![json!("text"), json!(123), json!(true)];
5412 let result = ToolGenerator::format_examples_for_description(&examples);
5413 assert_eq!(
5414 result,
5415 Some("Examples:\n- `\"text\"`\n- `123`\n- `true`".to_string())
5416 );
5417
5418 let examples = vec![json!(["a", "b", "c", "d", "e", "f"])];
5420 let result = ToolGenerator::format_examples_for_description(&examples);
5421 assert_eq!(
5422 result,
5423 Some("Example: `[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\"]`".to_string())
5424 );
5425
5426 let examples = vec![json!([1, 2])];
5428 let result = ToolGenerator::format_examples_for_description(&examples);
5429 assert_eq!(result, Some("Example: `[1,2]`".to_string()));
5430
5431 let examples = vec![json!({"user": {"name": "John", "age": 30}})];
5433 let result = ToolGenerator::format_examples_for_description(&examples);
5434 assert_eq!(
5435 result,
5436 Some("Example: `{\"user\":{\"name\":\"John\",\"age\":30}}`".to_string())
5437 );
5438
5439 let examples = vec![json!("a"), json!("b"), json!("c"), json!("d"), json!("e")];
5441 let result = ToolGenerator::format_examples_for_description(&examples);
5442 assert_eq!(
5443 result,
5444 Some("Examples:\n- `\"a\"`\n- `\"b\"`\n- `\"c\"`\n- `\"d\"`\n- `\"e\"`".to_string())
5445 );
5446
5447 let examples = vec![json!(3.5)];
5449 let result = ToolGenerator::format_examples_for_description(&examples);
5450 assert_eq!(result, Some("Example: `3.5`".to_string()));
5451
5452 let examples = vec![json!(-42)];
5454 let result = ToolGenerator::format_examples_for_description(&examples);
5455 assert_eq!(result, Some("Example: `-42`".to_string()));
5456
5457 let examples = vec![json!(false)];
5459 let result = ToolGenerator::format_examples_for_description(&examples);
5460 assert_eq!(result, Some("Example: `false`".to_string()));
5461
5462 let examples = vec![json!("hello \"world\"")];
5464 let result = ToolGenerator::format_examples_for_description(&examples);
5465 assert_eq!(result, Some(r#"Example: `"hello \"world\""`"#.to_string()));
5467
5468 let examples = vec![json!("")];
5470 let result = ToolGenerator::format_examples_for_description(&examples);
5471 assert_eq!(result, Some("Example: `\"\"`".to_string()));
5472
5473 let examples = vec![json!([])];
5475 let result = ToolGenerator::format_examples_for_description(&examples);
5476 assert_eq!(result, Some("Example: `[]`".to_string()));
5477
5478 let examples = vec![json!({})];
5480 let result = ToolGenerator::format_examples_for_description(&examples);
5481 assert_eq!(result, Some("Example: `{}`".to_string()));
5482 }
5483
5484 #[test]
5485 fn test_reference_metadata_functionality() {
5486 let metadata = ReferenceMetadata::new(
5488 Some("User Reference".to_string()),
5489 Some("A reference to user data with additional context".to_string()),
5490 );
5491
5492 assert!(!metadata.is_empty());
5493 assert_eq!(metadata.summary(), Some("User Reference"));
5494 assert_eq!(
5495 metadata.best_description(),
5496 Some("A reference to user data with additional context")
5497 );
5498
5499 let summary_only = ReferenceMetadata::new(Some("Pet Summary".to_string()), None);
5501 assert_eq!(summary_only.best_description(), Some("Pet Summary"));
5502
5503 let empty_metadata = ReferenceMetadata::new(None, None);
5505 assert!(empty_metadata.is_empty());
5506 assert_eq!(empty_metadata.best_description(), None);
5507
5508 let metadata = ReferenceMetadata::new(
5510 Some("Reference Summary".to_string()),
5511 Some("Reference Description".to_string()),
5512 );
5513
5514 let result = metadata.merge_with_description(None, false);
5516 assert_eq!(result, Some("Reference Description".to_string()));
5517
5518 let result = metadata.merge_with_description(Some("Existing desc"), false);
5520 assert_eq!(result, Some("Reference Description".to_string()));
5521
5522 let result = metadata.merge_with_description(Some("Existing desc"), true);
5524 assert_eq!(result, Some("Reference Description".to_string()));
5525
5526 let result = metadata.enhance_parameter_description("userId", Some("User ID parameter"));
5528 assert_eq!(result, Some("userId: Reference Description".to_string()));
5529
5530 let result = metadata.enhance_parameter_description("userId", None);
5531 assert_eq!(result, Some("userId: Reference Description".to_string()));
5532
5533 let summary_only = ReferenceMetadata::new(Some("API Token".to_string()), None);
5535
5536 let result = summary_only.merge_with_description(Some("Generic token"), false);
5537 assert_eq!(result, Some("API Token".to_string()));
5538
5539 let result = summary_only.merge_with_description(Some("Different desc"), true);
5540 assert_eq!(result, Some("API Token".to_string())); let result = summary_only.enhance_parameter_description("token", Some("Token field"));
5543 assert_eq!(result, Some("token: API Token".to_string()));
5544
5545 let empty_meta = ReferenceMetadata::new(None, None);
5547
5548 let result = empty_meta.merge_with_description(Some("Schema description"), false);
5549 assert_eq!(result, Some("Schema description".to_string()));
5550
5551 let result = empty_meta.enhance_parameter_description("param", Some("Schema param"));
5552 assert_eq!(result, Some("Schema param".to_string()));
5553
5554 let result = empty_meta.enhance_parameter_description("param", None);
5555 assert_eq!(result, Some("param parameter".to_string()));
5556 }
5557
5558 #[test]
5559 fn test_parameter_schema_with_reference_metadata() {
5560 let mut spec = create_test_spec();
5561
5562 spec.components.as_mut().unwrap().schemas.insert(
5564 "Pet".to_string(),
5565 ObjectOrReference::Object(ObjectSchema {
5566 description: None, schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5568 ..Default::default()
5569 }),
5570 );
5571
5572 let param_with_ref = Parameter {
5574 name: "user".to_string(),
5575 location: ParameterIn::Query,
5576 description: None,
5577 required: Some(true),
5578 deprecated: Some(false),
5579 allow_empty_value: Some(false),
5580 style: None,
5581 explode: None,
5582 allow_reserved: Some(false),
5583 schema: Some(ObjectOrReference::Ref {
5584 ref_path: "#/components/schemas/Pet".to_string(),
5585 summary: Some("Pet Reference".to_string()),
5586 description: Some("A reference to pet schema with additional context".to_string()),
5587 }),
5588 example: None,
5589 examples: BTreeMap::new(),
5590 content: None,
5591 extensions: Default::default(),
5592 };
5593
5594 let result = ToolGenerator::convert_parameter_schema(
5596 ¶m_with_ref,
5597 ParameterIn::Query,
5598 &spec,
5599 false,
5600 false,
5601 );
5602
5603 assert!(result.is_ok());
5604 let (schema, _annotations) = result.unwrap();
5605
5606 let description = schema.get("description").and_then(|v| v.as_str());
5608 assert!(description.is_some());
5609 assert!(
5611 description.unwrap().contains("Pet Reference")
5612 || description
5613 .unwrap()
5614 .contains("A reference to pet schema with additional context")
5615 );
5616 }
5617
5618 #[test]
5619 fn test_request_body_with_reference_metadata() {
5620 let spec = create_test_spec();
5621
5622 let request_body_ref = ObjectOrReference::Ref {
5624 ref_path: "#/components/requestBodies/PetBody".to_string(),
5625 summary: Some("Pet Request Body".to_string()),
5626 description: Some(
5627 "Request body containing pet information for API operations".to_string(),
5628 ),
5629 };
5630
5631 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body_ref, &spec);
5632
5633 assert!(result.is_ok());
5634 let schema_result = result.unwrap();
5635 assert!(schema_result.is_some());
5636
5637 let (schema, _annotations, _required) = schema_result.unwrap();
5638 let description = schema.get("description").and_then(|v| v.as_str());
5639
5640 assert!(description.is_some());
5641 assert_eq!(
5643 description.unwrap(),
5644 "Request body containing pet information for API operations"
5645 );
5646 }
5647
5648 #[test]
5649 fn test_response_schema_with_reference_metadata() {
5650 let spec = create_test_spec();
5651
5652 let mut responses = BTreeMap::new();
5654 responses.insert(
5655 "200".to_string(),
5656 ObjectOrReference::Ref {
5657 ref_path: "#/components/responses/PetResponse".to_string(),
5658 summary: Some("Successful Pet Response".to_string()),
5659 description: Some(
5660 "Response containing pet data on successful operation".to_string(),
5661 ),
5662 },
5663 );
5664 let responses_option = Some(responses);
5665
5666 let result = ToolGenerator::extract_output_schema(&responses_option, &spec);
5667
5668 assert!(result.is_ok());
5669 let schema = result.unwrap();
5670 assert!(schema.is_some());
5671
5672 let schema_value = schema.unwrap();
5673 let body_desc = schema_value
5674 .get("properties")
5675 .and_then(|props| props.get("body"))
5676 .and_then(|body| body.get("description"))
5677 .and_then(|desc| desc.as_str());
5678
5679 assert!(body_desc.is_some());
5680 assert_eq!(
5682 body_desc.unwrap(),
5683 "Response containing pet data on successful operation"
5684 );
5685 }
5686
5687 #[test]
5688 fn test_self_referencing_schema_does_not_overflow() {
5689 let mut spec = create_test_spec();
5692
5693 let node_schema = ObjectSchema {
5695 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5696 properties: {
5697 let mut props = BTreeMap::new();
5698 props.insert(
5699 "name".to_string(),
5700 ObjectOrReference::Object(ObjectSchema {
5701 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5702 ..Default::default()
5703 }),
5704 );
5705 props.insert(
5707 "children".to_string(),
5708 ObjectOrReference::Object(ObjectSchema {
5709 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
5710 items: Some(Box::new(Schema::Object(Box::new(ObjectOrReference::Ref {
5711 ref_path: "#/components/schemas/Node".to_string(),
5712 summary: None,
5713 description: None,
5714 })))),
5715 ..Default::default()
5716 }),
5717 );
5718 props
5719 },
5720 ..Default::default()
5721 };
5722
5723 if let Some(ref mut components) = spec.components {
5725 components
5726 .schemas
5727 .insert("Node".to_string(), ObjectOrReference::Object(node_schema));
5728 }
5729
5730 let mut visited = HashSet::new();
5732 let result = ToolGenerator::convert_schema_to_json_schema(
5733 &Schema::Object(Box::new(ObjectOrReference::Ref {
5734 ref_path: "#/components/schemas/Node".to_string(),
5735 summary: None,
5736 description: None,
5737 })),
5738 &spec,
5739 &mut visited,
5740 );
5741
5742 assert!(
5744 result.is_err(),
5745 "Expected circular reference error, got: {result:?}"
5746 );
5747 let error = result.unwrap_err();
5748 assert!(
5749 error.to_string().contains("Circular reference"),
5750 "Expected circular reference error message, got: {error}"
5751 );
5752 }
5753
5754 #[test]
5757 fn test_multipart_form_data_with_single_file() {
5758 let request_body = ObjectOrReference::Object(RequestBody {
5761 description: Some("File upload request".to_string()),
5762 content: {
5763 let mut content = BTreeMap::new();
5764 content.insert(
5765 "multipart/form-data".to_string(),
5766 MediaType {
5767 extensions: Default::default(),
5768 schema: Some(ObjectOrReference::Object(ObjectSchema {
5769 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5770 properties: {
5771 let mut props = BTreeMap::new();
5772 props.insert(
5773 "file".to_string(),
5774 ObjectOrReference::Object(ObjectSchema {
5775 schema_type: Some(SchemaTypeSet::Single(
5776 SchemaType::String,
5777 )),
5778 format: Some("binary".to_string()),
5779 description: Some("The file to upload".to_string()),
5780 ..Default::default()
5781 }),
5782 );
5783 props
5784 },
5785 required: vec!["file".to_string()],
5786 ..Default::default()
5787 })),
5788 examples: None,
5789 encoding: Default::default(),
5790 },
5791 );
5792 content
5793 },
5794 required: Some(true),
5795 });
5796
5797 let spec = create_test_spec();
5798 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
5799 .unwrap()
5800 .unwrap();
5801
5802 let (schema, annotations, is_required) = result;
5803
5804 let schema_obj = schema.as_object().unwrap();
5806 assert_eq!(schema_obj.get("type").unwrap(), "object");
5807
5808 let file_schema = schema_obj.get("properties").unwrap().get("file").unwrap();
5810
5811 assert_eq!(file_schema.get("type").unwrap(), "object");
5813 assert!(
5814 file_schema
5815 .get("properties")
5816 .unwrap()
5817 .get("content")
5818 .is_some()
5819 );
5820 assert!(
5821 file_schema
5822 .get("properties")
5823 .unwrap()
5824 .get("filename")
5825 .is_some()
5826 );
5827 assert!(
5828 file_schema
5829 .get("required")
5830 .unwrap()
5831 .as_array()
5832 .unwrap()
5833 .contains(&json!("content"))
5834 );
5835
5836 let annotations_value = serde_json::to_value(&annotations).unwrap();
5838 let annotations_obj = annotations_value.as_object().unwrap();
5839
5840 assert_eq!(
5842 annotations_obj.get("x-content-type").unwrap(),
5843 "multipart/form-data"
5844 );
5845
5846 let x_file_fields = annotations_obj
5848 .get("x-file-fields")
5849 .unwrap()
5850 .as_array()
5851 .unwrap();
5852 assert_eq!(x_file_fields.len(), 1);
5853 assert!(x_file_fields.contains(&json!("file")));
5854
5855 assert!(is_required);
5857
5858 insta::assert_json_snapshot!("test_multipart_form_data_with_single_file", schema);
5860 }
5861
5862 #[test]
5863 fn test_multipart_form_data_with_multiple_files() {
5864 let request_body = ObjectOrReference::Object(RequestBody {
5866 description: Some("Multiple file upload request".to_string()),
5867 content: {
5868 let mut content = BTreeMap::new();
5869 content.insert(
5870 "multipart/form-data".to_string(),
5871 MediaType {
5872 extensions: Default::default(),
5873 schema: Some(ObjectOrReference::Object(ObjectSchema {
5874 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5875 properties: {
5876 let mut props = BTreeMap::new();
5877 props.insert(
5878 "avatar".to_string(),
5879 ObjectOrReference::Object(ObjectSchema {
5880 schema_type: Some(SchemaTypeSet::Single(
5881 SchemaType::String,
5882 )),
5883 format: Some("binary".to_string()),
5884 description: Some("Profile avatar image".to_string()),
5885 ..Default::default()
5886 }),
5887 );
5888 props.insert(
5889 "document".to_string(),
5890 ObjectOrReference::Object(ObjectSchema {
5891 schema_type: Some(SchemaTypeSet::Single(
5892 SchemaType::String,
5893 )),
5894 format: Some("binary".to_string()),
5895 description: Some("Supporting document".to_string()),
5896 ..Default::default()
5897 }),
5898 );
5899 props.insert(
5900 "resume".to_string(),
5901 ObjectOrReference::Object(ObjectSchema {
5902 schema_type: Some(SchemaTypeSet::Single(
5903 SchemaType::String,
5904 )),
5905 format: Some("binary".to_string()),
5906 description: Some("Resume file".to_string()),
5907 ..Default::default()
5908 }),
5909 );
5910 props
5911 },
5912 required: vec!["avatar".to_string(), "resume".to_string()],
5913 ..Default::default()
5914 })),
5915 examples: None,
5916 encoding: Default::default(),
5917 },
5918 );
5919 content
5920 },
5921 required: Some(true),
5922 });
5923
5924 let spec = create_test_spec();
5925 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
5926 .unwrap()
5927 .unwrap();
5928
5929 let (schema, annotations, _is_required) = result;
5930
5931 let body_properties = schema.get("properties").unwrap();
5933 for field_name in ["avatar", "document", "resume"] {
5934 let field_schema = body_properties.get(field_name).unwrap();
5935 assert_eq!(
5936 field_schema.get("type").unwrap(),
5937 "object",
5938 "Field {field_name} should be transformed to object type"
5939 );
5940 assert!(
5941 field_schema
5942 .get("properties")
5943 .unwrap()
5944 .get("content")
5945 .is_some(),
5946 "Field {field_name} should have content property"
5947 );
5948 }
5949
5950 let annotations_value = serde_json::to_value(&annotations).unwrap();
5952 let annotations_obj = annotations_value.as_object().unwrap();
5953
5954 let x_file_fields = annotations_obj
5955 .get("x-file-fields")
5956 .unwrap()
5957 .as_array()
5958 .unwrap();
5959 assert_eq!(x_file_fields.len(), 3);
5960 assert!(x_file_fields.contains(&json!("avatar")));
5961 assert!(x_file_fields.contains(&json!("document")));
5962 assert!(x_file_fields.contains(&json!("resume")));
5963
5964 insta::assert_json_snapshot!("test_multipart_form_data_with_multiple_files", schema);
5966 }
5967
5968 #[test]
5969 fn test_multipart_form_data_mixed_fields() {
5970 let request_body = ObjectOrReference::Object(RequestBody {
5972 description: Some("Profile creation with file upload".to_string()),
5973 content: {
5974 let mut content = BTreeMap::new();
5975 content.insert(
5976 "multipart/form-data".to_string(),
5977 MediaType {
5978 extensions: Default::default(),
5979 schema: Some(ObjectOrReference::Object(ObjectSchema {
5980 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5981 properties: {
5982 let mut props = BTreeMap::new();
5983 props.insert(
5985 "avatar".to_string(),
5986 ObjectOrReference::Object(ObjectSchema {
5987 schema_type: Some(SchemaTypeSet::Single(
5988 SchemaType::String,
5989 )),
5990 format: Some("binary".to_string()),
5991 description: Some("Profile avatar image".to_string()),
5992 ..Default::default()
5993 }),
5994 );
5995 props.insert(
5997 "name".to_string(),
5998 ObjectOrReference::Object(ObjectSchema {
5999 schema_type: Some(SchemaTypeSet::Single(
6000 SchemaType::String,
6001 )),
6002 description: Some("User's display name".to_string()),
6003 ..Default::default()
6004 }),
6005 );
6006 props.insert(
6008 "age".to_string(),
6009 ObjectOrReference::Object(ObjectSchema {
6010 schema_type: Some(SchemaTypeSet::Single(
6011 SchemaType::Integer,
6012 )),
6013 description: Some("User's age".to_string()),
6014 ..Default::default()
6015 }),
6016 );
6017 props.insert(
6019 "email".to_string(),
6020 ObjectOrReference::Object(ObjectSchema {
6021 schema_type: Some(SchemaTypeSet::Single(
6022 SchemaType::String,
6023 )),
6024 format: Some("email".to_string()),
6025 description: Some("User's email address".to_string()),
6026 ..Default::default()
6027 }),
6028 );
6029 props
6030 },
6031 required: vec!["name".to_string(), "avatar".to_string()],
6032 ..Default::default()
6033 })),
6034 examples: None,
6035 encoding: Default::default(),
6036 },
6037 );
6038 content
6039 },
6040 required: Some(true),
6041 });
6042
6043 let spec = create_test_spec();
6044 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
6045 .unwrap()
6046 .unwrap();
6047
6048 let (schema, annotations, _is_required) = result;
6049 let body_properties = schema.get("properties").unwrap();
6050
6051 let avatar_schema = body_properties.get("avatar").unwrap();
6053 assert_eq!(avatar_schema.get("type").unwrap(), "object");
6054 assert!(
6055 avatar_schema
6056 .get("properties")
6057 .unwrap()
6058 .get("content")
6059 .is_some()
6060 );
6061 assert!(
6062 avatar_schema
6063 .get("properties")
6064 .unwrap()
6065 .get("filename")
6066 .is_some()
6067 );
6068
6069 let name_schema = body_properties.get("name").unwrap();
6071 assert_eq!(name_schema.get("type").unwrap(), "string");
6072 assert!(name_schema.get("properties").is_none()); let age_schema = body_properties.get("age").unwrap();
6076 assert_eq!(age_schema.get("type").unwrap(), "integer");
6077
6078 let email_schema = body_properties.get("email").unwrap();
6080 assert_eq!(email_schema.get("type").unwrap(), "string");
6081 assert_eq!(email_schema.get("format").unwrap(), "email");
6082
6083 let annotations_value = serde_json::to_value(&annotations).unwrap();
6085 let annotations_obj = annotations_value.as_object().unwrap();
6086
6087 let x_file_fields = annotations_obj
6088 .get("x-file-fields")
6089 .unwrap()
6090 .as_array()
6091 .unwrap();
6092 assert_eq!(x_file_fields.len(), 1);
6093 assert!(x_file_fields.contains(&json!("avatar")));
6094
6095 insta::assert_json_snapshot!("test_multipart_form_data_mixed_fields", schema);
6097 }
6098
6099 #[test]
6100 fn test_multipart_format_byte_detection() {
6101 let request_body = ObjectOrReference::Object(RequestBody {
6103 description: Some("Base64 encoded file upload".to_string()),
6104 content: {
6105 let mut content = BTreeMap::new();
6106 content.insert(
6107 "multipart/form-data".to_string(),
6108 MediaType {
6109 extensions: Default::default(),
6110 schema: Some(ObjectOrReference::Object(ObjectSchema {
6111 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
6112 properties: {
6113 let mut props = BTreeMap::new();
6114 props.insert(
6116 "data".to_string(),
6117 ObjectOrReference::Object(ObjectSchema {
6118 schema_type: Some(SchemaTypeSet::Single(
6119 SchemaType::String,
6120 )),
6121 format: Some("byte".to_string()),
6122 description: Some(
6123 "Base64 encoded file content".to_string(),
6124 ),
6125 ..Default::default()
6126 }),
6127 );
6128 props.insert(
6130 "attachment".to_string(),
6131 ObjectOrReference::Object(ObjectSchema {
6132 schema_type: Some(SchemaTypeSet::Single(
6133 SchemaType::String,
6134 )),
6135 format: Some("binary".to_string()),
6136 description: Some("Binary file attachment".to_string()),
6137 ..Default::default()
6138 }),
6139 );
6140 props
6141 },
6142 required: vec!["data".to_string()],
6143 ..Default::default()
6144 })),
6145 examples: None,
6146 encoding: Default::default(),
6147 },
6148 );
6149 content
6150 },
6151 required: Some(true),
6152 });
6153
6154 let spec = create_test_spec();
6155 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
6156 .unwrap()
6157 .unwrap();
6158
6159 let (schema, annotations, _is_required) = result;
6160 let body_properties = schema.get("properties").unwrap();
6161
6162 let data_schema = body_properties.get("data").unwrap();
6164 assert_eq!(data_schema.get("type").unwrap(), "object");
6165 assert!(
6166 data_schema
6167 .get("properties")
6168 .unwrap()
6169 .get("content")
6170 .is_some()
6171 );
6172
6173 let attachment_schema = body_properties.get("attachment").unwrap();
6174 assert_eq!(attachment_schema.get("type").unwrap(), "object");
6175 assert!(
6176 attachment_schema
6177 .get("properties")
6178 .unwrap()
6179 .get("content")
6180 .is_some()
6181 );
6182
6183 let annotations_value = serde_json::to_value(&annotations).unwrap();
6185 let annotations_obj = annotations_value.as_object().unwrap();
6186
6187 let x_file_fields = annotations_obj
6188 .get("x-file-fields")
6189 .unwrap()
6190 .as_array()
6191 .unwrap();
6192 assert_eq!(x_file_fields.len(), 2);
6193 assert!(x_file_fields.contains(&json!("data")));
6194 assert!(x_file_fields.contains(&json!("attachment")));
6195
6196 insta::assert_json_snapshot!("test_multipart_format_byte_detection", schema);
6198 }
6199
6200 #[test]
6201 fn test_multipart_non_file_fields_unchanged() {
6202 let request_body = ObjectOrReference::Object(RequestBody {
6204 description: Some("Form submission".to_string()),
6205 content: {
6206 let mut content = BTreeMap::new();
6207 content.insert(
6208 "multipart/form-data".to_string(),
6209 MediaType {
6210 extensions: Default::default(),
6211 schema: Some(ObjectOrReference::Object(ObjectSchema {
6212 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
6213 properties: {
6214 let mut props = BTreeMap::new();
6215 props.insert(
6217 "title".to_string(),
6218 ObjectOrReference::Object(ObjectSchema {
6219 schema_type: Some(SchemaTypeSet::Single(
6220 SchemaType::String,
6221 )),
6222 description: Some("Form title".to_string()),
6223 ..Default::default()
6224 }),
6225 );
6226 props.insert(
6227 "count".to_string(),
6228 ObjectOrReference::Object(ObjectSchema {
6229 schema_type: Some(SchemaTypeSet::Single(
6230 SchemaType::Integer,
6231 )),
6232 description: Some("Item count".to_string()),
6233 ..Default::default()
6234 }),
6235 );
6236 props.insert(
6237 "enabled".to_string(),
6238 ObjectOrReference::Object(ObjectSchema {
6239 schema_type: Some(SchemaTypeSet::Single(
6240 SchemaType::Boolean,
6241 )),
6242 description: Some("Enable flag".to_string()),
6243 ..Default::default()
6244 }),
6245 );
6246 props.insert(
6247 "price".to_string(),
6248 ObjectOrReference::Object(ObjectSchema {
6249 schema_type: Some(SchemaTypeSet::Single(
6250 SchemaType::Number,
6251 )),
6252 description: Some("Price value".to_string()),
6253 ..Default::default()
6254 }),
6255 );
6256 props.insert(
6257 "uuid".to_string(),
6258 ObjectOrReference::Object(ObjectSchema {
6259 schema_type: Some(SchemaTypeSet::Single(
6260 SchemaType::String,
6261 )),
6262 format: Some("uuid".to_string()),
6263 description: Some("UUID field".to_string()),
6264 ..Default::default()
6265 }),
6266 );
6267 props.insert(
6268 "date".to_string(),
6269 ObjectOrReference::Object(ObjectSchema {
6270 schema_type: Some(SchemaTypeSet::Single(
6271 SchemaType::String,
6272 )),
6273 format: Some("date".to_string()),
6274 description: Some("Date field".to_string()),
6275 ..Default::default()
6276 }),
6277 );
6278 props
6279 },
6280 required: vec!["title".to_string()],
6281 ..Default::default()
6282 })),
6283 examples: None,
6284 encoding: Default::default(),
6285 },
6286 );
6287 content
6288 },
6289 required: Some(true),
6290 });
6291
6292 let spec = create_test_spec();
6293 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body, &spec)
6294 .unwrap()
6295 .unwrap();
6296
6297 let (schema, annotations, _is_required) = result;
6298 let body_properties = schema.get("properties").unwrap();
6299
6300 let title_schema = body_properties.get("title").unwrap();
6302 assert_eq!(title_schema.get("type").unwrap(), "string");
6303 assert!(title_schema.get("properties").is_none());
6304
6305 let count_schema = body_properties.get("count").unwrap();
6307 assert_eq!(count_schema.get("type").unwrap(), "integer");
6308
6309 let enabled_schema = body_properties.get("enabled").unwrap();
6311 assert_eq!(enabled_schema.get("type").unwrap(), "boolean");
6312
6313 let price_schema = body_properties.get("price").unwrap();
6315 assert_eq!(price_schema.get("type").unwrap(), "number");
6316
6317 let uuid_schema = body_properties.get("uuid").unwrap();
6319 assert_eq!(uuid_schema.get("type").unwrap(), "string");
6320 assert_eq!(uuid_schema.get("format").unwrap(), "uuid");
6321
6322 let date_schema = body_properties.get("date").unwrap();
6324 assert_eq!(date_schema.get("type").unwrap(), "string");
6325 assert_eq!(date_schema.get("format").unwrap(), "date");
6326
6327 let annotations_value = serde_json::to_value(&annotations).unwrap();
6329 let annotations_obj = annotations_value.as_object().unwrap();
6330
6331 assert!(
6332 annotations_obj.get("x-file-fields").is_none(),
6333 "x-file-fields should not be present when there are no file fields"
6334 );
6335
6336 assert_eq!(
6338 annotations_obj.get("x-content-type").unwrap(),
6339 "multipart/form-data"
6340 );
6341
6342 insta::assert_json_snapshot!("test_multipart_non_file_fields_unchanged", schema);
6344 }
6345}