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";
131
132#[derive(Debug, Clone, Copy, PartialEq)]
134pub enum Location {
135 Parameter(ParameterIn),
137 Body,
139}
140
141impl Serialize for Location {
142 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
143 where
144 S: Serializer,
145 {
146 let str_value = match self {
147 Location::Parameter(param_in) => match param_in {
148 ParameterIn::Query => "query",
149 ParameterIn::Header => "header",
150 ParameterIn::Path => "path",
151 ParameterIn::Cookie => "cookie",
152 },
153 Location::Body => "body",
154 };
155 serializer.serialize_str(str_value)
156 }
157}
158
159#[derive(Debug, Clone, PartialEq)]
161pub enum Annotation {
162 Location(Location),
164 Required(bool),
166 ContentType(String),
168 OriginalName(String),
170 Explode(bool),
172}
173
174#[derive(Debug, Clone, Default)]
176pub struct Annotations {
177 annotations: Vec<Annotation>,
178}
179
180impl Annotations {
181 pub fn new() -> Self {
183 Self {
184 annotations: Vec::new(),
185 }
186 }
187
188 pub fn with_location(mut self, location: Location) -> Self {
190 self.annotations.push(Annotation::Location(location));
191 self
192 }
193
194 pub fn with_required(mut self, required: bool) -> Self {
196 self.annotations.push(Annotation::Required(required));
197 self
198 }
199
200 pub fn with_content_type(mut self, content_type: String) -> Self {
202 self.annotations.push(Annotation::ContentType(content_type));
203 self
204 }
205
206 pub fn with_original_name(mut self, original_name: String) -> Self {
208 self.annotations
209 .push(Annotation::OriginalName(original_name));
210 self
211 }
212
213 pub fn with_explode(mut self, explode: bool) -> Self {
215 self.annotations.push(Annotation::Explode(explode));
216 self
217 }
218}
219
220impl Serialize for Annotations {
221 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
222 where
223 S: Serializer,
224 {
225 use serde::ser::SerializeMap;
226
227 let mut map = serializer.serialize_map(Some(self.annotations.len()))?;
228
229 for annotation in &self.annotations {
230 match annotation {
231 Annotation::Location(location) => {
232 let key = match location {
234 Location::Parameter(param_in) => match param_in {
235 ParameterIn::Header | ParameterIn::Cookie => X_LOCATION,
236 _ => X_PARAMETER_LOCATION,
237 },
238 Location::Body => X_LOCATION,
239 };
240 map.serialize_entry(key, &location)?;
241
242 if let Location::Parameter(_) = location {
244 map.serialize_entry(X_PARAMETER_LOCATION, &location)?;
245 }
246 }
247 Annotation::Required(required) => {
248 map.serialize_entry(X_PARAMETER_REQUIRED, required)?;
249 }
250 Annotation::ContentType(content_type) => {
251 map.serialize_entry(X_CONTENT_TYPE, content_type)?;
252 }
253 Annotation::OriginalName(original_name) => {
254 map.serialize_entry(X_ORIGINAL_NAME, original_name)?;
255 }
256 Annotation::Explode(explode) => {
257 map.serialize_entry(X_PARAMETER_EXPLODE, explode)?;
258 }
259 }
260 }
261
262 map.end()
263 }
264}
265
266fn sanitize_property_name(name: &str) -> String {
275 let sanitized = name
277 .chars()
278 .map(|c| match c {
279 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '.' | '-' => c,
280 _ => '_',
281 })
282 .take(64)
283 .collect::<String>();
284
285 let mut collapsed = String::with_capacity(sanitized.len());
287 let mut prev_was_underscore = false;
288
289 for ch in sanitized.chars() {
290 if ch == '_' {
291 if !prev_was_underscore {
292 collapsed.push(ch);
293 }
294 prev_was_underscore = true;
295 } else {
296 collapsed.push(ch);
297 prev_was_underscore = false;
298 }
299 }
300
301 let trimmed = collapsed.trim_end_matches('_');
303
304 if trimmed.is_empty() || trimmed.chars().next().unwrap_or('0').is_numeric() {
306 format!("param_{trimmed}")
307 } else {
308 trimmed.to_string()
309 }
310}
311
312#[derive(Debug, Clone, Default)]
376pub struct ReferenceMetadata {
377 pub summary: Option<String>,
384
385 pub description: Option<String>,
392}
393
394impl ReferenceMetadata {
395 pub fn new(summary: Option<String>, description: Option<String>) -> Self {
397 Self {
398 summary,
399 description,
400 }
401 }
402
403 pub fn is_empty(&self) -> bool {
405 self.summary.is_none() && self.description.is_none()
406 }
407
408 pub fn best_description(&self) -> Option<&str> {
458 self.description.as_deref().or(self.summary.as_deref())
459 }
460
461 pub fn summary(&self) -> Option<&str> {
508 self.summary.as_deref()
509 }
510
511 pub fn merge_with_description(
597 &self,
598 existing_desc: Option<&str>,
599 prepend_summary: bool,
600 ) -> Option<String> {
601 match (self.best_description(), self.summary(), existing_desc) {
602 (Some(ref_desc), _, _) => Some(ref_desc.to_string()),
604
605 (None, Some(ref_summary), Some(existing)) if prepend_summary => {
607 if ref_summary != existing {
608 Some(format!("{}\n\n{}", ref_summary, existing))
609 } else {
610 Some(existing.to_string())
611 }
612 }
613 (None, Some(ref_summary), _) => Some(ref_summary.to_string()),
614
615 (None, None, Some(existing)) => Some(existing.to_string()),
617
618 (None, None, None) => None,
620 }
621 }
622
623 pub fn enhance_parameter_description(
709 &self,
710 param_name: &str,
711 existing_desc: Option<&str>,
712 ) -> Option<String> {
713 match (self.best_description(), self.summary(), existing_desc) {
714 (Some(ref_desc), _, _) => Some(format!("{}: {}", param_name, ref_desc)),
716
717 (None, Some(ref_summary), _) => Some(format!("{}: {}", param_name, ref_summary)),
719
720 (None, None, Some(existing)) => Some(existing.to_string()),
722
723 (None, None, None) => Some(format!("{} parameter", param_name)),
725 }
726 }
727}
728
729pub struct ToolGenerator;
731
732impl ToolGenerator {
733 pub fn generate_tool_metadata(
739 operation: &Operation,
740 method: String,
741 path: String,
742 spec: &Spec,
743 skip_tool_description: bool,
744 skip_parameter_descriptions: bool,
745 ) -> Result<ToolMetadata, Error> {
746 let name = operation.operation_id.clone().unwrap_or_else(|| {
747 format!(
748 "{}_{}",
749 method,
750 path.replace('/', "_").replace(['{', '}'], "")
751 )
752 });
753
754 let (parameters, parameter_mappings) = Self::generate_parameter_schema(
756 &operation.parameters,
757 &method,
758 &operation.request_body,
759 spec,
760 skip_parameter_descriptions,
761 )?;
762
763 let description =
765 (!skip_tool_description).then(|| Self::build_description(operation, &method, &path));
766
767 let output_schema = Self::extract_output_schema(&operation.responses, spec)?;
769
770 Ok(ToolMetadata {
771 name,
772 title: operation.summary.clone(),
773 description,
774 parameters,
775 output_schema,
776 method,
777 path,
778 security: None, parameter_mappings,
780 })
781 }
782
783 pub fn generate_openapi_tools(
789 tools_metadata: Vec<ToolMetadata>,
790 base_url: Option<url::Url>,
791 default_headers: Option<reqwest::header::HeaderMap>,
792 ) -> Result<Vec<crate::tool::Tool>, Error> {
793 let mut openapi_tools = Vec::with_capacity(tools_metadata.len());
794
795 let mut http_client = HttpClient::new();
796
797 if let Some(url) = base_url {
798 http_client = http_client.with_base_url(url)?;
799 }
800
801 if let Some(headers) = default_headers {
802 http_client = http_client.with_default_headers(headers);
803 }
804
805 for metadata in tools_metadata {
806 let tool = crate::tool::Tool::new(metadata, http_client.clone())?;
807 openapi_tools.push(tool);
808 }
809
810 Ok(openapi_tools)
811 }
812
813 fn build_description(operation: &Operation, method: &str, path: &str) -> String {
815 match (&operation.summary, &operation.description) {
816 (Some(summary), Some(desc)) => {
817 format!(
818 "{}\n\n{}\n\nEndpoint: {} {}",
819 summary,
820 desc,
821 method.to_uppercase(),
822 path
823 )
824 }
825 (Some(summary), None) => {
826 format!(
827 "{}\n\nEndpoint: {} {}",
828 summary,
829 method.to_uppercase(),
830 path
831 )
832 }
833 (None, Some(desc)) => {
834 format!("{}\n\nEndpoint: {} {}", desc, method.to_uppercase(), path)
835 }
836 (None, None) => {
837 format!("API endpoint: {} {}", method.to_uppercase(), path)
838 }
839 }
840 }
841
842 fn extract_output_schema(
846 responses: &Option<BTreeMap<String, ObjectOrReference<Response>>>,
847 spec: &Spec,
848 ) -> Result<Option<Value>, Error> {
849 let responses = match responses {
850 Some(r) => r,
851 None => return Ok(None),
852 };
853 let priority_codes = vec![
855 "200", "201", "202", "203", "204", "2XX", "default", ];
863
864 for status_code in priority_codes {
865 if let Some(response_or_ref) = responses.get(status_code) {
866 let response = match response_or_ref {
868 ObjectOrReference::Object(response) => response,
869 ObjectOrReference::Ref {
870 ref_path,
871 summary,
872 description,
873 } => {
874 let ref_metadata =
877 ReferenceMetadata::new(summary.clone(), description.clone());
878
879 if let Some(ref_desc) = ref_metadata.best_description() {
880 let response_schema = json!({
882 "type": "object",
883 "description": "Unified response structure with success and error variants",
884 "properties": {
885 "status_code": {
886 "type": "integer",
887 "description": "HTTP status code"
888 },
889 "body": {
890 "type": "object",
891 "description": ref_desc,
892 "additionalProperties": true
893 }
894 },
895 "required": ["status_code", "body"]
896 });
897
898 trace!(
899 reference_path = %ref_path,
900 reference_description = %ref_desc,
901 "Created response schema using reference metadata"
902 );
903
904 return Ok(Some(response_schema));
905 }
906
907 continue;
909 }
910 };
911
912 if status_code == "204" {
914 continue;
915 }
916
917 if !response.content.is_empty() {
919 let content = &response.content;
920 let json_media_types = vec![
922 "application/json",
923 "application/ld+json",
924 "application/vnd.api+json",
925 ];
926
927 for media_type_str in json_media_types {
928 if let Some(media_type) = content.get(media_type_str)
929 && let Some(schema_or_ref) = &media_type.schema
930 {
931 let wrapped_schema = Self::wrap_output_schema(schema_or_ref, spec)?;
933 return Ok(Some(wrapped_schema));
934 }
935 }
936
937 for media_type in content.values() {
939 if let Some(schema_or_ref) = &media_type.schema {
940 let wrapped_schema = Self::wrap_output_schema(schema_or_ref, spec)?;
942 return Ok(Some(wrapped_schema));
943 }
944 }
945 }
946 }
947 }
948
949 Ok(None)
951 }
952
953 fn convert_schema_to_json_schema(
963 schema: &Schema,
964 spec: &Spec,
965 visited: &mut HashSet<String>,
966 ) -> Result<Value, Error> {
967 match schema {
968 Schema::Object(obj_schema_or_ref) => match obj_schema_or_ref.as_ref() {
969 ObjectOrReference::Object(obj_schema) => {
970 Self::convert_object_schema_to_json_schema(obj_schema, spec, visited)
971 }
972 ObjectOrReference::Ref { ref_path, .. } => {
973 let resolved = Self::resolve_reference(ref_path, spec, visited)?;
974 let result =
975 Self::convert_object_schema_to_json_schema(&resolved, spec, visited);
976 visited.remove(ref_path);
980 result
981 }
982 },
983 Schema::Boolean(bool_schema) => {
984 if bool_schema.0 {
986 Ok(json!({})) } else {
988 Ok(json!({"not": {}})) }
990 }
991 }
992 }
993
994 fn convert_object_schema_to_json_schema(
1004 obj_schema: &ObjectSchema,
1005 spec: &Spec,
1006 visited: &mut HashSet<String>,
1007 ) -> Result<Value, Error> {
1008 let mut schema_obj = serde_json::Map::new();
1009
1010 if let Some(schema_type) = &obj_schema.schema_type {
1012 match schema_type {
1013 SchemaTypeSet::Single(single_type) => {
1014 schema_obj.insert(
1015 "type".to_string(),
1016 json!(Self::schema_type_to_string(single_type)),
1017 );
1018 }
1019 SchemaTypeSet::Multiple(type_set) => {
1020 let types: Vec<String> =
1021 type_set.iter().map(Self::schema_type_to_string).collect();
1022 schema_obj.insert("type".to_string(), json!(types));
1023 }
1024 }
1025 }
1026
1027 if let Some(desc) = &obj_schema.description {
1029 schema_obj.insert("description".to_string(), json!(desc));
1030 }
1031
1032 if !obj_schema.one_of.is_empty() {
1034 let mut one_of_schemas = Vec::new();
1035 for schema_ref in &obj_schema.one_of {
1036 let schema_json = match schema_ref {
1037 ObjectOrReference::Object(schema) => {
1038 Self::convert_object_schema_to_json_schema(schema, spec, visited)?
1039 }
1040 ObjectOrReference::Ref { ref_path, .. } => {
1041 let resolved = Self::resolve_reference(ref_path, spec, visited)?;
1042 let result =
1043 Self::convert_object_schema_to_json_schema(&resolved, spec, visited)?;
1044 visited.remove(ref_path);
1046 result
1047 }
1048 };
1049 one_of_schemas.push(schema_json);
1050 }
1051 schema_obj.insert("oneOf".to_string(), json!(one_of_schemas));
1052 return Ok(Value::Object(schema_obj));
1055 }
1056
1057 if !obj_schema.properties.is_empty() {
1059 let properties = &obj_schema.properties;
1060 let mut props_map = serde_json::Map::new();
1061 for (prop_name, prop_schema_or_ref) in properties {
1062 let prop_schema = match prop_schema_or_ref {
1063 ObjectOrReference::Object(schema) => {
1064 Self::convert_schema_to_json_schema(
1066 &Schema::Object(Box::new(ObjectOrReference::Object(schema.clone()))),
1067 spec,
1068 visited,
1069 )?
1070 }
1071 ObjectOrReference::Ref { ref_path, .. } => {
1072 let resolved = Self::resolve_reference(ref_path, spec, visited)?;
1073 let result =
1074 Self::convert_object_schema_to_json_schema(&resolved, spec, visited)?;
1075 visited.remove(ref_path);
1077 result
1078 }
1079 };
1080
1081 let sanitized_name = sanitize_property_name(prop_name);
1083 props_map.insert(sanitized_name, prop_schema);
1084 }
1085 schema_obj.insert("properties".to_string(), Value::Object(props_map));
1086 }
1087
1088 if !obj_schema.required.is_empty() {
1090 schema_obj.insert("required".to_string(), json!(&obj_schema.required));
1091 }
1092
1093 if let Some(schema_type) = &obj_schema.schema_type
1095 && matches!(schema_type, SchemaTypeSet::Single(SchemaType::Object))
1096 {
1097 match &obj_schema.additional_properties {
1099 None => {
1100 schema_obj.insert("additionalProperties".to_string(), json!(true));
1102 }
1103 Some(Schema::Boolean(BooleanSchema(value))) => {
1104 schema_obj.insert("additionalProperties".to_string(), json!(value));
1106 }
1107 Some(Schema::Object(schema_ref)) => {
1108 let additional_props_schema = Self::convert_schema_to_json_schema(
1110 &Schema::Object(schema_ref.clone()),
1111 spec,
1112 visited,
1113 )?;
1114 schema_obj.insert("additionalProperties".to_string(), additional_props_schema);
1115 }
1116 }
1117 }
1118
1119 if let Some(schema_type) = &obj_schema.schema_type {
1121 if matches!(schema_type, SchemaTypeSet::Single(SchemaType::Array)) {
1122 if !obj_schema.prefix_items.is_empty() {
1124 Self::convert_prefix_items_to_draft07(
1126 &obj_schema.prefix_items,
1127 &obj_schema.items,
1128 &mut schema_obj,
1129 spec,
1130 )?;
1131 } else if let Some(items_schema) = &obj_schema.items {
1132 let items_json =
1134 Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
1135 schema_obj.insert("items".to_string(), items_json);
1136 }
1137
1138 if let Some(min_items) = obj_schema.min_items {
1140 schema_obj.insert("minItems".to_string(), json!(min_items));
1141 }
1142 if let Some(max_items) = obj_schema.max_items {
1143 schema_obj.insert("maxItems".to_string(), json!(max_items));
1144 }
1145 } else if let Some(items_schema) = &obj_schema.items {
1146 let items_json = Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
1148 schema_obj.insert("items".to_string(), items_json);
1149 }
1150 }
1151
1152 if let Some(format) = &obj_schema.format {
1154 schema_obj.insert("format".to_string(), json!(format));
1155 }
1156
1157 if let Some(example) = &obj_schema.example {
1158 schema_obj.insert("example".to_string(), example.clone());
1159 }
1160
1161 if let Some(default) = &obj_schema.default {
1162 schema_obj.insert("default".to_string(), default.clone());
1163 }
1164
1165 if !obj_schema.enum_values.is_empty() {
1166 schema_obj.insert("enum".to_string(), json!(&obj_schema.enum_values));
1167 }
1168
1169 if let Some(min) = &obj_schema.minimum {
1170 schema_obj.insert("minimum".to_string(), json!(min));
1171 }
1172
1173 if let Some(max) = &obj_schema.maximum {
1174 schema_obj.insert("maximum".to_string(), json!(max));
1175 }
1176
1177 if let Some(min_length) = &obj_schema.min_length {
1178 schema_obj.insert("minLength".to_string(), json!(min_length));
1179 }
1180
1181 if let Some(max_length) = &obj_schema.max_length {
1182 schema_obj.insert("maxLength".to_string(), json!(max_length));
1183 }
1184
1185 if let Some(pattern) = &obj_schema.pattern {
1186 schema_obj.insert("pattern".to_string(), json!(pattern));
1187 }
1188
1189 Ok(Value::Object(schema_obj))
1190 }
1191
1192 fn schema_type_to_string(schema_type: &SchemaType) -> String {
1194 match schema_type {
1195 SchemaType::Boolean => "boolean",
1196 SchemaType::Integer => "integer",
1197 SchemaType::Number => "number",
1198 SchemaType::String => "string",
1199 SchemaType::Array => "array",
1200 SchemaType::Object => "object",
1201 SchemaType::Null => "null",
1202 }
1203 .to_string()
1204 }
1205
1206 fn resolve_reference(
1216 ref_path: &str,
1217 spec: &Spec,
1218 visited: &mut HashSet<String>,
1219 ) -> Result<ObjectSchema, Error> {
1220 if visited.contains(ref_path) {
1222 return Err(Error::ToolGeneration(format!(
1223 "Circular reference detected: {ref_path}"
1224 )));
1225 }
1226
1227 visited.insert(ref_path.to_string());
1229
1230 if !ref_path.starts_with("#/components/schemas/") {
1233 return Err(Error::ToolGeneration(format!(
1234 "Unsupported reference format: {ref_path}. Only #/components/schemas/ references are supported"
1235 )));
1236 }
1237
1238 let schema_name = ref_path.strip_prefix("#/components/schemas/").unwrap();
1239
1240 let components = spec.components.as_ref().ok_or_else(|| {
1242 Error::ToolGeneration(format!(
1243 "Reference {ref_path} points to components, but spec has no components section"
1244 ))
1245 })?;
1246
1247 let schema_ref = components.schemas.get(schema_name).ok_or_else(|| {
1248 Error::ToolGeneration(format!(
1249 "Schema '{schema_name}' not found in components/schemas"
1250 ))
1251 })?;
1252
1253 let resolved_schema = match schema_ref {
1255 ObjectOrReference::Object(obj_schema) => obj_schema.clone(),
1256 ObjectOrReference::Ref {
1257 ref_path: nested_ref,
1258 ..
1259 } => {
1260 Self::resolve_reference(nested_ref, spec, visited)?
1262 }
1263 };
1264
1265 Ok(resolved_schema)
1271 }
1272
1273 fn resolve_reference_with_metadata(
1278 ref_path: &str,
1279 summary: Option<String>,
1280 description: Option<String>,
1281 spec: &Spec,
1282 visited: &mut HashSet<String>,
1283 ) -> Result<(ObjectSchema, ReferenceMetadata), Error> {
1284 let resolved_schema = Self::resolve_reference(ref_path, spec, visited)?;
1285 let metadata = ReferenceMetadata::new(summary, description);
1286 Ok((resolved_schema, metadata))
1287 }
1288
1289 fn generate_parameter_schema(
1291 parameters: &[ObjectOrReference<Parameter>],
1292 _method: &str,
1293 request_body: &Option<ObjectOrReference<RequestBody>>,
1294 spec: &Spec,
1295 skip_parameter_descriptions: bool,
1296 ) -> Result<
1297 (
1298 Value,
1299 std::collections::HashMap<String, crate::tool::ParameterMapping>,
1300 ),
1301 Error,
1302 > {
1303 let mut properties = serde_json::Map::new();
1304 let mut required = Vec::new();
1305 let mut parameter_mappings = std::collections::HashMap::new();
1306
1307 let mut path_params = Vec::new();
1309 let mut query_params = Vec::new();
1310 let mut header_params = Vec::new();
1311 let mut cookie_params = Vec::new();
1312
1313 for param_ref in parameters {
1314 let param = match param_ref {
1315 ObjectOrReference::Object(param) => param,
1316 ObjectOrReference::Ref { ref_path, .. } => {
1317 warn!(
1321 reference_path = %ref_path,
1322 "Parameter reference not resolved"
1323 );
1324 continue;
1325 }
1326 };
1327
1328 match ¶m.location {
1329 ParameterIn::Query => query_params.push(param),
1330 ParameterIn::Header => header_params.push(param),
1331 ParameterIn::Path => path_params.push(param),
1332 ParameterIn::Cookie => cookie_params.push(param),
1333 }
1334 }
1335
1336 for param in path_params {
1338 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1339 param,
1340 ParameterIn::Path,
1341 spec,
1342 skip_parameter_descriptions,
1343 )?;
1344
1345 let sanitized_name = sanitize_property_name(¶m.name);
1347 if sanitized_name != param.name {
1348 annotations = annotations.with_original_name(param.name.clone());
1349 }
1350
1351 let explode = annotations
1353 .annotations
1354 .iter()
1355 .find_map(|a| {
1356 if let Annotation::Explode(e) = a {
1357 Some(*e)
1358 } else {
1359 None
1360 }
1361 })
1362 .unwrap_or(true);
1363
1364 parameter_mappings.insert(
1366 sanitized_name.clone(),
1367 crate::tool::ParameterMapping {
1368 sanitized_name: sanitized_name.clone(),
1369 original_name: param.name.clone(),
1370 location: "path".to_string(),
1371 explode,
1372 },
1373 );
1374
1375 properties.insert(sanitized_name.clone(), param_schema);
1377 required.push(sanitized_name);
1378 }
1379
1380 for param in &query_params {
1382 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1383 param,
1384 ParameterIn::Query,
1385 spec,
1386 skip_parameter_descriptions,
1387 )?;
1388
1389 let sanitized_name = sanitize_property_name(¶m.name);
1391 if sanitized_name != param.name {
1392 annotations = annotations.with_original_name(param.name.clone());
1393 }
1394
1395 let explode = annotations
1397 .annotations
1398 .iter()
1399 .find_map(|a| {
1400 if let Annotation::Explode(e) = a {
1401 Some(*e)
1402 } else {
1403 None
1404 }
1405 })
1406 .unwrap_or(true);
1407
1408 parameter_mappings.insert(
1410 sanitized_name.clone(),
1411 crate::tool::ParameterMapping {
1412 sanitized_name: sanitized_name.clone(),
1413 original_name: param.name.clone(),
1414 location: "query".to_string(),
1415 explode,
1416 },
1417 );
1418
1419 properties.insert(sanitized_name.clone(), param_schema);
1421 if param.required.unwrap_or(false) {
1422 required.push(sanitized_name);
1423 }
1424 }
1425
1426 for param in &header_params {
1428 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1429 param,
1430 ParameterIn::Header,
1431 spec,
1432 skip_parameter_descriptions,
1433 )?;
1434
1435 let prefixed_name = format!("header_{}", param.name);
1437 let sanitized_name = sanitize_property_name(&prefixed_name);
1438 if sanitized_name != prefixed_name {
1439 annotations = annotations.with_original_name(param.name.clone());
1440 }
1441
1442 let explode = annotations
1444 .annotations
1445 .iter()
1446 .find_map(|a| {
1447 if let Annotation::Explode(e) = a {
1448 Some(*e)
1449 } else {
1450 None
1451 }
1452 })
1453 .unwrap_or(true);
1454
1455 parameter_mappings.insert(
1457 sanitized_name.clone(),
1458 crate::tool::ParameterMapping {
1459 sanitized_name: sanitized_name.clone(),
1460 original_name: param.name.clone(),
1461 location: "header".to_string(),
1462 explode,
1463 },
1464 );
1465
1466 properties.insert(sanitized_name.clone(), param_schema);
1468 if param.required.unwrap_or(false) {
1469 required.push(sanitized_name);
1470 }
1471 }
1472
1473 for param in &cookie_params {
1475 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1476 param,
1477 ParameterIn::Cookie,
1478 spec,
1479 skip_parameter_descriptions,
1480 )?;
1481
1482 let prefixed_name = format!("cookie_{}", param.name);
1484 let sanitized_name = sanitize_property_name(&prefixed_name);
1485 if sanitized_name != prefixed_name {
1486 annotations = annotations.with_original_name(param.name.clone());
1487 }
1488
1489 let explode = annotations
1491 .annotations
1492 .iter()
1493 .find_map(|a| {
1494 if let Annotation::Explode(e) = a {
1495 Some(*e)
1496 } else {
1497 None
1498 }
1499 })
1500 .unwrap_or(true);
1501
1502 parameter_mappings.insert(
1504 sanitized_name.clone(),
1505 crate::tool::ParameterMapping {
1506 sanitized_name: sanitized_name.clone(),
1507 original_name: param.name.clone(),
1508 location: "cookie".to_string(),
1509 explode,
1510 },
1511 );
1512
1513 properties.insert(sanitized_name.clone(), param_schema);
1515 if param.required.unwrap_or(false) {
1516 required.push(sanitized_name);
1517 }
1518 }
1519
1520 if let Some(request_body) = request_body
1522 && let Some((body_schema, _annotations, is_required)) =
1523 Self::convert_request_body_to_json_schema(request_body, spec)?
1524 {
1525 parameter_mappings.insert(
1527 "request_body".to_string(),
1528 crate::tool::ParameterMapping {
1529 sanitized_name: "request_body".to_string(),
1530 original_name: "request_body".to_string(),
1531 location: "body".to_string(),
1532 explode: false,
1533 },
1534 );
1535
1536 properties.insert("request_body".to_string(), body_schema);
1538 if is_required {
1539 required.push("request_body".to_string());
1540 }
1541 }
1542
1543 if !query_params.is_empty() || !header_params.is_empty() || !cookie_params.is_empty() {
1545 properties.insert(
1547 "timeout_seconds".to_string(),
1548 json!({
1549 "type": "integer",
1550 "description": "Request timeout in seconds",
1551 "minimum": 1,
1552 "maximum": 300,
1553 "default": 30
1554 }),
1555 );
1556 }
1557
1558 let schema = json!({
1559 "type": "object",
1560 "properties": properties,
1561 "required": required,
1562 "additionalProperties": false
1563 });
1564
1565 Ok((schema, parameter_mappings))
1566 }
1567
1568 fn convert_parameter_schema(
1570 param: &Parameter,
1571 location: ParameterIn,
1572 spec: &Spec,
1573 skip_parameter_descriptions: bool,
1574 ) -> Result<(Value, Annotations), Error> {
1575 let base_schema = if let Some(schema_ref) = ¶m.schema {
1577 match schema_ref {
1578 ObjectOrReference::Object(obj_schema) => {
1579 let mut visited = HashSet::new();
1580 Self::convert_schema_to_json_schema(
1581 &Schema::Object(Box::new(ObjectOrReference::Object(obj_schema.clone()))),
1582 spec,
1583 &mut visited,
1584 )?
1585 }
1586 ObjectOrReference::Ref {
1587 ref_path,
1588 summary,
1589 description,
1590 } => {
1591 let mut visited = HashSet::new();
1593 match Self::resolve_reference_with_metadata(
1594 ref_path,
1595 summary.clone(),
1596 description.clone(),
1597 spec,
1598 &mut visited,
1599 ) {
1600 Ok((resolved_schema, ref_metadata)) => {
1601 let mut schema_json = Self::convert_schema_to_json_schema(
1602 &Schema::Object(Box::new(ObjectOrReference::Object(
1603 resolved_schema,
1604 ))),
1605 spec,
1606 &mut visited,
1607 )?;
1608
1609 if let Value::Object(ref mut schema_obj) = schema_json {
1611 if let Some(ref_desc) = ref_metadata.best_description() {
1613 schema_obj.insert("description".to_string(), json!(ref_desc));
1614 }
1615 }
1618
1619 schema_json
1620 }
1621 Err(_) => {
1622 json!({"type": "string"})
1624 }
1625 }
1626 }
1627 }
1628 } else {
1629 json!({"type": "string"})
1631 };
1632
1633 let mut result = match base_schema {
1635 Value::Object(obj) => obj,
1636 _ => {
1637 return Err(Error::ToolGeneration(format!(
1639 "Internal error: schema converter returned non-object for parameter '{}'",
1640 param.name
1641 )));
1642 }
1643 };
1644
1645 let mut collected_examples = Vec::new();
1647
1648 if let Some(example) = ¶m.example {
1650 collected_examples.push(example.clone());
1651 } else if !param.examples.is_empty() {
1652 for example_ref in param.examples.values() {
1654 match example_ref {
1655 ObjectOrReference::Object(example_obj) => {
1656 if let Some(value) = &example_obj.value {
1657 collected_examples.push(value.clone());
1658 }
1659 }
1660 ObjectOrReference::Ref { .. } => {
1661 }
1663 }
1664 }
1665 } else if let Some(Value::String(ex_str)) = result.get("example") {
1666 collected_examples.push(json!(ex_str));
1668 } else if let Some(ex) = result.get("example") {
1669 collected_examples.push(ex.clone());
1670 }
1671
1672 let base_description = param
1674 .description
1675 .as_ref()
1676 .map(|d| d.to_string())
1677 .or_else(|| {
1678 result
1679 .get("description")
1680 .and_then(|d| d.as_str())
1681 .map(|d| d.to_string())
1682 })
1683 .unwrap_or_else(|| format!("{} parameter", param.name));
1684
1685 let description_with_examples = if let Some(examples_str) =
1686 Self::format_examples_for_description(&collected_examples)
1687 {
1688 format!("{base_description}. {examples_str}")
1689 } else {
1690 base_description
1691 };
1692
1693 if !skip_parameter_descriptions {
1694 result.insert("description".to_string(), json!(description_with_examples));
1695 }
1696
1697 if let Some(example) = ¶m.example {
1702 result.insert("example".to_string(), example.clone());
1703 } else if !param.examples.is_empty() {
1704 let mut examples_array = Vec::new();
1707 for (example_name, example_ref) in ¶m.examples {
1708 match example_ref {
1709 ObjectOrReference::Object(example_obj) => {
1710 if let Some(value) = &example_obj.value {
1711 examples_array.push(json!({
1712 "name": example_name,
1713 "value": value
1714 }));
1715 }
1716 }
1717 ObjectOrReference::Ref { .. } => {
1718 }
1721 }
1722 }
1723
1724 if !examples_array.is_empty() {
1725 if let Some(first_example) = examples_array.first()
1727 && let Some(value) = first_example.get("value")
1728 {
1729 result.insert("example".to_string(), value.clone());
1730 }
1731 result.insert("x-examples".to_string(), json!(examples_array));
1733 }
1734 }
1735
1736 let mut annotations = Annotations::new()
1738 .with_location(Location::Parameter(location))
1739 .with_required(param.required.unwrap_or(false));
1740
1741 if let Some(explode) = param.explode {
1743 annotations = annotations.with_explode(explode);
1744 } else {
1745 let default_explode = match ¶m.style {
1749 Some(ParameterStyle::Form) | None => true, _ => false,
1751 };
1752 annotations = annotations.with_explode(default_explode);
1753 }
1754
1755 Ok((Value::Object(result), annotations))
1756 }
1757
1758 fn format_examples_for_description(examples: &[Value]) -> Option<String> {
1760 if examples.is_empty() {
1761 return None;
1762 }
1763
1764 if examples.len() == 1 {
1765 let example_str =
1766 serde_json::to_string(&examples[0]).unwrap_or_else(|_| "null".to_string());
1767 Some(format!("Example: `{example_str}`"))
1768 } else {
1769 let mut result = String::from("Examples:\n");
1770 for ex in examples {
1771 let json_str = serde_json::to_string(ex).unwrap_or_else(|_| "null".to_string());
1772 result.push_str(&format!("- `{json_str}`\n"));
1773 }
1774 result.pop();
1776 Some(result)
1777 }
1778 }
1779
1780 fn convert_prefix_items_to_draft07(
1791 prefix_items: &[ObjectOrReference<ObjectSchema>],
1792 items: &Option<Box<Schema>>,
1793 result: &mut serde_json::Map<String, Value>,
1794 spec: &Spec,
1795 ) -> Result<(), Error> {
1796 let prefix_count = prefix_items.len();
1797
1798 let mut item_types = Vec::new();
1800 for prefix_item in prefix_items {
1801 match prefix_item {
1802 ObjectOrReference::Object(obj_schema) => {
1803 if let Some(schema_type) = &obj_schema.schema_type {
1804 match schema_type {
1805 SchemaTypeSet::Single(SchemaType::String) => item_types.push("string"),
1806 SchemaTypeSet::Single(SchemaType::Integer) => {
1807 item_types.push("integer")
1808 }
1809 SchemaTypeSet::Single(SchemaType::Number) => item_types.push("number"),
1810 SchemaTypeSet::Single(SchemaType::Boolean) => {
1811 item_types.push("boolean")
1812 }
1813 SchemaTypeSet::Single(SchemaType::Array) => item_types.push("array"),
1814 SchemaTypeSet::Single(SchemaType::Object) => item_types.push("object"),
1815 _ => item_types.push("string"), }
1817 } else {
1818 item_types.push("string"); }
1820 }
1821 ObjectOrReference::Ref { ref_path, .. } => {
1822 let mut visited = HashSet::new();
1824 match Self::resolve_reference(ref_path, spec, &mut visited) {
1825 Ok(resolved_schema) => {
1826 if let Some(schema_type_set) = &resolved_schema.schema_type {
1828 match schema_type_set {
1829 SchemaTypeSet::Single(SchemaType::String) => {
1830 item_types.push("string")
1831 }
1832 SchemaTypeSet::Single(SchemaType::Integer) => {
1833 item_types.push("integer")
1834 }
1835 SchemaTypeSet::Single(SchemaType::Number) => {
1836 item_types.push("number")
1837 }
1838 SchemaTypeSet::Single(SchemaType::Boolean) => {
1839 item_types.push("boolean")
1840 }
1841 SchemaTypeSet::Single(SchemaType::Array) => {
1842 item_types.push("array")
1843 }
1844 SchemaTypeSet::Single(SchemaType::Object) => {
1845 item_types.push("object")
1846 }
1847 _ => item_types.push("string"), }
1849 } else {
1850 item_types.push("string"); }
1852 }
1853 Err(_) => {
1854 item_types.push("string");
1856 }
1857 }
1858 }
1859 }
1860 }
1861
1862 let items_is_false =
1864 matches!(items.as_ref().map(|i| i.as_ref()), Some(Schema::Boolean(b)) if !b.0);
1865
1866 if items_is_false {
1867 result.insert("minItems".to_string(), json!(prefix_count));
1869 result.insert("maxItems".to_string(), json!(prefix_count));
1870 }
1871
1872 let unique_types: std::collections::BTreeSet<_> = item_types.into_iter().collect();
1874
1875 if unique_types.len() == 1 {
1876 let item_type = unique_types.into_iter().next().unwrap();
1878 result.insert("items".to_string(), json!({"type": item_type}));
1879 } else if unique_types.len() > 1 {
1880 let one_of: Vec<Value> = unique_types
1882 .into_iter()
1883 .map(|t| json!({"type": t}))
1884 .collect();
1885 result.insert("items".to_string(), json!({"oneOf": one_of}));
1886 }
1887
1888 Ok(())
1889 }
1890
1891 fn convert_request_body_to_json_schema(
1903 request_body_ref: &ObjectOrReference<RequestBody>,
1904 spec: &Spec,
1905 ) -> Result<Option<(Value, Annotations, bool)>, Error> {
1906 match request_body_ref {
1907 ObjectOrReference::Object(request_body) => {
1908 let schema_info = request_body
1911 .content
1912 .get(mime::APPLICATION_JSON.as_ref())
1913 .or_else(|| request_body.content.get("application/json"))
1914 .or_else(|| {
1915 request_body.content.values().next()
1917 });
1918
1919 if let Some(media_type) = schema_info {
1920 if let Some(schema_ref) = &media_type.schema {
1921 let schema = Schema::Object(Box::new(schema_ref.clone()));
1923
1924 let mut visited = HashSet::new();
1926 let converted_schema =
1927 Self::convert_schema_to_json_schema(&schema, spec, &mut visited)?;
1928
1929 let mut schema_obj = match converted_schema {
1931 Value::Object(obj) => obj,
1932 _ => {
1933 let mut obj = serde_json::Map::new();
1935 obj.insert("type".to_string(), json!("object"));
1936 obj.insert("additionalProperties".to_string(), json!(true));
1937 obj
1938 }
1939 };
1940
1941 if !schema_obj.contains_key("description") {
1943 let description = request_body
1944 .description
1945 .clone()
1946 .unwrap_or_else(|| "Request body data".to_string());
1947 schema_obj.insert("description".to_string(), json!(description));
1948 }
1949
1950 let annotations = Annotations::new()
1952 .with_location(Location::Body)
1953 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
1954
1955 let required = request_body.required.unwrap_or(false);
1956 Ok(Some((Value::Object(schema_obj), annotations, required)))
1957 } else {
1958 Ok(None)
1959 }
1960 } else {
1961 Ok(None)
1962 }
1963 }
1964 ObjectOrReference::Ref {
1965 ref_path: _,
1966 summary,
1967 description,
1968 } => {
1969 let ref_metadata = ReferenceMetadata::new(summary.clone(), description.clone());
1971 let enhanced_description = ref_metadata
1972 .best_description()
1973 .map(|desc| desc.to_string())
1974 .unwrap_or_else(|| "Request body data".to_string());
1975
1976 let mut result = serde_json::Map::new();
1977 result.insert("type".to_string(), json!("object"));
1978 result.insert("additionalProperties".to_string(), json!(true));
1979 result.insert("description".to_string(), json!(enhanced_description));
1980
1981 let annotations = Annotations::new()
1983 .with_location(Location::Body)
1984 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
1985
1986 Ok(Some((Value::Object(result), annotations, false)))
1987 }
1988 }
1989 }
1990
1991 pub fn extract_parameters(
1997 tool_metadata: &ToolMetadata,
1998 arguments: &Value,
1999 ) -> Result<ExtractedParameters, ToolCallValidationError> {
2000 let args = arguments.as_object().ok_or_else(|| {
2001 ToolCallValidationError::RequestConstructionError {
2002 reason: "Arguments must be an object".to_string(),
2003 }
2004 })?;
2005
2006 trace!(
2007 tool_name = %tool_metadata.name,
2008 raw_arguments = ?arguments,
2009 "Starting parameter extraction"
2010 );
2011
2012 let mut path_params = HashMap::new();
2013 let mut query_params = HashMap::new();
2014 let mut header_params = HashMap::new();
2015 let mut cookie_params = HashMap::new();
2016 let mut body_params = HashMap::new();
2017 let mut config = RequestConfig::default();
2018
2019 if let Some(timeout) = args.get("timeout_seconds").and_then(Value::as_u64) {
2021 config.timeout_seconds = u32::try_from(timeout).unwrap_or(u32::MAX);
2022 }
2023
2024 for (key, value) in args {
2026 if key == "timeout_seconds" {
2027 continue; }
2029
2030 if key == "request_body" {
2032 body_params.insert("request_body".to_string(), value.clone());
2033 continue;
2034 }
2035
2036 let mapping = tool_metadata.parameter_mappings.get(key);
2038
2039 if let Some(mapping) = mapping {
2040 match mapping.location.as_str() {
2042 "path" => {
2043 path_params.insert(mapping.original_name.clone(), value.clone());
2044 }
2045 "query" => {
2046 query_params.insert(
2047 mapping.original_name.clone(),
2048 QueryParameter::new(value.clone(), mapping.explode),
2049 );
2050 }
2051 "header" => {
2052 header_params.insert(mapping.original_name.clone(), value.clone());
2053 }
2054 "cookie" => {
2055 cookie_params.insert(mapping.original_name.clone(), value.clone());
2056 }
2057 "body" => {
2058 body_params.insert(mapping.original_name.clone(), value.clone());
2059 }
2060 _ => {
2061 return Err(ToolCallValidationError::RequestConstructionError {
2062 reason: format!("Unknown parameter location for parameter: {key}"),
2063 });
2064 }
2065 }
2066 } else {
2067 let location = Self::get_parameter_location(tool_metadata, key).map_err(|e| {
2069 ToolCallValidationError::RequestConstructionError {
2070 reason: e.to_string(),
2071 }
2072 })?;
2073
2074 let original_name = Self::get_original_parameter_name(tool_metadata, key);
2075
2076 match location.as_str() {
2077 "path" => {
2078 path_params
2079 .insert(original_name.unwrap_or_else(|| key.clone()), value.clone());
2080 }
2081 "query" => {
2082 let param_name = original_name.unwrap_or_else(|| key.clone());
2083 let explode = Self::get_parameter_explode(tool_metadata, key);
2084 query_params
2085 .insert(param_name, QueryParameter::new(value.clone(), explode));
2086 }
2087 "header" => {
2088 let header_name = if let Some(orig) = original_name {
2089 orig
2090 } else if key.starts_with("header_") {
2091 key.strip_prefix("header_").unwrap_or(key).to_string()
2092 } else {
2093 key.clone()
2094 };
2095 header_params.insert(header_name, value.clone());
2096 }
2097 "cookie" => {
2098 let cookie_name = if let Some(orig) = original_name {
2099 orig
2100 } else if key.starts_with("cookie_") {
2101 key.strip_prefix("cookie_").unwrap_or(key).to_string()
2102 } else {
2103 key.clone()
2104 };
2105 cookie_params.insert(cookie_name, value.clone());
2106 }
2107 "body" => {
2108 let body_name = if key.starts_with("body_") {
2109 key.strip_prefix("body_").unwrap_or(key).to_string()
2110 } else {
2111 key.clone()
2112 };
2113 body_params.insert(body_name, value.clone());
2114 }
2115 _ => {
2116 return Err(ToolCallValidationError::RequestConstructionError {
2117 reason: format!("Unknown parameter location for parameter: {key}"),
2118 });
2119 }
2120 }
2121 }
2122 }
2123
2124 let extracted = ExtractedParameters {
2125 path: path_params,
2126 query: query_params,
2127 headers: header_params,
2128 cookies: cookie_params,
2129 body: body_params,
2130 config,
2131 };
2132
2133 trace!(
2134 tool_name = %tool_metadata.name,
2135 extracted_parameters = ?extracted,
2136 "Parameter extraction completed"
2137 );
2138
2139 Self::validate_parameters(tool_metadata, arguments)?;
2141
2142 Ok(extracted)
2143 }
2144
2145 fn get_original_parameter_name(
2147 tool_metadata: &ToolMetadata,
2148 param_name: &str,
2149 ) -> Option<String> {
2150 tool_metadata
2151 .parameters
2152 .get("properties")
2153 .and_then(|p| p.as_object())
2154 .and_then(|props| props.get(param_name))
2155 .and_then(|schema| schema.get(X_ORIGINAL_NAME))
2156 .and_then(|v| v.as_str())
2157 .map(|s| s.to_string())
2158 }
2159
2160 fn get_parameter_explode(tool_metadata: &ToolMetadata, param_name: &str) -> bool {
2162 tool_metadata
2163 .parameters
2164 .get("properties")
2165 .and_then(|p| p.as_object())
2166 .and_then(|props| props.get(param_name))
2167 .and_then(|schema| schema.get(X_PARAMETER_EXPLODE))
2168 .and_then(|v| v.as_bool())
2169 .unwrap_or(true) }
2171
2172 fn get_parameter_location(
2174 tool_metadata: &ToolMetadata,
2175 param_name: &str,
2176 ) -> Result<String, Error> {
2177 let properties = tool_metadata
2178 .parameters
2179 .get("properties")
2180 .and_then(|p| p.as_object())
2181 .ok_or_else(|| Error::ToolGeneration("Invalid tool parameters schema".to_string()))?;
2182
2183 if let Some(param_schema) = properties.get(param_name)
2184 && let Some(location) = param_schema
2185 .get(X_PARAMETER_LOCATION)
2186 .and_then(|v| v.as_str())
2187 {
2188 return Ok(location.to_string());
2189 }
2190
2191 if param_name.starts_with("header_") {
2193 Ok("header".to_string())
2194 } else if param_name.starts_with("cookie_") {
2195 Ok("cookie".to_string())
2196 } else if param_name.starts_with("body_") {
2197 Ok("body".to_string())
2198 } else {
2199 Ok("query".to_string())
2201 }
2202 }
2203
2204 fn validate_parameters(
2206 tool_metadata: &ToolMetadata,
2207 arguments: &Value,
2208 ) -> Result<(), ToolCallValidationError> {
2209 let schema = &tool_metadata.parameters;
2210
2211 let required_params = schema
2213 .get("required")
2214 .and_then(|r| r.as_array())
2215 .map(|arr| {
2216 arr.iter()
2217 .filter_map(|v| v.as_str())
2218 .collect::<std::collections::HashSet<_>>()
2219 })
2220 .unwrap_or_default();
2221
2222 let properties = schema
2223 .get("properties")
2224 .and_then(|p| p.as_object())
2225 .ok_or_else(|| ToolCallValidationError::RequestConstructionError {
2226 reason: "Tool schema missing properties".to_string(),
2227 })?;
2228
2229 let args = arguments.as_object().ok_or_else(|| {
2230 ToolCallValidationError::RequestConstructionError {
2231 reason: "Arguments must be an object".to_string(),
2232 }
2233 })?;
2234
2235 let mut all_errors = Vec::new();
2237
2238 all_errors.extend(Self::check_unknown_parameters(args, properties));
2240
2241 all_errors.extend(Self::check_missing_required(
2243 args,
2244 properties,
2245 &required_params,
2246 ));
2247
2248 all_errors.extend(Self::validate_parameter_values(
2250 args,
2251 properties,
2252 &required_params,
2253 ));
2254
2255 if !all_errors.is_empty() {
2257 return Err(ToolCallValidationError::InvalidParameters {
2258 violations: all_errors,
2259 });
2260 }
2261
2262 Ok(())
2263 }
2264
2265 fn check_unknown_parameters(
2267 args: &serde_json::Map<String, Value>,
2268 properties: &serde_json::Map<String, Value>,
2269 ) -> Vec<ValidationError> {
2270 let mut errors = Vec::new();
2271
2272 let valid_params: Vec<String> = properties.keys().map(|s| s.to_string()).collect();
2274
2275 for (arg_name, _) in args.iter() {
2277 if !properties.contains_key(arg_name) {
2278 errors.push(ValidationError::invalid_parameter(
2280 arg_name.clone(),
2281 &valid_params,
2282 ));
2283 }
2284 }
2285
2286 errors
2287 }
2288
2289 fn check_missing_required(
2291 args: &serde_json::Map<String, Value>,
2292 properties: &serde_json::Map<String, Value>,
2293 required_params: &HashSet<&str>,
2294 ) -> Vec<ValidationError> {
2295 let mut errors = Vec::new();
2296
2297 for required_param in required_params {
2298 if !args.contains_key(*required_param) {
2299 let param_schema = properties.get(*required_param);
2301
2302 let description = param_schema
2303 .and_then(|schema| schema.get("description"))
2304 .and_then(|d| d.as_str())
2305 .map(|s| s.to_string());
2306
2307 let expected_type = param_schema
2308 .and_then(Self::get_expected_type)
2309 .unwrap_or_else(|| "unknown".to_string());
2310
2311 errors.push(ValidationError::MissingRequiredParameter {
2312 parameter: (*required_param).to_string(),
2313 description,
2314 expected_type,
2315 });
2316 }
2317 }
2318
2319 errors
2320 }
2321
2322 fn validate_parameter_values(
2324 args: &serde_json::Map<String, Value>,
2325 properties: &serde_json::Map<String, Value>,
2326 required_params: &std::collections::HashSet<&str>,
2327 ) -> Vec<ValidationError> {
2328 let mut errors = Vec::new();
2329
2330 for (param_name, param_value) in args {
2331 if let Some(param_schema) = properties.get(param_name) {
2332 let is_null_value = param_value.is_null();
2334 let is_required = required_params.contains(param_name.as_str());
2335
2336 let schema = json!({
2338 "type": "object",
2339 "properties": {
2340 param_name: param_schema
2341 }
2342 });
2343
2344 let compiled = match jsonschema::validator_for(&schema) {
2346 Ok(compiled) => compiled,
2347 Err(e) => {
2348 errors.push(ValidationError::ConstraintViolation {
2349 parameter: param_name.clone(),
2350 message: format!(
2351 "Failed to compile schema for parameter '{param_name}': {e}"
2352 ),
2353 field_path: None,
2354 actual_value: None,
2355 expected_type: None,
2356 constraints: vec![],
2357 });
2358 continue;
2359 }
2360 };
2361
2362 let instance = json!({ param_name: param_value });
2364
2365 let validation_errors: Vec<_> =
2367 compiled.validate(&instance).err().into_iter().collect();
2368
2369 for validation_error in validation_errors {
2370 let error_message = validation_error.to_string();
2372 let instance_path_str = validation_error.instance_path().to_string();
2373 let field_path = if instance_path_str.is_empty() || instance_path_str == "/" {
2374 Some(param_name.clone())
2375 } else {
2376 Some(instance_path_str.trim_start_matches('/').to_string())
2377 };
2378
2379 let constraints = Self::extract_constraints_from_schema(param_schema);
2381
2382 let expected_type = Self::get_expected_type(param_schema);
2384
2385 let maybe_type_error = match &validation_error.kind() {
2389 ValidationErrorKind::Type { kind } => Some(kind),
2390 _ => None,
2391 };
2392 let is_type_error = maybe_type_error.is_some();
2393 let is_null_error = is_null_value
2394 || (is_type_error && validation_error.instance().as_null().is_some());
2395 let message = if is_null_error && let Some(type_error) = maybe_type_error {
2396 let field_name = field_path.as_ref().unwrap_or(param_name);
2398
2399 let final_expected_type =
2401 expected_type.clone().unwrap_or_else(|| match type_error {
2402 TypeKind::Single(json_type) => json_type.to_string(),
2403 TypeKind::Multiple(json_type_set) => json_type_set
2404 .iter()
2405 .map(|t| t.to_string())
2406 .collect::<Vec<_>>()
2407 .join(", "),
2408 });
2409
2410 let actual_field_name = field_path
2413 .as_ref()
2414 .and_then(|path| path.split('/').next_back())
2415 .unwrap_or(param_name);
2416
2417 let is_nested_field = field_path.as_ref().is_some_and(|p| p.contains('/'));
2420
2421 let field_is_required = if is_nested_field {
2422 constraints.iter().any(|c| {
2423 if let ValidationConstraint::Required { properties } = c {
2424 properties.contains(&actual_field_name.to_string())
2425 } else {
2426 false
2427 }
2428 })
2429 } else {
2430 is_required
2431 };
2432
2433 if field_is_required {
2434 format!(
2435 "Parameter '{field_name}' is required and must not be null (expected: {final_expected_type})"
2436 )
2437 } else {
2438 format!(
2439 "Parameter '{field_name}' is optional but must not be null (expected: {final_expected_type})"
2440 )
2441 }
2442 } else {
2443 error_message
2444 };
2445
2446 errors.push(ValidationError::ConstraintViolation {
2447 parameter: param_name.clone(),
2448 message,
2449 field_path,
2450 actual_value: Some(Box::new(param_value.clone())),
2451 expected_type,
2452 constraints,
2453 });
2454 }
2455 }
2456 }
2457
2458 errors
2459 }
2460
2461 fn extract_constraints_from_schema(schema: &Value) -> Vec<ValidationConstraint> {
2463 let mut constraints = Vec::new();
2464
2465 if let Some(min_value) = schema.get("minimum").and_then(|v| v.as_f64()) {
2467 let exclusive = schema
2468 .get("exclusiveMinimum")
2469 .and_then(|v| v.as_bool())
2470 .unwrap_or(false);
2471 constraints.push(ValidationConstraint::Minimum {
2472 value: min_value,
2473 exclusive,
2474 });
2475 }
2476
2477 if let Some(max_value) = schema.get("maximum").and_then(|v| v.as_f64()) {
2479 let exclusive = schema
2480 .get("exclusiveMaximum")
2481 .and_then(|v| v.as_bool())
2482 .unwrap_or(false);
2483 constraints.push(ValidationConstraint::Maximum {
2484 value: max_value,
2485 exclusive,
2486 });
2487 }
2488
2489 if let Some(min_len) = schema
2491 .get("minLength")
2492 .and_then(|v| v.as_u64())
2493 .map(|v| v as usize)
2494 {
2495 constraints.push(ValidationConstraint::MinLength { value: min_len });
2496 }
2497
2498 if let Some(max_len) = schema
2500 .get("maxLength")
2501 .and_then(|v| v.as_u64())
2502 .map(|v| v as usize)
2503 {
2504 constraints.push(ValidationConstraint::MaxLength { value: max_len });
2505 }
2506
2507 if let Some(pattern) = schema
2509 .get("pattern")
2510 .and_then(|v| v.as_str())
2511 .map(|s| s.to_string())
2512 {
2513 constraints.push(ValidationConstraint::Pattern { pattern });
2514 }
2515
2516 if let Some(enum_values) = schema.get("enum").and_then(|v| v.as_array()).cloned() {
2518 constraints.push(ValidationConstraint::EnumValues {
2519 values: enum_values,
2520 });
2521 }
2522
2523 if let Some(format) = schema
2525 .get("format")
2526 .and_then(|v| v.as_str())
2527 .map(|s| s.to_string())
2528 {
2529 constraints.push(ValidationConstraint::Format { format });
2530 }
2531
2532 if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
2534 constraints.push(ValidationConstraint::MultipleOf { value: multiple_of });
2535 }
2536
2537 if let Some(min_items) = schema
2539 .get("minItems")
2540 .and_then(|v| v.as_u64())
2541 .map(|v| v as usize)
2542 {
2543 constraints.push(ValidationConstraint::MinItems { value: min_items });
2544 }
2545
2546 if let Some(max_items) = schema
2548 .get("maxItems")
2549 .and_then(|v| v.as_u64())
2550 .map(|v| v as usize)
2551 {
2552 constraints.push(ValidationConstraint::MaxItems { value: max_items });
2553 }
2554
2555 if let Some(true) = schema.get("uniqueItems").and_then(|v| v.as_bool()) {
2557 constraints.push(ValidationConstraint::UniqueItems);
2558 }
2559
2560 if let Some(min_props) = schema
2562 .get("minProperties")
2563 .and_then(|v| v.as_u64())
2564 .map(|v| v as usize)
2565 {
2566 constraints.push(ValidationConstraint::MinProperties { value: min_props });
2567 }
2568
2569 if let Some(max_props) = schema
2571 .get("maxProperties")
2572 .and_then(|v| v.as_u64())
2573 .map(|v| v as usize)
2574 {
2575 constraints.push(ValidationConstraint::MaxProperties { value: max_props });
2576 }
2577
2578 if let Some(const_value) = schema.get("const").cloned() {
2580 constraints.push(ValidationConstraint::ConstValue { value: const_value });
2581 }
2582
2583 if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
2585 let properties: Vec<String> = required
2586 .iter()
2587 .filter_map(|v| v.as_str().map(|s| s.to_string()))
2588 .collect();
2589 if !properties.is_empty() {
2590 constraints.push(ValidationConstraint::Required { properties });
2591 }
2592 }
2593
2594 constraints
2595 }
2596
2597 fn get_expected_type(schema: &Value) -> Option<String> {
2599 if let Some(type_value) = schema.get("type") {
2600 if let Some(type_str) = type_value.as_str() {
2601 return Some(type_str.to_string());
2602 } else if let Some(type_array) = type_value.as_array() {
2603 let types: Vec<String> = type_array
2605 .iter()
2606 .filter_map(|v| v.as_str())
2607 .map(|s| s.to_string())
2608 .collect();
2609 if !types.is_empty() {
2610 return Some(types.join(" | "));
2611 }
2612 }
2613 }
2614 None
2615 }
2616
2617 fn wrap_output_schema(
2641 body_schema: &ObjectOrReference<ObjectSchema>,
2642 spec: &Spec,
2643 ) -> Result<Value, Error> {
2644 let mut visited = HashSet::new();
2646 let body_schema_json = match body_schema {
2647 ObjectOrReference::Object(obj_schema) => {
2648 Self::convert_object_schema_to_json_schema(obj_schema, spec, &mut visited)?
2649 }
2650 ObjectOrReference::Ref { ref_path, .. } => {
2651 let resolved = Self::resolve_reference(ref_path, spec, &mut visited)?;
2652 let result =
2653 Self::convert_object_schema_to_json_schema(&resolved, spec, &mut visited)?;
2654 visited.remove(ref_path);
2656 result
2657 }
2658 };
2659
2660 let error_schema = create_error_response_schema();
2661
2662 Ok(json!({
2663 "type": "object",
2664 "description": "Unified response structure with success and error variants",
2665 "required": ["status", "body"],
2666 "additionalProperties": false,
2667 "properties": {
2668 "status": {
2669 "type": "integer",
2670 "description": "HTTP status code",
2671 "minimum": 100,
2672 "maximum": 599
2673 },
2674 "body": {
2675 "description": "Response body - either success data or error information",
2676 "oneOf": [
2677 body_schema_json,
2678 error_schema
2679 ]
2680 }
2681 }
2682 }))
2683 }
2684}
2685
2686fn create_error_response_schema() -> Value {
2688 let root_schema = schema_for!(ErrorResponse);
2689 let schema_json = serde_json::to_value(root_schema).expect("Valid error schema");
2690
2691 let definitions = schema_json
2693 .get("$defs")
2694 .or_else(|| schema_json.get("definitions"))
2695 .cloned()
2696 .unwrap_or_else(|| json!({}));
2697
2698 let mut result = schema_json.clone();
2700 if let Some(obj) = result.as_object_mut() {
2701 obj.remove("$schema");
2702 obj.remove("$defs");
2703 obj.remove("definitions");
2704 obj.remove("title");
2705 }
2706
2707 inline_refs(&mut result, &definitions);
2709
2710 result
2711}
2712
2713fn inline_refs(schema: &mut Value, definitions: &Value) {
2715 match schema {
2716 Value::Object(obj) => {
2717 if let Some(ref_value) = obj.get("$ref").cloned()
2719 && let Some(ref_str) = ref_value.as_str()
2720 {
2721 let def_name = ref_str
2723 .strip_prefix("#/$defs/")
2724 .or_else(|| ref_str.strip_prefix("#/definitions/"));
2725
2726 if let Some(name) = def_name
2727 && let Some(definition) = definitions.get(name)
2728 {
2729 *schema = definition.clone();
2731 inline_refs(schema, definitions);
2733 return;
2734 }
2735 }
2736
2737 for (_, value) in obj.iter_mut() {
2739 inline_refs(value, definitions);
2740 }
2741 }
2742 Value::Array(arr) => {
2743 for item in arr.iter_mut() {
2745 inline_refs(item, definitions);
2746 }
2747 }
2748 _ => {} }
2750}
2751
2752#[derive(Debug, Clone)]
2754pub struct QueryParameter {
2755 pub value: Value,
2756 pub explode: bool,
2757}
2758
2759impl QueryParameter {
2760 pub fn new(value: Value, explode: bool) -> Self {
2761 Self { value, explode }
2762 }
2763}
2764
2765#[derive(Debug, Clone)]
2767pub struct ExtractedParameters {
2768 pub path: HashMap<String, Value>,
2769 pub query: HashMap<String, QueryParameter>,
2770 pub headers: HashMap<String, Value>,
2771 pub cookies: HashMap<String, Value>,
2772 pub body: HashMap<String, Value>,
2773 pub config: RequestConfig,
2774}
2775
2776#[derive(Debug, Clone)]
2778pub struct RequestConfig {
2779 pub timeout_seconds: u32,
2780 pub content_type: String,
2781}
2782
2783impl Default for RequestConfig {
2784 fn default() -> Self {
2785 Self {
2786 timeout_seconds: 30,
2787 content_type: mime::APPLICATION_JSON.to_string(),
2788 }
2789 }
2790}
2791
2792#[cfg(test)]
2793mod tests {
2794 use super::*;
2795
2796 use insta::assert_json_snapshot;
2797 use oas3::spec::{
2798 BooleanSchema, Components, MediaType, ObjectOrReference, ObjectSchema, Operation,
2799 Parameter, ParameterIn, RequestBody, Schema, SchemaType, SchemaTypeSet, Spec,
2800 };
2801 use rmcp::model::Tool;
2802 use serde_json::{Value, json};
2803 use std::collections::BTreeMap;
2804
2805 fn create_test_spec() -> Spec {
2807 Spec {
2808 openapi: "3.0.0".to_string(),
2809 info: oas3::spec::Info {
2810 title: "Test API".to_string(),
2811 version: "1.0.0".to_string(),
2812 summary: None,
2813 description: Some("Test API for unit tests".to_string()),
2814 terms_of_service: None,
2815 contact: None,
2816 license: None,
2817 extensions: Default::default(),
2818 },
2819 components: Some(Components {
2820 schemas: BTreeMap::new(),
2821 responses: BTreeMap::new(),
2822 parameters: BTreeMap::new(),
2823 examples: BTreeMap::new(),
2824 request_bodies: BTreeMap::new(),
2825 headers: BTreeMap::new(),
2826 security_schemes: BTreeMap::new(),
2827 links: BTreeMap::new(),
2828 callbacks: BTreeMap::new(),
2829 path_items: BTreeMap::new(),
2830 extensions: Default::default(),
2831 }),
2832 servers: vec![],
2833 paths: None,
2834 external_docs: None,
2835 tags: vec![],
2836 security: vec![],
2837 webhooks: BTreeMap::new(),
2838 extensions: Default::default(),
2839 }
2840 }
2841
2842 fn validate_tool_against_mcp_schema(metadata: &ToolMetadata) {
2843 let schema_content = std::fs::read_to_string("schema/2025-06-18/schema.json")
2844 .expect("Failed to read MCP schema file");
2845 let full_schema: Value =
2846 serde_json::from_str(&schema_content).expect("Failed to parse MCP schema JSON");
2847
2848 let tool_schema = json!({
2850 "$schema": "http://json-schema.org/draft-07/schema#",
2851 "definitions": full_schema.get("definitions"),
2852 "$ref": "#/definitions/Tool"
2853 });
2854
2855 let validator =
2856 jsonschema::validator_for(&tool_schema).expect("Failed to compile MCP Tool schema");
2857
2858 let tool = Tool::from(metadata);
2860
2861 let mcp_tool_json = serde_json::to_value(&tool).expect("Failed to serialize Tool to JSON");
2863
2864 let errors: Vec<String> = validator
2866 .iter_errors(&mcp_tool_json)
2867 .map(|e| e.to_string())
2868 .collect();
2869
2870 if !errors.is_empty() {
2871 panic!("Generated tool failed MCP schema validation: {errors:?}");
2872 }
2873 }
2874
2875 #[test]
2876 fn test_error_schema_structure() {
2877 let error_schema = create_error_response_schema();
2878
2879 assert!(error_schema.get("$schema").is_none());
2881 assert!(error_schema.get("definitions").is_none());
2882
2883 assert_json_snapshot!(error_schema);
2885 }
2886
2887 #[test]
2888 fn test_petstore_get_pet_by_id() {
2889 use oas3::spec::Response;
2890
2891 let mut operation = Operation {
2892 operation_id: Some("getPetById".to_string()),
2893 summary: Some("Find pet by ID".to_string()),
2894 description: Some("Returns a single pet".to_string()),
2895 tags: vec![],
2896 external_docs: None,
2897 parameters: vec![],
2898 request_body: None,
2899 responses: Default::default(),
2900 callbacks: Default::default(),
2901 deprecated: Some(false),
2902 security: vec![],
2903 servers: vec![],
2904 extensions: Default::default(),
2905 };
2906
2907 let param = Parameter {
2909 name: "petId".to_string(),
2910 location: ParameterIn::Path,
2911 description: Some("ID of pet to return".to_string()),
2912 required: Some(true),
2913 deprecated: Some(false),
2914 allow_empty_value: Some(false),
2915 style: None,
2916 explode: None,
2917 allow_reserved: Some(false),
2918 schema: Some(ObjectOrReference::Object(ObjectSchema {
2919 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2920 minimum: Some(serde_json::Number::from(1_i64)),
2921 format: Some("int64".to_string()),
2922 ..Default::default()
2923 })),
2924 example: None,
2925 examples: Default::default(),
2926 content: None,
2927 extensions: Default::default(),
2928 };
2929
2930 operation.parameters.push(ObjectOrReference::Object(param));
2931
2932 let mut responses = BTreeMap::new();
2934 let mut content = BTreeMap::new();
2935 content.insert(
2936 "application/json".to_string(),
2937 MediaType {
2938 extensions: Default::default(),
2939 schema: Some(ObjectOrReference::Object(ObjectSchema {
2940 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2941 properties: {
2942 let mut props = BTreeMap::new();
2943 props.insert(
2944 "id".to_string(),
2945 ObjectOrReference::Object(ObjectSchema {
2946 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2947 format: Some("int64".to_string()),
2948 ..Default::default()
2949 }),
2950 );
2951 props.insert(
2952 "name".to_string(),
2953 ObjectOrReference::Object(ObjectSchema {
2954 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2955 ..Default::default()
2956 }),
2957 );
2958 props.insert(
2959 "status".to_string(),
2960 ObjectOrReference::Object(ObjectSchema {
2961 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2962 ..Default::default()
2963 }),
2964 );
2965 props
2966 },
2967 required: vec!["id".to_string(), "name".to_string()],
2968 ..Default::default()
2969 })),
2970 examples: None,
2971 encoding: Default::default(),
2972 },
2973 );
2974
2975 responses.insert(
2976 "200".to_string(),
2977 ObjectOrReference::Object(Response {
2978 description: Some("successful operation".to_string()),
2979 headers: Default::default(),
2980 content,
2981 links: Default::default(),
2982 extensions: Default::default(),
2983 }),
2984 );
2985 operation.responses = Some(responses);
2986
2987 let spec = create_test_spec();
2988 let metadata = ToolGenerator::generate_tool_metadata(
2989 &operation,
2990 "get".to_string(),
2991 "/pet/{petId}".to_string(),
2992 &spec,
2993 false,
2994 false,
2995 )
2996 .unwrap();
2997
2998 assert_eq!(metadata.name, "getPetById");
2999 assert_eq!(metadata.method, "get");
3000 assert_eq!(metadata.path, "/pet/{petId}");
3001 assert!(
3002 metadata
3003 .description
3004 .clone()
3005 .unwrap()
3006 .contains("Find pet by ID")
3007 );
3008
3009 assert!(metadata.output_schema.is_some());
3011 let output_schema = metadata.output_schema.as_ref().unwrap();
3012
3013 insta::assert_json_snapshot!("test_petstore_get_pet_by_id_output_schema", output_schema);
3015
3016 validate_tool_against_mcp_schema(&metadata);
3018 }
3019
3020 #[test]
3021 fn test_convert_prefix_items_to_draft07_mixed_types() {
3022 let prefix_items = vec![
3025 ObjectOrReference::Object(ObjectSchema {
3026 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3027 format: Some("int32".to_string()),
3028 ..Default::default()
3029 }),
3030 ObjectOrReference::Object(ObjectSchema {
3031 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3032 ..Default::default()
3033 }),
3034 ];
3035
3036 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
3038
3039 let mut result = serde_json::Map::new();
3040 let spec = create_test_spec();
3041 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
3042 .unwrap();
3043
3044 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_mixed_types", result);
3046 }
3047
3048 #[test]
3049 fn test_convert_prefix_items_to_draft07_uniform_types() {
3050 let prefix_items = vec![
3052 ObjectOrReference::Object(ObjectSchema {
3053 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3054 ..Default::default()
3055 }),
3056 ObjectOrReference::Object(ObjectSchema {
3057 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3058 ..Default::default()
3059 }),
3060 ];
3061
3062 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
3064
3065 let mut result = serde_json::Map::new();
3066 let spec = create_test_spec();
3067 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
3068 .unwrap();
3069
3070 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_uniform_types", result);
3072 }
3073
3074 #[test]
3075 fn test_array_with_prefix_items_integration() {
3076 let param = Parameter {
3078 name: "coordinates".to_string(),
3079 location: ParameterIn::Query,
3080 description: Some("X,Y coordinates as tuple".to_string()),
3081 required: Some(true),
3082 deprecated: Some(false),
3083 allow_empty_value: Some(false),
3084 style: None,
3085 explode: None,
3086 allow_reserved: Some(false),
3087 schema: Some(ObjectOrReference::Object(ObjectSchema {
3088 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3089 prefix_items: vec![
3090 ObjectOrReference::Object(ObjectSchema {
3091 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
3092 format: Some("double".to_string()),
3093 ..Default::default()
3094 }),
3095 ObjectOrReference::Object(ObjectSchema {
3096 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
3097 format: Some("double".to_string()),
3098 ..Default::default()
3099 }),
3100 ],
3101 items: Some(Box::new(Schema::Boolean(BooleanSchema(false)))),
3102 ..Default::default()
3103 })),
3104 example: None,
3105 examples: Default::default(),
3106 content: None,
3107 extensions: Default::default(),
3108 };
3109
3110 let spec = create_test_spec();
3111 let (result, _annotations) =
3112 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, false)
3113 .unwrap();
3114
3115 insta::assert_json_snapshot!("test_array_with_prefix_items_integration", result);
3117 }
3118
3119 #[test]
3120 fn test_skip_tool_description() {
3121 let operation = Operation {
3122 operation_id: Some("getPetById".to_string()),
3123 summary: Some("Find pet by ID".to_string()),
3124 description: Some("Returns a single pet".to_string()),
3125 tags: vec![],
3126 external_docs: None,
3127 parameters: vec![],
3128 request_body: None,
3129 responses: Default::default(),
3130 callbacks: Default::default(),
3131 deprecated: Some(false),
3132 security: vec![],
3133 servers: vec![],
3134 extensions: Default::default(),
3135 };
3136
3137 let spec = create_test_spec();
3138 let metadata = ToolGenerator::generate_tool_metadata(
3139 &operation,
3140 "get".to_string(),
3141 "/pet/{petId}".to_string(),
3142 &spec,
3143 true,
3144 false,
3145 )
3146 .unwrap();
3147
3148 assert_eq!(metadata.name, "getPetById");
3149 assert_eq!(metadata.method, "get");
3150 assert_eq!(metadata.path, "/pet/{petId}");
3151 assert!(metadata.description.is_none());
3152
3153 insta::assert_json_snapshot!("test_skip_tool_description", metadata);
3155
3156 validate_tool_against_mcp_schema(&metadata);
3158 }
3159
3160 #[test]
3161 fn test_keep_tool_description() {
3162 let description = Some("Returns a single pet".to_string());
3163 let operation = Operation {
3164 operation_id: Some("getPetById".to_string()),
3165 summary: Some("Find pet by ID".to_string()),
3166 description: description.clone(),
3167 tags: vec![],
3168 external_docs: None,
3169 parameters: vec![],
3170 request_body: None,
3171 responses: Default::default(),
3172 callbacks: Default::default(),
3173 deprecated: Some(false),
3174 security: vec![],
3175 servers: vec![],
3176 extensions: Default::default(),
3177 };
3178
3179 let spec = create_test_spec();
3180 let metadata = ToolGenerator::generate_tool_metadata(
3181 &operation,
3182 "get".to_string(),
3183 "/pet/{petId}".to_string(),
3184 &spec,
3185 false,
3186 false,
3187 )
3188 .unwrap();
3189
3190 assert_eq!(metadata.name, "getPetById");
3191 assert_eq!(metadata.method, "get");
3192 assert_eq!(metadata.path, "/pet/{petId}");
3193 assert!(metadata.description.is_some());
3194
3195 insta::assert_json_snapshot!("test_keep_tool_description", metadata);
3197
3198 validate_tool_against_mcp_schema(&metadata);
3200 }
3201
3202 #[test]
3203 fn test_skip_parameter_descriptions() {
3204 let param = Parameter {
3205 name: "status".to_string(),
3206 location: ParameterIn::Query,
3207 description: Some("Filter by status".to_string()),
3208 required: Some(false),
3209 deprecated: Some(false),
3210 allow_empty_value: Some(false),
3211 style: None,
3212 explode: None,
3213 allow_reserved: Some(false),
3214 schema: Some(ObjectOrReference::Object(ObjectSchema {
3215 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3216 enum_values: vec![json!("available"), json!("pending"), json!("sold")],
3217 ..Default::default()
3218 })),
3219 example: Some(json!("available")),
3220 examples: Default::default(),
3221 content: None,
3222 extensions: Default::default(),
3223 };
3224
3225 let spec = create_test_spec();
3226 let (schema, _) =
3227 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, true)
3228 .unwrap();
3229
3230 assert!(schema.get("description").is_none());
3232
3233 assert_eq!(schema.get("type").unwrap(), "string");
3235 assert_eq!(schema.get("example").unwrap(), "available");
3236
3237 insta::assert_json_snapshot!("test_skip_parameter_descriptions", schema);
3238 }
3239
3240 #[test]
3241 fn test_keep_parameter_descriptions() {
3242 let param = Parameter {
3243 name: "status".to_string(),
3244 location: ParameterIn::Query,
3245 description: Some("Filter by status".to_string()),
3246 required: Some(false),
3247 deprecated: Some(false),
3248 allow_empty_value: Some(false),
3249 style: None,
3250 explode: None,
3251 allow_reserved: Some(false),
3252 schema: Some(ObjectOrReference::Object(ObjectSchema {
3253 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3254 enum_values: vec![json!("available"), json!("pending"), json!("sold")],
3255 ..Default::default()
3256 })),
3257 example: Some(json!("available")),
3258 examples: Default::default(),
3259 content: None,
3260 extensions: Default::default(),
3261 };
3262
3263 let spec = create_test_spec();
3264 let (schema, _) =
3265 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, false)
3266 .unwrap();
3267
3268 assert!(schema.get("description").is_some());
3270 let description = schema.get("description").unwrap().as_str().unwrap();
3271 assert!(description.contains("Filter by status"));
3272 assert!(description.contains("Example: `\"available\"`"));
3273
3274 assert_eq!(schema.get("type").unwrap(), "string");
3276 assert_eq!(schema.get("example").unwrap(), "available");
3277
3278 insta::assert_json_snapshot!("test_keep_parameter_descriptions", schema);
3279 }
3280
3281 #[test]
3282 fn test_array_with_regular_items_schema() {
3283 let param = Parameter {
3285 name: "tags".to_string(),
3286 location: ParameterIn::Query,
3287 description: Some("List of tags".to_string()),
3288 required: Some(false),
3289 deprecated: Some(false),
3290 allow_empty_value: Some(false),
3291 style: None,
3292 explode: None,
3293 allow_reserved: Some(false),
3294 schema: Some(ObjectOrReference::Object(ObjectSchema {
3295 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3296 items: Some(Box::new(Schema::Object(Box::new(
3297 ObjectOrReference::Object(ObjectSchema {
3298 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3299 min_length: Some(1),
3300 max_length: Some(50),
3301 ..Default::default()
3302 }),
3303 )))),
3304 ..Default::default()
3305 })),
3306 example: None,
3307 examples: Default::default(),
3308 content: None,
3309 extensions: Default::default(),
3310 };
3311
3312 let spec = create_test_spec();
3313 let (result, _annotations) =
3314 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, false)
3315 .unwrap();
3316
3317 insta::assert_json_snapshot!("test_array_with_regular_items_schema", result);
3319 }
3320
3321 #[test]
3322 fn test_request_body_object_schema() {
3323 let operation = Operation {
3325 operation_id: Some("createPet".to_string()),
3326 summary: Some("Create a new pet".to_string()),
3327 description: Some("Creates a new pet in the store".to_string()),
3328 tags: vec![],
3329 external_docs: None,
3330 parameters: vec![],
3331 request_body: Some(ObjectOrReference::Object(RequestBody {
3332 description: Some("Pet object that needs to be added to the store".to_string()),
3333 content: {
3334 let mut content = BTreeMap::new();
3335 content.insert(
3336 "application/json".to_string(),
3337 MediaType {
3338 extensions: Default::default(),
3339 schema: Some(ObjectOrReference::Object(ObjectSchema {
3340 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3341 ..Default::default()
3342 })),
3343 examples: None,
3344 encoding: Default::default(),
3345 },
3346 );
3347 content
3348 },
3349 required: Some(true),
3350 })),
3351 responses: Default::default(),
3352 callbacks: Default::default(),
3353 deprecated: Some(false),
3354 security: vec![],
3355 servers: vec![],
3356 extensions: Default::default(),
3357 };
3358
3359 let spec = create_test_spec();
3360 let metadata = ToolGenerator::generate_tool_metadata(
3361 &operation,
3362 "post".to_string(),
3363 "/pets".to_string(),
3364 &spec,
3365 false,
3366 false,
3367 )
3368 .unwrap();
3369
3370 let properties = metadata
3372 .parameters
3373 .get("properties")
3374 .unwrap()
3375 .as_object()
3376 .unwrap();
3377 assert!(properties.contains_key("request_body"));
3378
3379 let required = metadata
3381 .parameters
3382 .get("required")
3383 .unwrap()
3384 .as_array()
3385 .unwrap();
3386 assert!(required.contains(&json!("request_body")));
3387
3388 let request_body_schema = properties.get("request_body").unwrap();
3390 insta::assert_json_snapshot!("test_request_body_object_schema", request_body_schema);
3391
3392 validate_tool_against_mcp_schema(&metadata);
3394 }
3395
3396 #[test]
3397 fn test_request_body_array_schema() {
3398 let operation = Operation {
3400 operation_id: Some("createPets".to_string()),
3401 summary: Some("Create multiple pets".to_string()),
3402 description: None,
3403 tags: vec![],
3404 external_docs: None,
3405 parameters: vec![],
3406 request_body: Some(ObjectOrReference::Object(RequestBody {
3407 description: Some("Array of pet objects".to_string()),
3408 content: {
3409 let mut content = BTreeMap::new();
3410 content.insert(
3411 "application/json".to_string(),
3412 MediaType {
3413 extensions: Default::default(),
3414 schema: Some(ObjectOrReference::Object(ObjectSchema {
3415 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3416 items: Some(Box::new(Schema::Object(Box::new(
3417 ObjectOrReference::Object(ObjectSchema {
3418 schema_type: Some(SchemaTypeSet::Single(
3419 SchemaType::Object,
3420 )),
3421 ..Default::default()
3422 }),
3423 )))),
3424 ..Default::default()
3425 })),
3426 examples: None,
3427 encoding: Default::default(),
3428 },
3429 );
3430 content
3431 },
3432 required: Some(false),
3433 })),
3434 responses: Default::default(),
3435 callbacks: Default::default(),
3436 deprecated: Some(false),
3437 security: vec![],
3438 servers: vec![],
3439 extensions: Default::default(),
3440 };
3441
3442 let spec = create_test_spec();
3443 let metadata = ToolGenerator::generate_tool_metadata(
3444 &operation,
3445 "post".to_string(),
3446 "/pets/batch".to_string(),
3447 &spec,
3448 false,
3449 false,
3450 )
3451 .unwrap();
3452
3453 let properties = metadata
3455 .parameters
3456 .get("properties")
3457 .unwrap()
3458 .as_object()
3459 .unwrap();
3460 assert!(properties.contains_key("request_body"));
3461
3462 let required = metadata
3464 .parameters
3465 .get("required")
3466 .unwrap()
3467 .as_array()
3468 .unwrap();
3469 assert!(!required.contains(&json!("request_body")));
3470
3471 let request_body_schema = properties.get("request_body").unwrap();
3473 insta::assert_json_snapshot!("test_request_body_array_schema", request_body_schema);
3474
3475 validate_tool_against_mcp_schema(&metadata);
3477 }
3478
3479 #[test]
3480 fn test_request_body_string_schema() {
3481 let operation = Operation {
3483 operation_id: Some("updatePetName".to_string()),
3484 summary: Some("Update pet name".to_string()),
3485 description: None,
3486 tags: vec![],
3487 external_docs: None,
3488 parameters: vec![],
3489 request_body: Some(ObjectOrReference::Object(RequestBody {
3490 description: None,
3491 content: {
3492 let mut content = BTreeMap::new();
3493 content.insert(
3494 "text/plain".to_string(),
3495 MediaType {
3496 extensions: Default::default(),
3497 schema: Some(ObjectOrReference::Object(ObjectSchema {
3498 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3499 min_length: Some(1),
3500 max_length: Some(100),
3501 ..Default::default()
3502 })),
3503 examples: None,
3504 encoding: Default::default(),
3505 },
3506 );
3507 content
3508 },
3509 required: Some(true),
3510 })),
3511 responses: Default::default(),
3512 callbacks: Default::default(),
3513 deprecated: Some(false),
3514 security: vec![],
3515 servers: vec![],
3516 extensions: Default::default(),
3517 };
3518
3519 let spec = create_test_spec();
3520 let metadata = ToolGenerator::generate_tool_metadata(
3521 &operation,
3522 "put".to_string(),
3523 "/pets/{petId}/name".to_string(),
3524 &spec,
3525 false,
3526 false,
3527 )
3528 .unwrap();
3529
3530 let properties = metadata
3532 .parameters
3533 .get("properties")
3534 .unwrap()
3535 .as_object()
3536 .unwrap();
3537 let request_body_schema = properties.get("request_body").unwrap();
3538 insta::assert_json_snapshot!("test_request_body_string_schema", request_body_schema);
3539
3540 validate_tool_against_mcp_schema(&metadata);
3542 }
3543
3544 #[test]
3545 fn test_request_body_ref_schema() {
3546 let operation = Operation {
3548 operation_id: Some("updatePet".to_string()),
3549 summary: Some("Update existing pet".to_string()),
3550 description: None,
3551 tags: vec![],
3552 external_docs: None,
3553 parameters: vec![],
3554 request_body: Some(ObjectOrReference::Ref {
3555 ref_path: "#/components/requestBodies/PetBody".to_string(),
3556 summary: None,
3557 description: None,
3558 }),
3559 responses: Default::default(),
3560 callbacks: Default::default(),
3561 deprecated: Some(false),
3562 security: vec![],
3563 servers: vec![],
3564 extensions: Default::default(),
3565 };
3566
3567 let spec = create_test_spec();
3568 let metadata = ToolGenerator::generate_tool_metadata(
3569 &operation,
3570 "put".to_string(),
3571 "/pets/{petId}".to_string(),
3572 &spec,
3573 false,
3574 false,
3575 )
3576 .unwrap();
3577
3578 let properties = metadata
3580 .parameters
3581 .get("properties")
3582 .unwrap()
3583 .as_object()
3584 .unwrap();
3585 let request_body_schema = properties.get("request_body").unwrap();
3586 insta::assert_json_snapshot!("test_request_body_ref_schema", request_body_schema);
3587
3588 validate_tool_against_mcp_schema(&metadata);
3590 }
3591
3592 #[test]
3593 fn test_no_request_body_for_get() {
3594 let operation = Operation {
3596 operation_id: Some("listPets".to_string()),
3597 summary: Some("List all pets".to_string()),
3598 description: None,
3599 tags: vec![],
3600 external_docs: None,
3601 parameters: vec![],
3602 request_body: None,
3603 responses: Default::default(),
3604 callbacks: Default::default(),
3605 deprecated: Some(false),
3606 security: vec![],
3607 servers: vec![],
3608 extensions: Default::default(),
3609 };
3610
3611 let spec = create_test_spec();
3612 let metadata = ToolGenerator::generate_tool_metadata(
3613 &operation,
3614 "get".to_string(),
3615 "/pets".to_string(),
3616 &spec,
3617 false,
3618 false,
3619 )
3620 .unwrap();
3621
3622 let properties = metadata
3624 .parameters
3625 .get("properties")
3626 .unwrap()
3627 .as_object()
3628 .unwrap();
3629 assert!(!properties.contains_key("request_body"));
3630
3631 validate_tool_against_mcp_schema(&metadata);
3633 }
3634
3635 #[test]
3636 fn test_request_body_simple_object_with_properties() {
3637 let operation = Operation {
3639 operation_id: Some("updatePetStatus".to_string()),
3640 summary: Some("Update pet status".to_string()),
3641 description: None,
3642 tags: vec![],
3643 external_docs: None,
3644 parameters: vec![],
3645 request_body: Some(ObjectOrReference::Object(RequestBody {
3646 description: Some("Pet status update".to_string()),
3647 content: {
3648 let mut content = BTreeMap::new();
3649 content.insert(
3650 "application/json".to_string(),
3651 MediaType {
3652 extensions: Default::default(),
3653 schema: Some(ObjectOrReference::Object(ObjectSchema {
3654 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3655 properties: {
3656 let mut props = BTreeMap::new();
3657 props.insert(
3658 "status".to_string(),
3659 ObjectOrReference::Object(ObjectSchema {
3660 schema_type: Some(SchemaTypeSet::Single(
3661 SchemaType::String,
3662 )),
3663 ..Default::default()
3664 }),
3665 );
3666 props.insert(
3667 "reason".to_string(),
3668 ObjectOrReference::Object(ObjectSchema {
3669 schema_type: Some(SchemaTypeSet::Single(
3670 SchemaType::String,
3671 )),
3672 ..Default::default()
3673 }),
3674 );
3675 props
3676 },
3677 required: vec!["status".to_string()],
3678 ..Default::default()
3679 })),
3680 examples: None,
3681 encoding: Default::default(),
3682 },
3683 );
3684 content
3685 },
3686 required: Some(false),
3687 })),
3688 responses: Default::default(),
3689 callbacks: Default::default(),
3690 deprecated: Some(false),
3691 security: vec![],
3692 servers: vec![],
3693 extensions: Default::default(),
3694 };
3695
3696 let spec = create_test_spec();
3697 let metadata = ToolGenerator::generate_tool_metadata(
3698 &operation,
3699 "patch".to_string(),
3700 "/pets/{petId}/status".to_string(),
3701 &spec,
3702 false,
3703 false,
3704 )
3705 .unwrap();
3706
3707 let properties = metadata
3709 .parameters
3710 .get("properties")
3711 .unwrap()
3712 .as_object()
3713 .unwrap();
3714 let request_body_schema = properties.get("request_body").unwrap();
3715 insta::assert_json_snapshot!(
3716 "test_request_body_simple_object_with_properties",
3717 request_body_schema
3718 );
3719
3720 let required = metadata
3722 .parameters
3723 .get("required")
3724 .unwrap()
3725 .as_array()
3726 .unwrap();
3727 assert!(!required.contains(&json!("request_body")));
3728
3729 validate_tool_against_mcp_schema(&metadata);
3731 }
3732
3733 #[test]
3734 fn test_request_body_with_nested_properties() {
3735 let operation = Operation {
3737 operation_id: Some("createUser".to_string()),
3738 summary: Some("Create a new user".to_string()),
3739 description: None,
3740 tags: vec![],
3741 external_docs: None,
3742 parameters: vec![],
3743 request_body: Some(ObjectOrReference::Object(RequestBody {
3744 description: Some("User creation data".to_string()),
3745 content: {
3746 let mut content = BTreeMap::new();
3747 content.insert(
3748 "application/json".to_string(),
3749 MediaType {
3750 extensions: Default::default(),
3751 schema: Some(ObjectOrReference::Object(ObjectSchema {
3752 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3753 properties: {
3754 let mut props = BTreeMap::new();
3755 props.insert(
3756 "name".to_string(),
3757 ObjectOrReference::Object(ObjectSchema {
3758 schema_type: Some(SchemaTypeSet::Single(
3759 SchemaType::String,
3760 )),
3761 ..Default::default()
3762 }),
3763 );
3764 props.insert(
3765 "age".to_string(),
3766 ObjectOrReference::Object(ObjectSchema {
3767 schema_type: Some(SchemaTypeSet::Single(
3768 SchemaType::Integer,
3769 )),
3770 minimum: Some(serde_json::Number::from(0)),
3771 maximum: Some(serde_json::Number::from(150)),
3772 ..Default::default()
3773 }),
3774 );
3775 props
3776 },
3777 required: vec!["name".to_string()],
3778 ..Default::default()
3779 })),
3780 examples: None,
3781 encoding: Default::default(),
3782 },
3783 );
3784 content
3785 },
3786 required: Some(true),
3787 })),
3788 responses: Default::default(),
3789 callbacks: Default::default(),
3790 deprecated: Some(false),
3791 security: vec![],
3792 servers: vec![],
3793 extensions: Default::default(),
3794 };
3795
3796 let spec = create_test_spec();
3797 let metadata = ToolGenerator::generate_tool_metadata(
3798 &operation,
3799 "post".to_string(),
3800 "/users".to_string(),
3801 &spec,
3802 false,
3803 false,
3804 )
3805 .unwrap();
3806
3807 let properties = metadata
3809 .parameters
3810 .get("properties")
3811 .unwrap()
3812 .as_object()
3813 .unwrap();
3814 let request_body_schema = properties.get("request_body").unwrap();
3815 insta::assert_json_snapshot!(
3816 "test_request_body_with_nested_properties",
3817 request_body_schema
3818 );
3819
3820 validate_tool_against_mcp_schema(&metadata);
3822 }
3823
3824 #[test]
3825 fn test_operation_without_responses_has_no_output_schema() {
3826 let operation = Operation {
3827 operation_id: Some("testOperation".to_string()),
3828 summary: Some("Test operation".to_string()),
3829 description: None,
3830 tags: vec![],
3831 external_docs: None,
3832 parameters: vec![],
3833 request_body: None,
3834 responses: None,
3835 callbacks: Default::default(),
3836 deprecated: Some(false),
3837 security: vec![],
3838 servers: vec![],
3839 extensions: Default::default(),
3840 };
3841
3842 let spec = create_test_spec();
3843 let metadata = ToolGenerator::generate_tool_metadata(
3844 &operation,
3845 "get".to_string(),
3846 "/test".to_string(),
3847 &spec,
3848 false,
3849 false,
3850 )
3851 .unwrap();
3852
3853 assert!(metadata.output_schema.is_none());
3855
3856 validate_tool_against_mcp_schema(&metadata);
3858 }
3859
3860 #[test]
3861 fn test_extract_output_schema_with_200_response() {
3862 use oas3::spec::Response;
3863
3864 let mut responses = BTreeMap::new();
3866 let mut content = BTreeMap::new();
3867 content.insert(
3868 "application/json".to_string(),
3869 MediaType {
3870 extensions: Default::default(),
3871 schema: Some(ObjectOrReference::Object(ObjectSchema {
3872 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3873 properties: {
3874 let mut props = BTreeMap::new();
3875 props.insert(
3876 "id".to_string(),
3877 ObjectOrReference::Object(ObjectSchema {
3878 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3879 ..Default::default()
3880 }),
3881 );
3882 props.insert(
3883 "name".to_string(),
3884 ObjectOrReference::Object(ObjectSchema {
3885 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3886 ..Default::default()
3887 }),
3888 );
3889 props
3890 },
3891 required: vec!["id".to_string(), "name".to_string()],
3892 ..Default::default()
3893 })),
3894 examples: None,
3895 encoding: Default::default(),
3896 },
3897 );
3898
3899 responses.insert(
3900 "200".to_string(),
3901 ObjectOrReference::Object(Response {
3902 description: Some("Successful response".to_string()),
3903 headers: Default::default(),
3904 content,
3905 links: Default::default(),
3906 extensions: Default::default(),
3907 }),
3908 );
3909
3910 let spec = create_test_spec();
3911 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
3912
3913 insta::assert_json_snapshot!(result);
3915 }
3916
3917 #[test]
3918 fn test_extract_output_schema_with_201_response() {
3919 use oas3::spec::Response;
3920
3921 let mut responses = BTreeMap::new();
3923 let mut content = BTreeMap::new();
3924 content.insert(
3925 "application/json".to_string(),
3926 MediaType {
3927 extensions: Default::default(),
3928 schema: Some(ObjectOrReference::Object(ObjectSchema {
3929 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3930 properties: {
3931 let mut props = BTreeMap::new();
3932 props.insert(
3933 "created".to_string(),
3934 ObjectOrReference::Object(ObjectSchema {
3935 schema_type: Some(SchemaTypeSet::Single(SchemaType::Boolean)),
3936 ..Default::default()
3937 }),
3938 );
3939 props
3940 },
3941 ..Default::default()
3942 })),
3943 examples: None,
3944 encoding: Default::default(),
3945 },
3946 );
3947
3948 responses.insert(
3949 "201".to_string(),
3950 ObjectOrReference::Object(Response {
3951 description: Some("Created".to_string()),
3952 headers: Default::default(),
3953 content,
3954 links: Default::default(),
3955 extensions: Default::default(),
3956 }),
3957 );
3958
3959 let spec = create_test_spec();
3960 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
3961
3962 insta::assert_json_snapshot!(result);
3964 }
3965
3966 #[test]
3967 fn test_extract_output_schema_with_2xx_response() {
3968 use oas3::spec::Response;
3969
3970 let mut responses = BTreeMap::new();
3972 let mut content = BTreeMap::new();
3973 content.insert(
3974 "application/json".to_string(),
3975 MediaType {
3976 extensions: Default::default(),
3977 schema: Some(ObjectOrReference::Object(ObjectSchema {
3978 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3979 items: Some(Box::new(Schema::Object(Box::new(
3980 ObjectOrReference::Object(ObjectSchema {
3981 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3982 ..Default::default()
3983 }),
3984 )))),
3985 ..Default::default()
3986 })),
3987 examples: None,
3988 encoding: Default::default(),
3989 },
3990 );
3991
3992 responses.insert(
3993 "2XX".to_string(),
3994 ObjectOrReference::Object(Response {
3995 description: Some("Success".to_string()),
3996 headers: Default::default(),
3997 content,
3998 links: Default::default(),
3999 extensions: Default::default(),
4000 }),
4001 );
4002
4003 let spec = create_test_spec();
4004 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4005
4006 insta::assert_json_snapshot!(result);
4008 }
4009
4010 #[test]
4011 fn test_extract_output_schema_no_responses() {
4012 let spec = create_test_spec();
4013 let result = ToolGenerator::extract_output_schema(&None, &spec).unwrap();
4014
4015 insta::assert_json_snapshot!(result);
4017 }
4018
4019 #[test]
4020 fn test_extract_output_schema_only_error_responses() {
4021 use oas3::spec::Response;
4022
4023 let mut responses = BTreeMap::new();
4025 responses.insert(
4026 "404".to_string(),
4027 ObjectOrReference::Object(Response {
4028 description: Some("Not found".to_string()),
4029 headers: Default::default(),
4030 content: Default::default(),
4031 links: Default::default(),
4032 extensions: Default::default(),
4033 }),
4034 );
4035 responses.insert(
4036 "500".to_string(),
4037 ObjectOrReference::Object(Response {
4038 description: Some("Server error".to_string()),
4039 headers: Default::default(),
4040 content: Default::default(),
4041 links: Default::default(),
4042 extensions: Default::default(),
4043 }),
4044 );
4045
4046 let spec = create_test_spec();
4047 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4048
4049 insta::assert_json_snapshot!(result);
4051 }
4052
4053 #[test]
4054 fn test_extract_output_schema_with_ref() {
4055 use oas3::spec::Response;
4056
4057 let mut spec = create_test_spec();
4059 let mut schemas = BTreeMap::new();
4060 schemas.insert(
4061 "Pet".to_string(),
4062 ObjectOrReference::Object(ObjectSchema {
4063 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4064 properties: {
4065 let mut props = BTreeMap::new();
4066 props.insert(
4067 "name".to_string(),
4068 ObjectOrReference::Object(ObjectSchema {
4069 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4070 ..Default::default()
4071 }),
4072 );
4073 props
4074 },
4075 ..Default::default()
4076 }),
4077 );
4078 spec.components.as_mut().unwrap().schemas = schemas;
4079
4080 let mut responses = BTreeMap::new();
4082 let mut content = BTreeMap::new();
4083 content.insert(
4084 "application/json".to_string(),
4085 MediaType {
4086 extensions: Default::default(),
4087 schema: Some(ObjectOrReference::Ref {
4088 ref_path: "#/components/schemas/Pet".to_string(),
4089 summary: None,
4090 description: None,
4091 }),
4092 examples: None,
4093 encoding: Default::default(),
4094 },
4095 );
4096
4097 responses.insert(
4098 "200".to_string(),
4099 ObjectOrReference::Object(Response {
4100 description: Some("Success".to_string()),
4101 headers: Default::default(),
4102 content,
4103 links: Default::default(),
4104 extensions: Default::default(),
4105 }),
4106 );
4107
4108 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4109
4110 insta::assert_json_snapshot!(result);
4112 }
4113
4114 #[test]
4115 fn test_generate_tool_metadata_includes_output_schema() {
4116 use oas3::spec::Response;
4117
4118 let mut operation = Operation {
4119 operation_id: Some("getPet".to_string()),
4120 summary: Some("Get a pet".to_string()),
4121 description: None,
4122 tags: vec![],
4123 external_docs: None,
4124 parameters: vec![],
4125 request_body: None,
4126 responses: Default::default(),
4127 callbacks: Default::default(),
4128 deprecated: Some(false),
4129 security: vec![],
4130 servers: vec![],
4131 extensions: Default::default(),
4132 };
4133
4134 let mut responses = BTreeMap::new();
4136 let mut content = BTreeMap::new();
4137 content.insert(
4138 "application/json".to_string(),
4139 MediaType {
4140 extensions: Default::default(),
4141 schema: Some(ObjectOrReference::Object(ObjectSchema {
4142 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4143 properties: {
4144 let mut props = BTreeMap::new();
4145 props.insert(
4146 "id".to_string(),
4147 ObjectOrReference::Object(ObjectSchema {
4148 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4149 ..Default::default()
4150 }),
4151 );
4152 props
4153 },
4154 ..Default::default()
4155 })),
4156 examples: None,
4157 encoding: Default::default(),
4158 },
4159 );
4160
4161 responses.insert(
4162 "200".to_string(),
4163 ObjectOrReference::Object(Response {
4164 description: Some("Success".to_string()),
4165 headers: Default::default(),
4166 content,
4167 links: Default::default(),
4168 extensions: Default::default(),
4169 }),
4170 );
4171 operation.responses = Some(responses);
4172
4173 let spec = create_test_spec();
4174 let metadata = ToolGenerator::generate_tool_metadata(
4175 &operation,
4176 "get".to_string(),
4177 "/pets/{id}".to_string(),
4178 &spec,
4179 false,
4180 false,
4181 )
4182 .unwrap();
4183
4184 assert!(metadata.output_schema.is_some());
4186 let output_schema = metadata.output_schema.as_ref().unwrap();
4187
4188 insta::assert_json_snapshot!(
4190 "test_generate_tool_metadata_includes_output_schema",
4191 output_schema
4192 );
4193
4194 validate_tool_against_mcp_schema(&metadata);
4196 }
4197
4198 #[test]
4199 fn test_sanitize_property_name() {
4200 assert_eq!(sanitize_property_name("user name"), "user_name");
4202 assert_eq!(
4203 sanitize_property_name("first name last name"),
4204 "first_name_last_name"
4205 );
4206
4207 assert_eq!(sanitize_property_name("user(admin)"), "user_admin");
4209 assert_eq!(sanitize_property_name("user[admin]"), "user_admin");
4210 assert_eq!(sanitize_property_name("price($)"), "price");
4211 assert_eq!(sanitize_property_name("email@address"), "email_address");
4212 assert_eq!(sanitize_property_name("item#1"), "item_1");
4213 assert_eq!(sanitize_property_name("a/b/c"), "a_b_c");
4214
4215 assert_eq!(sanitize_property_name("user_name"), "user_name");
4217 assert_eq!(sanitize_property_name("userName123"), "userName123");
4218 assert_eq!(sanitize_property_name("user.name"), "user.name");
4219 assert_eq!(sanitize_property_name("user-name"), "user-name");
4220
4221 assert_eq!(sanitize_property_name("123name"), "param_123name");
4223 assert_eq!(sanitize_property_name("1st_place"), "param_1st_place");
4224
4225 assert_eq!(sanitize_property_name(""), "param_");
4227
4228 let long_name = "a".repeat(100);
4230 assert_eq!(sanitize_property_name(&long_name).len(), 64);
4231
4232 assert_eq!(sanitize_property_name("!@#$%^&*()"), "param_");
4235 }
4236
4237 #[test]
4238 fn test_sanitize_property_name_trailing_underscores() {
4239 assert_eq!(sanitize_property_name("page[size]"), "page_size");
4241 assert_eq!(sanitize_property_name("user[id]"), "user_id");
4242 assert_eq!(sanitize_property_name("field[]"), "field");
4243
4244 assert_eq!(sanitize_property_name("field___"), "field");
4246 assert_eq!(sanitize_property_name("test[[["), "test");
4247 }
4248
4249 #[test]
4250 fn test_sanitize_property_name_consecutive_underscores() {
4251 assert_eq!(sanitize_property_name("user__name"), "user_name");
4253 assert_eq!(sanitize_property_name("first___last"), "first_last");
4254 assert_eq!(sanitize_property_name("a____b____c"), "a_b_c");
4255
4256 assert_eq!(sanitize_property_name("user[[name]]"), "user_name");
4258 assert_eq!(sanitize_property_name("field@#$value"), "field_value");
4259 }
4260
4261 #[test]
4262 fn test_sanitize_property_name_edge_cases() {
4263 assert_eq!(sanitize_property_name("_private"), "_private");
4265 assert_eq!(sanitize_property_name("__dunder"), "_dunder");
4266
4267 assert_eq!(sanitize_property_name("[[["), "param_");
4269 assert_eq!(sanitize_property_name("@@@"), "param_");
4270
4271 assert_eq!(sanitize_property_name(""), "param_");
4273
4274 assert_eq!(sanitize_property_name("_field[size]"), "_field_size");
4276 assert_eq!(sanitize_property_name("__test__"), "_test");
4277 }
4278
4279 #[test]
4280 fn test_sanitize_property_name_complex_cases() {
4281 assert_eq!(sanitize_property_name("page[size]"), "page_size");
4283 assert_eq!(sanitize_property_name("filter[status]"), "filter_status");
4284 assert_eq!(
4285 sanitize_property_name("sort[-created_at]"),
4286 "sort_-created_at"
4287 );
4288 assert_eq!(
4289 sanitize_property_name("include[author.posts]"),
4290 "include_author.posts"
4291 );
4292
4293 let long_name = "very_long_field_name_with_special[characters]_that_needs_truncation_____";
4295 let expected = "very_long_field_name_with_special_characters_that_needs_truncat";
4296 assert_eq!(sanitize_property_name(long_name), expected);
4297 }
4298
4299 #[test]
4300 fn test_property_sanitization_with_annotations() {
4301 let spec = create_test_spec();
4302 let mut visited = HashSet::new();
4303
4304 let obj_schema = ObjectSchema {
4306 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4307 properties: {
4308 let mut props = BTreeMap::new();
4309 props.insert(
4311 "user name".to_string(),
4312 ObjectOrReference::Object(ObjectSchema {
4313 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4314 ..Default::default()
4315 }),
4316 );
4317 props.insert(
4319 "price($)".to_string(),
4320 ObjectOrReference::Object(ObjectSchema {
4321 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
4322 ..Default::default()
4323 }),
4324 );
4325 props.insert(
4327 "validName".to_string(),
4328 ObjectOrReference::Object(ObjectSchema {
4329 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4330 ..Default::default()
4331 }),
4332 );
4333 props
4334 },
4335 ..Default::default()
4336 };
4337
4338 let result =
4339 ToolGenerator::convert_object_schema_to_json_schema(&obj_schema, &spec, &mut visited)
4340 .unwrap();
4341
4342 insta::assert_json_snapshot!("test_property_sanitization_with_annotations", result);
4344 }
4345
4346 #[test]
4347 fn test_parameter_sanitization_and_extraction() {
4348 let spec = create_test_spec();
4349
4350 let operation = Operation {
4352 operation_id: Some("testOp".to_string()),
4353 parameters: vec![
4354 ObjectOrReference::Object(Parameter {
4356 name: "user(id)".to_string(),
4357 location: ParameterIn::Path,
4358 description: Some("User ID".to_string()),
4359 required: Some(true),
4360 deprecated: Some(false),
4361 allow_empty_value: Some(false),
4362 style: None,
4363 explode: None,
4364 allow_reserved: Some(false),
4365 schema: Some(ObjectOrReference::Object(ObjectSchema {
4366 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4367 ..Default::default()
4368 })),
4369 example: None,
4370 examples: Default::default(),
4371 content: None,
4372 extensions: Default::default(),
4373 }),
4374 ObjectOrReference::Object(Parameter {
4376 name: "page size".to_string(),
4377 location: ParameterIn::Query,
4378 description: Some("Page size".to_string()),
4379 required: Some(false),
4380 deprecated: Some(false),
4381 allow_empty_value: Some(false),
4382 style: None,
4383 explode: None,
4384 allow_reserved: Some(false),
4385 schema: Some(ObjectOrReference::Object(ObjectSchema {
4386 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4387 ..Default::default()
4388 })),
4389 example: None,
4390 examples: Default::default(),
4391 content: None,
4392 extensions: Default::default(),
4393 }),
4394 ObjectOrReference::Object(Parameter {
4396 name: "auth-token!".to_string(),
4397 location: ParameterIn::Header,
4398 description: Some("Auth token".to_string()),
4399 required: Some(false),
4400 deprecated: Some(false),
4401 allow_empty_value: Some(false),
4402 style: None,
4403 explode: None,
4404 allow_reserved: Some(false),
4405 schema: Some(ObjectOrReference::Object(ObjectSchema {
4406 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4407 ..Default::default()
4408 })),
4409 example: None,
4410 examples: Default::default(),
4411 content: None,
4412 extensions: Default::default(),
4413 }),
4414 ],
4415 ..Default::default()
4416 };
4417
4418 let tool_metadata = ToolGenerator::generate_tool_metadata(
4419 &operation,
4420 "get".to_string(),
4421 "/users/{user(id)}".to_string(),
4422 &spec,
4423 false,
4424 false,
4425 )
4426 .unwrap();
4427
4428 let properties = tool_metadata
4430 .parameters
4431 .get("properties")
4432 .unwrap()
4433 .as_object()
4434 .unwrap();
4435
4436 assert!(properties.contains_key("user_id"));
4437 assert!(properties.contains_key("page_size"));
4438 assert!(properties.contains_key("header_auth-token"));
4439
4440 let required = tool_metadata
4442 .parameters
4443 .get("required")
4444 .unwrap()
4445 .as_array()
4446 .unwrap();
4447 assert!(required.contains(&json!("user_id")));
4448
4449 let arguments = json!({
4451 "user_id": "123",
4452 "page_size": 10,
4453 "header_auth-token": "secret"
4454 });
4455
4456 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
4457
4458 assert_eq!(extracted.path.get("user(id)"), Some(&json!("123")));
4460
4461 assert_eq!(
4463 extracted.query.get("page size").map(|q| &q.value),
4464 Some(&json!(10))
4465 );
4466
4467 assert_eq!(extracted.headers.get("auth-token!"), Some(&json!("secret")));
4469 }
4470
4471 #[test]
4472 fn test_check_unknown_parameters() {
4473 let mut properties = serde_json::Map::new();
4475 properties.insert("page_size".to_string(), json!({"type": "integer"}));
4476 properties.insert("user_id".to_string(), json!({"type": "string"}));
4477
4478 let mut args = serde_json::Map::new();
4479 args.insert("page_sixe".to_string(), json!(10)); let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4482 assert!(!result.is_empty());
4483 assert_eq!(result.len(), 1);
4484
4485 match &result[0] {
4486 ValidationError::InvalidParameter {
4487 parameter,
4488 suggestions,
4489 valid_parameters,
4490 } => {
4491 assert_eq!(parameter, "page_sixe");
4492 assert_eq!(suggestions, &vec!["page_size".to_string()]);
4493 assert_eq!(
4494 valid_parameters,
4495 &vec!["page_size".to_string(), "user_id".to_string()]
4496 );
4497 }
4498 _ => panic!("Expected InvalidParameter variant"),
4499 }
4500 }
4501
4502 #[test]
4503 fn test_check_unknown_parameters_no_suggestions() {
4504 let mut properties = serde_json::Map::new();
4506 properties.insert("limit".to_string(), json!({"type": "integer"}));
4507 properties.insert("offset".to_string(), json!({"type": "integer"}));
4508
4509 let mut args = serde_json::Map::new();
4510 args.insert("xyz123".to_string(), json!("value"));
4511
4512 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4513 assert!(!result.is_empty());
4514 assert_eq!(result.len(), 1);
4515
4516 match &result[0] {
4517 ValidationError::InvalidParameter {
4518 parameter,
4519 suggestions,
4520 valid_parameters,
4521 } => {
4522 assert_eq!(parameter, "xyz123");
4523 assert!(suggestions.is_empty());
4524 assert!(valid_parameters.contains(&"limit".to_string()));
4525 assert!(valid_parameters.contains(&"offset".to_string()));
4526 }
4527 _ => panic!("Expected InvalidParameter variant"),
4528 }
4529 }
4530
4531 #[test]
4532 fn test_check_unknown_parameters_multiple_suggestions() {
4533 let mut properties = serde_json::Map::new();
4535 properties.insert("user_id".to_string(), json!({"type": "string"}));
4536 properties.insert("user_iid".to_string(), json!({"type": "string"}));
4537 properties.insert("user_name".to_string(), json!({"type": "string"}));
4538
4539 let mut args = serde_json::Map::new();
4540 args.insert("usr_id".to_string(), json!("123"));
4541
4542 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4543 assert!(!result.is_empty());
4544 assert_eq!(result.len(), 1);
4545
4546 match &result[0] {
4547 ValidationError::InvalidParameter {
4548 parameter,
4549 suggestions,
4550 valid_parameters,
4551 } => {
4552 assert_eq!(parameter, "usr_id");
4553 assert!(!suggestions.is_empty());
4554 assert!(suggestions.contains(&"user_id".to_string()));
4555 assert_eq!(valid_parameters.len(), 3);
4556 }
4557 _ => panic!("Expected InvalidParameter variant"),
4558 }
4559 }
4560
4561 #[test]
4562 fn test_check_unknown_parameters_valid() {
4563 let mut properties = serde_json::Map::new();
4565 properties.insert("name".to_string(), json!({"type": "string"}));
4566 properties.insert("email".to_string(), json!({"type": "string"}));
4567
4568 let mut args = serde_json::Map::new();
4569 args.insert("name".to_string(), json!("John"));
4570 args.insert("email".to_string(), json!("john@example.com"));
4571
4572 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4573 assert!(result.is_empty());
4574 }
4575
4576 #[test]
4577 fn test_check_unknown_parameters_empty() {
4578 let properties = serde_json::Map::new();
4580
4581 let mut args = serde_json::Map::new();
4582 args.insert("any_param".to_string(), json!("value"));
4583
4584 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4585 assert!(!result.is_empty());
4586 assert_eq!(result.len(), 1);
4587
4588 match &result[0] {
4589 ValidationError::InvalidParameter {
4590 parameter,
4591 suggestions,
4592 valid_parameters,
4593 } => {
4594 assert_eq!(parameter, "any_param");
4595 assert!(suggestions.is_empty());
4596 assert!(valid_parameters.is_empty());
4597 }
4598 _ => panic!("Expected InvalidParameter variant"),
4599 }
4600 }
4601
4602 #[test]
4603 fn test_check_unknown_parameters_gltf_pagination() {
4604 let mut properties = serde_json::Map::new();
4606 properties.insert(
4607 "page_number".to_string(),
4608 json!({
4609 "type": "integer",
4610 "x-original-name": "page[number]"
4611 }),
4612 );
4613 properties.insert(
4614 "page_size".to_string(),
4615 json!({
4616 "type": "integer",
4617 "x-original-name": "page[size]"
4618 }),
4619 );
4620
4621 let mut args = serde_json::Map::new();
4623 args.insert("page".to_string(), json!(1));
4624 args.insert("per_page".to_string(), json!(10));
4625
4626 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4627 assert_eq!(result.len(), 2, "Should have 2 unknown parameters");
4628
4629 let page_error = result
4631 .iter()
4632 .find(|e| {
4633 if let ValidationError::InvalidParameter { parameter, .. } = e {
4634 parameter == "page"
4635 } else {
4636 false
4637 }
4638 })
4639 .expect("Should have error for 'page'");
4640
4641 let per_page_error = result
4642 .iter()
4643 .find(|e| {
4644 if let ValidationError::InvalidParameter { parameter, .. } = e {
4645 parameter == "per_page"
4646 } else {
4647 false
4648 }
4649 })
4650 .expect("Should have error for 'per_page'");
4651
4652 match page_error {
4654 ValidationError::InvalidParameter {
4655 suggestions,
4656 valid_parameters,
4657 ..
4658 } => {
4659 assert!(
4660 suggestions.contains(&"page_number".to_string()),
4661 "Should suggest 'page_number' for 'page'"
4662 );
4663 assert_eq!(valid_parameters.len(), 2);
4664 assert!(valid_parameters.contains(&"page_number".to_string()));
4665 assert!(valid_parameters.contains(&"page_size".to_string()));
4666 }
4667 _ => panic!("Expected InvalidParameter"),
4668 }
4669
4670 match per_page_error {
4672 ValidationError::InvalidParameter {
4673 parameter,
4674 suggestions,
4675 valid_parameters,
4676 ..
4677 } => {
4678 assert_eq!(parameter, "per_page");
4679 assert_eq!(valid_parameters.len(), 2);
4680 if !suggestions.is_empty() {
4683 assert!(suggestions.contains(&"page_size".to_string()));
4684 }
4685 }
4686 _ => panic!("Expected InvalidParameter"),
4687 }
4688 }
4689
4690 #[test]
4691 fn test_validate_parameters_with_invalid_params() {
4692 let tool_metadata = ToolMetadata {
4694 name: "listItems".to_string(),
4695 title: None,
4696 description: Some("List items".to_string()),
4697 parameters: json!({
4698 "type": "object",
4699 "properties": {
4700 "page_number": {
4701 "type": "integer",
4702 "x-original-name": "page[number]"
4703 },
4704 "page_size": {
4705 "type": "integer",
4706 "x-original-name": "page[size]"
4707 }
4708 },
4709 "required": []
4710 }),
4711 output_schema: None,
4712 method: "GET".to_string(),
4713 path: "/items".to_string(),
4714 security: None,
4715 parameter_mappings: std::collections::HashMap::new(),
4716 };
4717
4718 let arguments = json!({
4720 "page": 1,
4721 "per_page": 10
4722 });
4723
4724 let result = ToolGenerator::validate_parameters(&tool_metadata, &arguments);
4725 assert!(
4726 result.is_err(),
4727 "Should fail validation with unknown parameters"
4728 );
4729
4730 let error = result.unwrap_err();
4731 match error {
4732 ToolCallValidationError::InvalidParameters { violations } => {
4733 assert_eq!(violations.len(), 2, "Should have 2 validation errors");
4734
4735 let has_page_error = violations.iter().any(|v| {
4737 if let ValidationError::InvalidParameter { parameter, .. } = v {
4738 parameter == "page"
4739 } else {
4740 false
4741 }
4742 });
4743
4744 let has_per_page_error = violations.iter().any(|v| {
4745 if let ValidationError::InvalidParameter { parameter, .. } = v {
4746 parameter == "per_page"
4747 } else {
4748 false
4749 }
4750 });
4751
4752 assert!(has_page_error, "Should have error for 'page' parameter");
4753 assert!(
4754 has_per_page_error,
4755 "Should have error for 'per_page' parameter"
4756 );
4757 }
4758 _ => panic!("Expected InvalidParameters"),
4759 }
4760 }
4761
4762 #[test]
4763 fn test_cookie_parameter_sanitization() {
4764 let spec = create_test_spec();
4765
4766 let operation = Operation {
4767 operation_id: Some("testCookie".to_string()),
4768 parameters: vec![ObjectOrReference::Object(Parameter {
4769 name: "session[id]".to_string(),
4770 location: ParameterIn::Cookie,
4771 description: Some("Session ID".to_string()),
4772 required: Some(false),
4773 deprecated: Some(false),
4774 allow_empty_value: Some(false),
4775 style: None,
4776 explode: None,
4777 allow_reserved: Some(false),
4778 schema: Some(ObjectOrReference::Object(ObjectSchema {
4779 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4780 ..Default::default()
4781 })),
4782 example: None,
4783 examples: Default::default(),
4784 content: None,
4785 extensions: Default::default(),
4786 })],
4787 ..Default::default()
4788 };
4789
4790 let tool_metadata = ToolGenerator::generate_tool_metadata(
4791 &operation,
4792 "get".to_string(),
4793 "/data".to_string(),
4794 &spec,
4795 false,
4796 false,
4797 )
4798 .unwrap();
4799
4800 let properties = tool_metadata
4801 .parameters
4802 .get("properties")
4803 .unwrap()
4804 .as_object()
4805 .unwrap();
4806
4807 assert!(properties.contains_key("cookie_session_id"));
4809
4810 let arguments = json!({
4812 "cookie_session_id": "abc123"
4813 });
4814
4815 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
4816
4817 assert_eq!(extracted.cookies.get("session[id]"), Some(&json!("abc123")));
4819 }
4820
4821 #[test]
4822 fn test_parameter_description_with_examples() {
4823 let spec = create_test_spec();
4824
4825 let param_with_example = Parameter {
4827 name: "status".to_string(),
4828 location: ParameterIn::Query,
4829 description: Some("Filter by status".to_string()),
4830 required: Some(false),
4831 deprecated: Some(false),
4832 allow_empty_value: Some(false),
4833 style: None,
4834 explode: None,
4835 allow_reserved: Some(false),
4836 schema: Some(ObjectOrReference::Object(ObjectSchema {
4837 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4838 ..Default::default()
4839 })),
4840 example: Some(json!("active")),
4841 examples: Default::default(),
4842 content: None,
4843 extensions: Default::default(),
4844 };
4845
4846 let (schema, _) = ToolGenerator::convert_parameter_schema(
4847 ¶m_with_example,
4848 ParameterIn::Query,
4849 &spec,
4850 false,
4851 )
4852 .unwrap();
4853 let description = schema.get("description").unwrap().as_str().unwrap();
4854 assert_eq!(description, "Filter by status. Example: `\"active\"`");
4855
4856 let mut examples_map = std::collections::BTreeMap::new();
4858 examples_map.insert(
4859 "example1".to_string(),
4860 ObjectOrReference::Object(oas3::spec::Example {
4861 value: Some(json!("pending")),
4862 ..Default::default()
4863 }),
4864 );
4865 examples_map.insert(
4866 "example2".to_string(),
4867 ObjectOrReference::Object(oas3::spec::Example {
4868 value: Some(json!("completed")),
4869 ..Default::default()
4870 }),
4871 );
4872
4873 let param_with_examples = Parameter {
4874 name: "status".to_string(),
4875 location: ParameterIn::Query,
4876 description: Some("Filter by status".to_string()),
4877 required: Some(false),
4878 deprecated: Some(false),
4879 allow_empty_value: Some(false),
4880 style: None,
4881 explode: None,
4882 allow_reserved: Some(false),
4883 schema: Some(ObjectOrReference::Object(ObjectSchema {
4884 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4885 ..Default::default()
4886 })),
4887 example: None,
4888 examples: examples_map,
4889 content: None,
4890 extensions: Default::default(),
4891 };
4892
4893 let (schema, _) = ToolGenerator::convert_parameter_schema(
4894 ¶m_with_examples,
4895 ParameterIn::Query,
4896 &spec,
4897 false,
4898 )
4899 .unwrap();
4900 let description = schema.get("description").unwrap().as_str().unwrap();
4901 assert!(description.starts_with("Filter by status. Examples:\n"));
4902 assert!(description.contains("`\"pending\"`"));
4903 assert!(description.contains("`\"completed\"`"));
4904
4905 let param_no_desc = Parameter {
4907 name: "limit".to_string(),
4908 location: ParameterIn::Query,
4909 description: None,
4910 required: Some(false),
4911 deprecated: Some(false),
4912 allow_empty_value: Some(false),
4913 style: None,
4914 explode: None,
4915 allow_reserved: Some(false),
4916 schema: Some(ObjectOrReference::Object(ObjectSchema {
4917 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4918 ..Default::default()
4919 })),
4920 example: Some(json!(100)),
4921 examples: Default::default(),
4922 content: None,
4923 extensions: Default::default(),
4924 };
4925
4926 let (schema, _) = ToolGenerator::convert_parameter_schema(
4927 ¶m_no_desc,
4928 ParameterIn::Query,
4929 &spec,
4930 false,
4931 )
4932 .unwrap();
4933 let description = schema.get("description").unwrap().as_str().unwrap();
4934 assert_eq!(description, "limit parameter. Example: `100`");
4935 }
4936
4937 #[test]
4938 fn test_format_examples_for_description() {
4939 let examples = vec![json!("active")];
4941 let result = ToolGenerator::format_examples_for_description(&examples);
4942 assert_eq!(result, Some("Example: `\"active\"`".to_string()));
4943
4944 let examples = vec![json!(42)];
4946 let result = ToolGenerator::format_examples_for_description(&examples);
4947 assert_eq!(result, Some("Example: `42`".to_string()));
4948
4949 let examples = vec![json!(true)];
4951 let result = ToolGenerator::format_examples_for_description(&examples);
4952 assert_eq!(result, Some("Example: `true`".to_string()));
4953
4954 let examples = vec![json!("active"), json!("pending"), json!("completed")];
4956 let result = ToolGenerator::format_examples_for_description(&examples);
4957 assert_eq!(
4958 result,
4959 Some("Examples:\n- `\"active\"`\n- `\"pending\"`\n- `\"completed\"`".to_string())
4960 );
4961
4962 let examples = vec![json!(["a", "b", "c"])];
4964 let result = ToolGenerator::format_examples_for_description(&examples);
4965 assert_eq!(result, Some("Example: `[\"a\",\"b\",\"c\"]`".to_string()));
4966
4967 let examples = vec![json!({"key": "value"})];
4969 let result = ToolGenerator::format_examples_for_description(&examples);
4970 assert_eq!(result, Some("Example: `{\"key\":\"value\"}`".to_string()));
4971
4972 let examples = vec![];
4974 let result = ToolGenerator::format_examples_for_description(&examples);
4975 assert_eq!(result, None);
4976
4977 let examples = vec![json!(null)];
4979 let result = ToolGenerator::format_examples_for_description(&examples);
4980 assert_eq!(result, Some("Example: `null`".to_string()));
4981
4982 let examples = vec![json!("text"), json!(123), json!(true)];
4984 let result = ToolGenerator::format_examples_for_description(&examples);
4985 assert_eq!(
4986 result,
4987 Some("Examples:\n- `\"text\"`\n- `123`\n- `true`".to_string())
4988 );
4989
4990 let examples = vec![json!(["a", "b", "c", "d", "e", "f"])];
4992 let result = ToolGenerator::format_examples_for_description(&examples);
4993 assert_eq!(
4994 result,
4995 Some("Example: `[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\"]`".to_string())
4996 );
4997
4998 let examples = vec![json!([1, 2])];
5000 let result = ToolGenerator::format_examples_for_description(&examples);
5001 assert_eq!(result, Some("Example: `[1,2]`".to_string()));
5002
5003 let examples = vec![json!({"user": {"name": "John", "age": 30}})];
5005 let result = ToolGenerator::format_examples_for_description(&examples);
5006 assert_eq!(
5007 result,
5008 Some("Example: `{\"user\":{\"name\":\"John\",\"age\":30}}`".to_string())
5009 );
5010
5011 let examples = vec![json!("a"), json!("b"), json!("c"), json!("d"), json!("e")];
5013 let result = ToolGenerator::format_examples_for_description(&examples);
5014 assert_eq!(
5015 result,
5016 Some("Examples:\n- `\"a\"`\n- `\"b\"`\n- `\"c\"`\n- `\"d\"`\n- `\"e\"`".to_string())
5017 );
5018
5019 let examples = vec![json!(3.5)];
5021 let result = ToolGenerator::format_examples_for_description(&examples);
5022 assert_eq!(result, Some("Example: `3.5`".to_string()));
5023
5024 let examples = vec![json!(-42)];
5026 let result = ToolGenerator::format_examples_for_description(&examples);
5027 assert_eq!(result, Some("Example: `-42`".to_string()));
5028
5029 let examples = vec![json!(false)];
5031 let result = ToolGenerator::format_examples_for_description(&examples);
5032 assert_eq!(result, Some("Example: `false`".to_string()));
5033
5034 let examples = vec![json!("hello \"world\"")];
5036 let result = ToolGenerator::format_examples_for_description(&examples);
5037 assert_eq!(result, Some(r#"Example: `"hello \"world\""`"#.to_string()));
5039
5040 let examples = vec![json!("")];
5042 let result = ToolGenerator::format_examples_for_description(&examples);
5043 assert_eq!(result, Some("Example: `\"\"`".to_string()));
5044
5045 let examples = vec![json!([])];
5047 let result = ToolGenerator::format_examples_for_description(&examples);
5048 assert_eq!(result, Some("Example: `[]`".to_string()));
5049
5050 let examples = vec![json!({})];
5052 let result = ToolGenerator::format_examples_for_description(&examples);
5053 assert_eq!(result, Some("Example: `{}`".to_string()));
5054 }
5055
5056 #[test]
5057 fn test_reference_metadata_functionality() {
5058 let metadata = ReferenceMetadata::new(
5060 Some("User Reference".to_string()),
5061 Some("A reference to user data with additional context".to_string()),
5062 );
5063
5064 assert!(!metadata.is_empty());
5065 assert_eq!(metadata.summary(), Some("User Reference"));
5066 assert_eq!(
5067 metadata.best_description(),
5068 Some("A reference to user data with additional context")
5069 );
5070
5071 let summary_only = ReferenceMetadata::new(Some("Pet Summary".to_string()), None);
5073 assert_eq!(summary_only.best_description(), Some("Pet Summary"));
5074
5075 let empty_metadata = ReferenceMetadata::new(None, None);
5077 assert!(empty_metadata.is_empty());
5078 assert_eq!(empty_metadata.best_description(), None);
5079
5080 let metadata = ReferenceMetadata::new(
5082 Some("Reference Summary".to_string()),
5083 Some("Reference Description".to_string()),
5084 );
5085
5086 let result = metadata.merge_with_description(None, false);
5088 assert_eq!(result, Some("Reference Description".to_string()));
5089
5090 let result = metadata.merge_with_description(Some("Existing desc"), false);
5092 assert_eq!(result, Some("Reference Description".to_string()));
5093
5094 let result = metadata.merge_with_description(Some("Existing desc"), true);
5096 assert_eq!(result, Some("Reference Description".to_string()));
5097
5098 let result = metadata.enhance_parameter_description("userId", Some("User ID parameter"));
5100 assert_eq!(result, Some("userId: Reference Description".to_string()));
5101
5102 let result = metadata.enhance_parameter_description("userId", None);
5103 assert_eq!(result, Some("userId: Reference Description".to_string()));
5104
5105 let summary_only = ReferenceMetadata::new(Some("API Token".to_string()), None);
5107
5108 let result = summary_only.merge_with_description(Some("Generic token"), false);
5109 assert_eq!(result, Some("API Token".to_string()));
5110
5111 let result = summary_only.merge_with_description(Some("Different desc"), true);
5112 assert_eq!(result, Some("API Token".to_string())); let result = summary_only.enhance_parameter_description("token", Some("Token field"));
5115 assert_eq!(result, Some("token: API Token".to_string()));
5116
5117 let empty_meta = ReferenceMetadata::new(None, None);
5119
5120 let result = empty_meta.merge_with_description(Some("Schema description"), false);
5121 assert_eq!(result, Some("Schema description".to_string()));
5122
5123 let result = empty_meta.enhance_parameter_description("param", Some("Schema param"));
5124 assert_eq!(result, Some("Schema param".to_string()));
5125
5126 let result = empty_meta.enhance_parameter_description("param", None);
5127 assert_eq!(result, Some("param parameter".to_string()));
5128 }
5129
5130 #[test]
5131 fn test_parameter_schema_with_reference_metadata() {
5132 let mut spec = create_test_spec();
5133
5134 spec.components.as_mut().unwrap().schemas.insert(
5136 "Pet".to_string(),
5137 ObjectOrReference::Object(ObjectSchema {
5138 description: None, schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5140 ..Default::default()
5141 }),
5142 );
5143
5144 let param_with_ref = Parameter {
5146 name: "user".to_string(),
5147 location: ParameterIn::Query,
5148 description: None,
5149 required: Some(true),
5150 deprecated: Some(false),
5151 allow_empty_value: Some(false),
5152 style: None,
5153 explode: None,
5154 allow_reserved: Some(false),
5155 schema: Some(ObjectOrReference::Ref {
5156 ref_path: "#/components/schemas/Pet".to_string(),
5157 summary: Some("Pet Reference".to_string()),
5158 description: Some("A reference to pet schema with additional context".to_string()),
5159 }),
5160 example: None,
5161 examples: BTreeMap::new(),
5162 content: None,
5163 extensions: Default::default(),
5164 };
5165
5166 let result = ToolGenerator::convert_parameter_schema(
5168 ¶m_with_ref,
5169 ParameterIn::Query,
5170 &spec,
5171 false,
5172 );
5173
5174 assert!(result.is_ok());
5175 let (schema, _annotations) = result.unwrap();
5176
5177 let description = schema.get("description").and_then(|v| v.as_str());
5179 assert!(description.is_some());
5180 assert!(
5182 description.unwrap().contains("Pet Reference")
5183 || description
5184 .unwrap()
5185 .contains("A reference to pet schema with additional context")
5186 );
5187 }
5188
5189 #[test]
5190 fn test_request_body_with_reference_metadata() {
5191 let spec = create_test_spec();
5192
5193 let request_body_ref = ObjectOrReference::Ref {
5195 ref_path: "#/components/requestBodies/PetBody".to_string(),
5196 summary: Some("Pet Request Body".to_string()),
5197 description: Some(
5198 "Request body containing pet information for API operations".to_string(),
5199 ),
5200 };
5201
5202 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body_ref, &spec);
5203
5204 assert!(result.is_ok());
5205 let schema_result = result.unwrap();
5206 assert!(schema_result.is_some());
5207
5208 let (schema, _annotations, _required) = schema_result.unwrap();
5209 let description = schema.get("description").and_then(|v| v.as_str());
5210
5211 assert!(description.is_some());
5212 assert_eq!(
5214 description.unwrap(),
5215 "Request body containing pet information for API operations"
5216 );
5217 }
5218
5219 #[test]
5220 fn test_response_schema_with_reference_metadata() {
5221 let spec = create_test_spec();
5222
5223 let mut responses = BTreeMap::new();
5225 responses.insert(
5226 "200".to_string(),
5227 ObjectOrReference::Ref {
5228 ref_path: "#/components/responses/PetResponse".to_string(),
5229 summary: Some("Successful Pet Response".to_string()),
5230 description: Some(
5231 "Response containing pet data on successful operation".to_string(),
5232 ),
5233 },
5234 );
5235 let responses_option = Some(responses);
5236
5237 let result = ToolGenerator::extract_output_schema(&responses_option, &spec);
5238
5239 assert!(result.is_ok());
5240 let schema = result.unwrap();
5241 assert!(schema.is_some());
5242
5243 let schema_value = schema.unwrap();
5244 let body_desc = schema_value
5245 .get("properties")
5246 .and_then(|props| props.get("body"))
5247 .and_then(|body| body.get("description"))
5248 .and_then(|desc| desc.as_str());
5249
5250 assert!(body_desc.is_some());
5251 assert_eq!(
5253 body_desc.unwrap(),
5254 "Response containing pet data on successful operation"
5255 );
5256 }
5257
5258 #[test]
5259 fn test_self_referencing_schema_does_not_overflow() {
5260 let mut spec = create_test_spec();
5263
5264 let node_schema = ObjectSchema {
5266 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
5267 properties: {
5268 let mut props = BTreeMap::new();
5269 props.insert(
5270 "name".to_string(),
5271 ObjectOrReference::Object(ObjectSchema {
5272 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5273 ..Default::default()
5274 }),
5275 );
5276 props.insert(
5278 "children".to_string(),
5279 ObjectOrReference::Object(ObjectSchema {
5280 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
5281 items: Some(Box::new(Schema::Object(Box::new(ObjectOrReference::Ref {
5282 ref_path: "#/components/schemas/Node".to_string(),
5283 summary: None,
5284 description: None,
5285 })))),
5286 ..Default::default()
5287 }),
5288 );
5289 props
5290 },
5291 ..Default::default()
5292 };
5293
5294 if let Some(ref mut components) = spec.components {
5296 components
5297 .schemas
5298 .insert("Node".to_string(), ObjectOrReference::Object(node_schema));
5299 }
5300
5301 let mut visited = HashSet::new();
5303 let result = ToolGenerator::convert_schema_to_json_schema(
5304 &Schema::Object(Box::new(ObjectOrReference::Ref {
5305 ref_path: "#/components/schemas/Node".to_string(),
5306 summary: None,
5307 description: None,
5308 })),
5309 &spec,
5310 &mut visited,
5311 );
5312
5313 assert!(
5315 result.is_err(),
5316 "Expected circular reference error, got: {result:?}"
5317 );
5318 let error = result.unwrap_err();
5319 assert!(
5320 error.to_string().contains("Circular reference"),
5321 "Expected circular reference error message, got: {error}"
5322 );
5323 }
5324}