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 Self::convert_object_schema_to_json_schema(&resolved, spec, visited)
975 }
976 },
977 Schema::Boolean(bool_schema) => {
978 if bool_schema.0 {
980 Ok(json!({})) } else {
982 Ok(json!({"not": {}})) }
984 }
985 }
986 }
987
988 fn convert_object_schema_to_json_schema(
998 obj_schema: &ObjectSchema,
999 spec: &Spec,
1000 visited: &mut HashSet<String>,
1001 ) -> Result<Value, Error> {
1002 let mut schema_obj = serde_json::Map::new();
1003
1004 if let Some(schema_type) = &obj_schema.schema_type {
1006 match schema_type {
1007 SchemaTypeSet::Single(single_type) => {
1008 schema_obj.insert(
1009 "type".to_string(),
1010 json!(Self::schema_type_to_string(single_type)),
1011 );
1012 }
1013 SchemaTypeSet::Multiple(type_set) => {
1014 let types: Vec<String> =
1015 type_set.iter().map(Self::schema_type_to_string).collect();
1016 schema_obj.insert("type".to_string(), json!(types));
1017 }
1018 }
1019 }
1020
1021 if let Some(desc) = &obj_schema.description {
1023 schema_obj.insert("description".to_string(), json!(desc));
1024 }
1025
1026 if !obj_schema.one_of.is_empty() {
1028 let mut one_of_schemas = Vec::new();
1029 for schema_ref in &obj_schema.one_of {
1030 let schema_json = match schema_ref {
1031 ObjectOrReference::Object(schema) => {
1032 Self::convert_object_schema_to_json_schema(schema, spec, visited)?
1033 }
1034 ObjectOrReference::Ref { ref_path, .. } => {
1035 let resolved = Self::resolve_reference(ref_path, spec, visited)?;
1036 Self::convert_object_schema_to_json_schema(&resolved, spec, visited)?
1037 }
1038 };
1039 one_of_schemas.push(schema_json);
1040 }
1041 schema_obj.insert("oneOf".to_string(), json!(one_of_schemas));
1042 return Ok(Value::Object(schema_obj));
1045 }
1046
1047 if !obj_schema.properties.is_empty() {
1049 let properties = &obj_schema.properties;
1050 let mut props_map = serde_json::Map::new();
1051 for (prop_name, prop_schema_or_ref) in properties {
1052 let prop_schema = match prop_schema_or_ref {
1053 ObjectOrReference::Object(schema) => {
1054 Self::convert_schema_to_json_schema(
1056 &Schema::Object(Box::new(ObjectOrReference::Object(schema.clone()))),
1057 spec,
1058 visited,
1059 )?
1060 }
1061 ObjectOrReference::Ref { ref_path, .. } => {
1062 let resolved = Self::resolve_reference(ref_path, spec, visited)?;
1063 Self::convert_object_schema_to_json_schema(&resolved, spec, visited)?
1064 }
1065 };
1066
1067 let sanitized_name = sanitize_property_name(prop_name);
1069 props_map.insert(sanitized_name, prop_schema);
1070 }
1071 schema_obj.insert("properties".to_string(), Value::Object(props_map));
1072 }
1073
1074 if !obj_schema.required.is_empty() {
1076 schema_obj.insert("required".to_string(), json!(&obj_schema.required));
1077 }
1078
1079 if let Some(schema_type) = &obj_schema.schema_type
1081 && matches!(schema_type, SchemaTypeSet::Single(SchemaType::Object))
1082 {
1083 match &obj_schema.additional_properties {
1085 None => {
1086 schema_obj.insert("additionalProperties".to_string(), json!(true));
1088 }
1089 Some(Schema::Boolean(BooleanSchema(value))) => {
1090 schema_obj.insert("additionalProperties".to_string(), json!(value));
1092 }
1093 Some(Schema::Object(schema_ref)) => {
1094 let mut visited = HashSet::new();
1096 let additional_props_schema = Self::convert_schema_to_json_schema(
1097 &Schema::Object(schema_ref.clone()),
1098 spec,
1099 &mut visited,
1100 )?;
1101 schema_obj.insert("additionalProperties".to_string(), additional_props_schema);
1102 }
1103 }
1104 }
1105
1106 if let Some(schema_type) = &obj_schema.schema_type {
1108 if matches!(schema_type, SchemaTypeSet::Single(SchemaType::Array)) {
1109 if !obj_schema.prefix_items.is_empty() {
1111 Self::convert_prefix_items_to_draft07(
1113 &obj_schema.prefix_items,
1114 &obj_schema.items,
1115 &mut schema_obj,
1116 spec,
1117 )?;
1118 } else if let Some(items_schema) = &obj_schema.items {
1119 let items_json =
1121 Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
1122 schema_obj.insert("items".to_string(), items_json);
1123 }
1124
1125 if let Some(min_items) = obj_schema.min_items {
1127 schema_obj.insert("minItems".to_string(), json!(min_items));
1128 }
1129 if let Some(max_items) = obj_schema.max_items {
1130 schema_obj.insert("maxItems".to_string(), json!(max_items));
1131 }
1132 } else if let Some(items_schema) = &obj_schema.items {
1133 let items_json = Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
1135 schema_obj.insert("items".to_string(), items_json);
1136 }
1137 }
1138
1139 if let Some(format) = &obj_schema.format {
1141 schema_obj.insert("format".to_string(), json!(format));
1142 }
1143
1144 if let Some(example) = &obj_schema.example {
1145 schema_obj.insert("example".to_string(), example.clone());
1146 }
1147
1148 if let Some(default) = &obj_schema.default {
1149 schema_obj.insert("default".to_string(), default.clone());
1150 }
1151
1152 if !obj_schema.enum_values.is_empty() {
1153 schema_obj.insert("enum".to_string(), json!(&obj_schema.enum_values));
1154 }
1155
1156 if let Some(min) = &obj_schema.minimum {
1157 schema_obj.insert("minimum".to_string(), json!(min));
1158 }
1159
1160 if let Some(max) = &obj_schema.maximum {
1161 schema_obj.insert("maximum".to_string(), json!(max));
1162 }
1163
1164 if let Some(min_length) = &obj_schema.min_length {
1165 schema_obj.insert("minLength".to_string(), json!(min_length));
1166 }
1167
1168 if let Some(max_length) = &obj_schema.max_length {
1169 schema_obj.insert("maxLength".to_string(), json!(max_length));
1170 }
1171
1172 if let Some(pattern) = &obj_schema.pattern {
1173 schema_obj.insert("pattern".to_string(), json!(pattern));
1174 }
1175
1176 Ok(Value::Object(schema_obj))
1177 }
1178
1179 fn schema_type_to_string(schema_type: &SchemaType) -> String {
1181 match schema_type {
1182 SchemaType::Boolean => "boolean",
1183 SchemaType::Integer => "integer",
1184 SchemaType::Number => "number",
1185 SchemaType::String => "string",
1186 SchemaType::Array => "array",
1187 SchemaType::Object => "object",
1188 SchemaType::Null => "null",
1189 }
1190 .to_string()
1191 }
1192
1193 fn resolve_reference(
1203 ref_path: &str,
1204 spec: &Spec,
1205 visited: &mut HashSet<String>,
1206 ) -> Result<ObjectSchema, Error> {
1207 if visited.contains(ref_path) {
1209 return Err(Error::ToolGeneration(format!(
1210 "Circular reference detected: {ref_path}"
1211 )));
1212 }
1213
1214 visited.insert(ref_path.to_string());
1216
1217 if !ref_path.starts_with("#/components/schemas/") {
1220 return Err(Error::ToolGeneration(format!(
1221 "Unsupported reference format: {ref_path}. Only #/components/schemas/ references are supported"
1222 )));
1223 }
1224
1225 let schema_name = ref_path.strip_prefix("#/components/schemas/").unwrap();
1226
1227 let components = spec.components.as_ref().ok_or_else(|| {
1229 Error::ToolGeneration(format!(
1230 "Reference {ref_path} points to components, but spec has no components section"
1231 ))
1232 })?;
1233
1234 let schema_ref = components.schemas.get(schema_name).ok_or_else(|| {
1235 Error::ToolGeneration(format!(
1236 "Schema '{schema_name}' not found in components/schemas"
1237 ))
1238 })?;
1239
1240 let resolved_schema = match schema_ref {
1242 ObjectOrReference::Object(obj_schema) => obj_schema.clone(),
1243 ObjectOrReference::Ref {
1244 ref_path: nested_ref,
1245 ..
1246 } => {
1247 Self::resolve_reference(nested_ref, spec, visited)?
1249 }
1250 };
1251
1252 visited.remove(ref_path);
1254
1255 Ok(resolved_schema)
1256 }
1257
1258 fn resolve_reference_with_metadata(
1263 ref_path: &str,
1264 summary: Option<String>,
1265 description: Option<String>,
1266 spec: &Spec,
1267 visited: &mut HashSet<String>,
1268 ) -> Result<(ObjectSchema, ReferenceMetadata), Error> {
1269 let resolved_schema = Self::resolve_reference(ref_path, spec, visited)?;
1270 let metadata = ReferenceMetadata::new(summary, description);
1271 Ok((resolved_schema, metadata))
1272 }
1273
1274 fn generate_parameter_schema(
1276 parameters: &[ObjectOrReference<Parameter>],
1277 _method: &str,
1278 request_body: &Option<ObjectOrReference<RequestBody>>,
1279 spec: &Spec,
1280 skip_parameter_descriptions: bool,
1281 ) -> Result<
1282 (
1283 Value,
1284 std::collections::HashMap<String, crate::tool::ParameterMapping>,
1285 ),
1286 Error,
1287 > {
1288 let mut properties = serde_json::Map::new();
1289 let mut required = Vec::new();
1290 let mut parameter_mappings = std::collections::HashMap::new();
1291
1292 let mut path_params = Vec::new();
1294 let mut query_params = Vec::new();
1295 let mut header_params = Vec::new();
1296 let mut cookie_params = Vec::new();
1297
1298 for param_ref in parameters {
1299 let param = match param_ref {
1300 ObjectOrReference::Object(param) => param,
1301 ObjectOrReference::Ref { ref_path, .. } => {
1302 warn!(
1306 reference_path = %ref_path,
1307 "Parameter reference not resolved"
1308 );
1309 continue;
1310 }
1311 };
1312
1313 match ¶m.location {
1314 ParameterIn::Query => query_params.push(param),
1315 ParameterIn::Header => header_params.push(param),
1316 ParameterIn::Path => path_params.push(param),
1317 ParameterIn::Cookie => cookie_params.push(param),
1318 }
1319 }
1320
1321 for param in path_params {
1323 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1324 param,
1325 ParameterIn::Path,
1326 spec,
1327 skip_parameter_descriptions,
1328 )?;
1329
1330 let sanitized_name = sanitize_property_name(¶m.name);
1332 if sanitized_name != param.name {
1333 annotations = annotations.with_original_name(param.name.clone());
1334 }
1335
1336 let explode = annotations
1338 .annotations
1339 .iter()
1340 .find_map(|a| {
1341 if let Annotation::Explode(e) = a {
1342 Some(*e)
1343 } else {
1344 None
1345 }
1346 })
1347 .unwrap_or(true);
1348
1349 parameter_mappings.insert(
1351 sanitized_name.clone(),
1352 crate::tool::ParameterMapping {
1353 sanitized_name: sanitized_name.clone(),
1354 original_name: param.name.clone(),
1355 location: "path".to_string(),
1356 explode,
1357 },
1358 );
1359
1360 properties.insert(sanitized_name.clone(), param_schema);
1362 required.push(sanitized_name);
1363 }
1364
1365 for param in &query_params {
1367 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1368 param,
1369 ParameterIn::Query,
1370 spec,
1371 skip_parameter_descriptions,
1372 )?;
1373
1374 let sanitized_name = sanitize_property_name(¶m.name);
1376 if sanitized_name != param.name {
1377 annotations = annotations.with_original_name(param.name.clone());
1378 }
1379
1380 let explode = annotations
1382 .annotations
1383 .iter()
1384 .find_map(|a| {
1385 if let Annotation::Explode(e) = a {
1386 Some(*e)
1387 } else {
1388 None
1389 }
1390 })
1391 .unwrap_or(true);
1392
1393 parameter_mappings.insert(
1395 sanitized_name.clone(),
1396 crate::tool::ParameterMapping {
1397 sanitized_name: sanitized_name.clone(),
1398 original_name: param.name.clone(),
1399 location: "query".to_string(),
1400 explode,
1401 },
1402 );
1403
1404 properties.insert(sanitized_name.clone(), param_schema);
1406 if param.required.unwrap_or(false) {
1407 required.push(sanitized_name);
1408 }
1409 }
1410
1411 for param in &header_params {
1413 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1414 param,
1415 ParameterIn::Header,
1416 spec,
1417 skip_parameter_descriptions,
1418 )?;
1419
1420 let prefixed_name = format!("header_{}", param.name);
1422 let sanitized_name = sanitize_property_name(&prefixed_name);
1423 if sanitized_name != prefixed_name {
1424 annotations = annotations.with_original_name(param.name.clone());
1425 }
1426
1427 let explode = annotations
1429 .annotations
1430 .iter()
1431 .find_map(|a| {
1432 if let Annotation::Explode(e) = a {
1433 Some(*e)
1434 } else {
1435 None
1436 }
1437 })
1438 .unwrap_or(true);
1439
1440 parameter_mappings.insert(
1442 sanitized_name.clone(),
1443 crate::tool::ParameterMapping {
1444 sanitized_name: sanitized_name.clone(),
1445 original_name: param.name.clone(),
1446 location: "header".to_string(),
1447 explode,
1448 },
1449 );
1450
1451 properties.insert(sanitized_name.clone(), param_schema);
1453 if param.required.unwrap_or(false) {
1454 required.push(sanitized_name);
1455 }
1456 }
1457
1458 for param in &cookie_params {
1460 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1461 param,
1462 ParameterIn::Cookie,
1463 spec,
1464 skip_parameter_descriptions,
1465 )?;
1466
1467 let prefixed_name = format!("cookie_{}", param.name);
1469 let sanitized_name = sanitize_property_name(&prefixed_name);
1470 if sanitized_name != prefixed_name {
1471 annotations = annotations.with_original_name(param.name.clone());
1472 }
1473
1474 let explode = annotations
1476 .annotations
1477 .iter()
1478 .find_map(|a| {
1479 if let Annotation::Explode(e) = a {
1480 Some(*e)
1481 } else {
1482 None
1483 }
1484 })
1485 .unwrap_or(true);
1486
1487 parameter_mappings.insert(
1489 sanitized_name.clone(),
1490 crate::tool::ParameterMapping {
1491 sanitized_name: sanitized_name.clone(),
1492 original_name: param.name.clone(),
1493 location: "cookie".to_string(),
1494 explode,
1495 },
1496 );
1497
1498 properties.insert(sanitized_name.clone(), param_schema);
1500 if param.required.unwrap_or(false) {
1501 required.push(sanitized_name);
1502 }
1503 }
1504
1505 if let Some(request_body) = request_body
1507 && let Some((body_schema, _annotations, is_required)) =
1508 Self::convert_request_body_to_json_schema(request_body, spec)?
1509 {
1510 parameter_mappings.insert(
1512 "request_body".to_string(),
1513 crate::tool::ParameterMapping {
1514 sanitized_name: "request_body".to_string(),
1515 original_name: "request_body".to_string(),
1516 location: "body".to_string(),
1517 explode: false,
1518 },
1519 );
1520
1521 properties.insert("request_body".to_string(), body_schema);
1523 if is_required {
1524 required.push("request_body".to_string());
1525 }
1526 }
1527
1528 if !query_params.is_empty() || !header_params.is_empty() || !cookie_params.is_empty() {
1530 properties.insert(
1532 "timeout_seconds".to_string(),
1533 json!({
1534 "type": "integer",
1535 "description": "Request timeout in seconds",
1536 "minimum": 1,
1537 "maximum": 300,
1538 "default": 30
1539 }),
1540 );
1541 }
1542
1543 let schema = json!({
1544 "type": "object",
1545 "properties": properties,
1546 "required": required,
1547 "additionalProperties": false
1548 });
1549
1550 Ok((schema, parameter_mappings))
1551 }
1552
1553 fn convert_parameter_schema(
1555 param: &Parameter,
1556 location: ParameterIn,
1557 spec: &Spec,
1558 skip_parameter_descriptions: bool,
1559 ) -> Result<(Value, Annotations), Error> {
1560 let base_schema = if let Some(schema_ref) = ¶m.schema {
1562 match schema_ref {
1563 ObjectOrReference::Object(obj_schema) => {
1564 let mut visited = HashSet::new();
1565 Self::convert_schema_to_json_schema(
1566 &Schema::Object(Box::new(ObjectOrReference::Object(obj_schema.clone()))),
1567 spec,
1568 &mut visited,
1569 )?
1570 }
1571 ObjectOrReference::Ref {
1572 ref_path,
1573 summary,
1574 description,
1575 } => {
1576 let mut visited = HashSet::new();
1578 match Self::resolve_reference_with_metadata(
1579 ref_path,
1580 summary.clone(),
1581 description.clone(),
1582 spec,
1583 &mut visited,
1584 ) {
1585 Ok((resolved_schema, ref_metadata)) => {
1586 let mut schema_json = Self::convert_schema_to_json_schema(
1587 &Schema::Object(Box::new(ObjectOrReference::Object(
1588 resolved_schema,
1589 ))),
1590 spec,
1591 &mut visited,
1592 )?;
1593
1594 if let Value::Object(ref mut schema_obj) = schema_json {
1596 if let Some(ref_desc) = ref_metadata.best_description() {
1598 schema_obj.insert("description".to_string(), json!(ref_desc));
1599 }
1600 }
1603
1604 schema_json
1605 }
1606 Err(_) => {
1607 json!({"type": "string"})
1609 }
1610 }
1611 }
1612 }
1613 } else {
1614 json!({"type": "string"})
1616 };
1617
1618 let mut result = match base_schema {
1620 Value::Object(obj) => obj,
1621 _ => {
1622 return Err(Error::ToolGeneration(format!(
1624 "Internal error: schema converter returned non-object for parameter '{}'",
1625 param.name
1626 )));
1627 }
1628 };
1629
1630 let mut collected_examples = Vec::new();
1632
1633 if let Some(example) = ¶m.example {
1635 collected_examples.push(example.clone());
1636 } else if !param.examples.is_empty() {
1637 for example_ref in param.examples.values() {
1639 match example_ref {
1640 ObjectOrReference::Object(example_obj) => {
1641 if let Some(value) = &example_obj.value {
1642 collected_examples.push(value.clone());
1643 }
1644 }
1645 ObjectOrReference::Ref { .. } => {
1646 }
1648 }
1649 }
1650 } else if let Some(Value::String(ex_str)) = result.get("example") {
1651 collected_examples.push(json!(ex_str));
1653 } else if let Some(ex) = result.get("example") {
1654 collected_examples.push(ex.clone());
1655 }
1656
1657 let base_description = param
1659 .description
1660 .as_ref()
1661 .map(|d| d.to_string())
1662 .or_else(|| {
1663 result
1664 .get("description")
1665 .and_then(|d| d.as_str())
1666 .map(|d| d.to_string())
1667 })
1668 .unwrap_or_else(|| format!("{} parameter", param.name));
1669
1670 let description_with_examples = if let Some(examples_str) =
1671 Self::format_examples_for_description(&collected_examples)
1672 {
1673 format!("{base_description}. {examples_str}")
1674 } else {
1675 base_description
1676 };
1677
1678 if !skip_parameter_descriptions {
1679 result.insert("description".to_string(), json!(description_with_examples));
1680 }
1681
1682 if let Some(example) = ¶m.example {
1687 result.insert("example".to_string(), example.clone());
1688 } else if !param.examples.is_empty() {
1689 let mut examples_array = Vec::new();
1692 for (example_name, example_ref) in ¶m.examples {
1693 match example_ref {
1694 ObjectOrReference::Object(example_obj) => {
1695 if let Some(value) = &example_obj.value {
1696 examples_array.push(json!({
1697 "name": example_name,
1698 "value": value
1699 }));
1700 }
1701 }
1702 ObjectOrReference::Ref { .. } => {
1703 }
1706 }
1707 }
1708
1709 if !examples_array.is_empty() {
1710 if let Some(first_example) = examples_array.first()
1712 && let Some(value) = first_example.get("value")
1713 {
1714 result.insert("example".to_string(), value.clone());
1715 }
1716 result.insert("x-examples".to_string(), json!(examples_array));
1718 }
1719 }
1720
1721 let mut annotations = Annotations::new()
1723 .with_location(Location::Parameter(location))
1724 .with_required(param.required.unwrap_or(false));
1725
1726 if let Some(explode) = param.explode {
1728 annotations = annotations.with_explode(explode);
1729 } else {
1730 let default_explode = match ¶m.style {
1734 Some(ParameterStyle::Form) | None => true, _ => false,
1736 };
1737 annotations = annotations.with_explode(default_explode);
1738 }
1739
1740 Ok((Value::Object(result), annotations))
1741 }
1742
1743 fn format_examples_for_description(examples: &[Value]) -> Option<String> {
1745 if examples.is_empty() {
1746 return None;
1747 }
1748
1749 if examples.len() == 1 {
1750 let example_str =
1751 serde_json::to_string(&examples[0]).unwrap_or_else(|_| "null".to_string());
1752 Some(format!("Example: `{example_str}`"))
1753 } else {
1754 let mut result = String::from("Examples:\n");
1755 for ex in examples {
1756 let json_str = serde_json::to_string(ex).unwrap_or_else(|_| "null".to_string());
1757 result.push_str(&format!("- `{json_str}`\n"));
1758 }
1759 result.pop();
1761 Some(result)
1762 }
1763 }
1764
1765 fn convert_prefix_items_to_draft07(
1776 prefix_items: &[ObjectOrReference<ObjectSchema>],
1777 items: &Option<Box<Schema>>,
1778 result: &mut serde_json::Map<String, Value>,
1779 spec: &Spec,
1780 ) -> Result<(), Error> {
1781 let prefix_count = prefix_items.len();
1782
1783 let mut item_types = Vec::new();
1785 for prefix_item in prefix_items {
1786 match prefix_item {
1787 ObjectOrReference::Object(obj_schema) => {
1788 if let Some(schema_type) = &obj_schema.schema_type {
1789 match schema_type {
1790 SchemaTypeSet::Single(SchemaType::String) => item_types.push("string"),
1791 SchemaTypeSet::Single(SchemaType::Integer) => {
1792 item_types.push("integer")
1793 }
1794 SchemaTypeSet::Single(SchemaType::Number) => item_types.push("number"),
1795 SchemaTypeSet::Single(SchemaType::Boolean) => {
1796 item_types.push("boolean")
1797 }
1798 SchemaTypeSet::Single(SchemaType::Array) => item_types.push("array"),
1799 SchemaTypeSet::Single(SchemaType::Object) => item_types.push("object"),
1800 _ => item_types.push("string"), }
1802 } else {
1803 item_types.push("string"); }
1805 }
1806 ObjectOrReference::Ref { ref_path, .. } => {
1807 let mut visited = HashSet::new();
1809 match Self::resolve_reference(ref_path, spec, &mut visited) {
1810 Ok(resolved_schema) => {
1811 if let Some(schema_type_set) = &resolved_schema.schema_type {
1813 match schema_type_set {
1814 SchemaTypeSet::Single(SchemaType::String) => {
1815 item_types.push("string")
1816 }
1817 SchemaTypeSet::Single(SchemaType::Integer) => {
1818 item_types.push("integer")
1819 }
1820 SchemaTypeSet::Single(SchemaType::Number) => {
1821 item_types.push("number")
1822 }
1823 SchemaTypeSet::Single(SchemaType::Boolean) => {
1824 item_types.push("boolean")
1825 }
1826 SchemaTypeSet::Single(SchemaType::Array) => {
1827 item_types.push("array")
1828 }
1829 SchemaTypeSet::Single(SchemaType::Object) => {
1830 item_types.push("object")
1831 }
1832 _ => item_types.push("string"), }
1834 } else {
1835 item_types.push("string"); }
1837 }
1838 Err(_) => {
1839 item_types.push("string");
1841 }
1842 }
1843 }
1844 }
1845 }
1846
1847 let items_is_false =
1849 matches!(items.as_ref().map(|i| i.as_ref()), Some(Schema::Boolean(b)) if !b.0);
1850
1851 if items_is_false {
1852 result.insert("minItems".to_string(), json!(prefix_count));
1854 result.insert("maxItems".to_string(), json!(prefix_count));
1855 }
1856
1857 let unique_types: std::collections::BTreeSet<_> = item_types.into_iter().collect();
1859
1860 if unique_types.len() == 1 {
1861 let item_type = unique_types.into_iter().next().unwrap();
1863 result.insert("items".to_string(), json!({"type": item_type}));
1864 } else if unique_types.len() > 1 {
1865 let one_of: Vec<Value> = unique_types
1867 .into_iter()
1868 .map(|t| json!({"type": t}))
1869 .collect();
1870 result.insert("items".to_string(), json!({"oneOf": one_of}));
1871 }
1872
1873 Ok(())
1874 }
1875
1876 fn convert_request_body_to_json_schema(
1888 request_body_ref: &ObjectOrReference<RequestBody>,
1889 spec: &Spec,
1890 ) -> Result<Option<(Value, Annotations, bool)>, Error> {
1891 match request_body_ref {
1892 ObjectOrReference::Object(request_body) => {
1893 let schema_info = request_body
1896 .content
1897 .get(mime::APPLICATION_JSON.as_ref())
1898 .or_else(|| request_body.content.get("application/json"))
1899 .or_else(|| {
1900 request_body.content.values().next()
1902 });
1903
1904 if let Some(media_type) = schema_info {
1905 if let Some(schema_ref) = &media_type.schema {
1906 let schema = Schema::Object(Box::new(schema_ref.clone()));
1908
1909 let mut visited = HashSet::new();
1911 let converted_schema =
1912 Self::convert_schema_to_json_schema(&schema, spec, &mut visited)?;
1913
1914 let mut schema_obj = match converted_schema {
1916 Value::Object(obj) => obj,
1917 _ => {
1918 let mut obj = serde_json::Map::new();
1920 obj.insert("type".to_string(), json!("object"));
1921 obj.insert("additionalProperties".to_string(), json!(true));
1922 obj
1923 }
1924 };
1925
1926 if !schema_obj.contains_key("description") {
1928 let description = request_body
1929 .description
1930 .clone()
1931 .unwrap_or_else(|| "Request body data".to_string());
1932 schema_obj.insert("description".to_string(), json!(description));
1933 }
1934
1935 let annotations = Annotations::new()
1937 .with_location(Location::Body)
1938 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
1939
1940 let required = request_body.required.unwrap_or(false);
1941 Ok(Some((Value::Object(schema_obj), annotations, required)))
1942 } else {
1943 Ok(None)
1944 }
1945 } else {
1946 Ok(None)
1947 }
1948 }
1949 ObjectOrReference::Ref {
1950 ref_path: _,
1951 summary,
1952 description,
1953 } => {
1954 let ref_metadata = ReferenceMetadata::new(summary.clone(), description.clone());
1956 let enhanced_description = ref_metadata
1957 .best_description()
1958 .map(|desc| desc.to_string())
1959 .unwrap_or_else(|| "Request body data".to_string());
1960
1961 let mut result = serde_json::Map::new();
1962 result.insert("type".to_string(), json!("object"));
1963 result.insert("additionalProperties".to_string(), json!(true));
1964 result.insert("description".to_string(), json!(enhanced_description));
1965
1966 let annotations = Annotations::new()
1968 .with_location(Location::Body)
1969 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
1970
1971 Ok(Some((Value::Object(result), annotations, false)))
1972 }
1973 }
1974 }
1975
1976 pub fn extract_parameters(
1982 tool_metadata: &ToolMetadata,
1983 arguments: &Value,
1984 ) -> Result<ExtractedParameters, ToolCallValidationError> {
1985 let args = arguments.as_object().ok_or_else(|| {
1986 ToolCallValidationError::RequestConstructionError {
1987 reason: "Arguments must be an object".to_string(),
1988 }
1989 })?;
1990
1991 trace!(
1992 tool_name = %tool_metadata.name,
1993 raw_arguments = ?arguments,
1994 "Starting parameter extraction"
1995 );
1996
1997 let mut path_params = HashMap::new();
1998 let mut query_params = HashMap::new();
1999 let mut header_params = HashMap::new();
2000 let mut cookie_params = HashMap::new();
2001 let mut body_params = HashMap::new();
2002 let mut config = RequestConfig::default();
2003
2004 if let Some(timeout) = args.get("timeout_seconds").and_then(Value::as_u64) {
2006 config.timeout_seconds = u32::try_from(timeout).unwrap_or(u32::MAX);
2007 }
2008
2009 for (key, value) in args {
2011 if key == "timeout_seconds" {
2012 continue; }
2014
2015 if key == "request_body" {
2017 body_params.insert("request_body".to_string(), value.clone());
2018 continue;
2019 }
2020
2021 let mapping = tool_metadata.parameter_mappings.get(key);
2023
2024 if let Some(mapping) = mapping {
2025 match mapping.location.as_str() {
2027 "path" => {
2028 path_params.insert(mapping.original_name.clone(), value.clone());
2029 }
2030 "query" => {
2031 query_params.insert(
2032 mapping.original_name.clone(),
2033 QueryParameter::new(value.clone(), mapping.explode),
2034 );
2035 }
2036 "header" => {
2037 header_params.insert(mapping.original_name.clone(), value.clone());
2038 }
2039 "cookie" => {
2040 cookie_params.insert(mapping.original_name.clone(), value.clone());
2041 }
2042 "body" => {
2043 body_params.insert(mapping.original_name.clone(), value.clone());
2044 }
2045 _ => {
2046 return Err(ToolCallValidationError::RequestConstructionError {
2047 reason: format!("Unknown parameter location for parameter: {key}"),
2048 });
2049 }
2050 }
2051 } else {
2052 let location = Self::get_parameter_location(tool_metadata, key).map_err(|e| {
2054 ToolCallValidationError::RequestConstructionError {
2055 reason: e.to_string(),
2056 }
2057 })?;
2058
2059 let original_name = Self::get_original_parameter_name(tool_metadata, key);
2060
2061 match location.as_str() {
2062 "path" => {
2063 path_params
2064 .insert(original_name.unwrap_or_else(|| key.clone()), value.clone());
2065 }
2066 "query" => {
2067 let param_name = original_name.unwrap_or_else(|| key.clone());
2068 let explode = Self::get_parameter_explode(tool_metadata, key);
2069 query_params
2070 .insert(param_name, QueryParameter::new(value.clone(), explode));
2071 }
2072 "header" => {
2073 let header_name = if let Some(orig) = original_name {
2074 orig
2075 } else if key.starts_with("header_") {
2076 key.strip_prefix("header_").unwrap_or(key).to_string()
2077 } else {
2078 key.clone()
2079 };
2080 header_params.insert(header_name, value.clone());
2081 }
2082 "cookie" => {
2083 let cookie_name = if let Some(orig) = original_name {
2084 orig
2085 } else if key.starts_with("cookie_") {
2086 key.strip_prefix("cookie_").unwrap_or(key).to_string()
2087 } else {
2088 key.clone()
2089 };
2090 cookie_params.insert(cookie_name, value.clone());
2091 }
2092 "body" => {
2093 let body_name = if key.starts_with("body_") {
2094 key.strip_prefix("body_").unwrap_or(key).to_string()
2095 } else {
2096 key.clone()
2097 };
2098 body_params.insert(body_name, value.clone());
2099 }
2100 _ => {
2101 return Err(ToolCallValidationError::RequestConstructionError {
2102 reason: format!("Unknown parameter location for parameter: {key}"),
2103 });
2104 }
2105 }
2106 }
2107 }
2108
2109 let extracted = ExtractedParameters {
2110 path: path_params,
2111 query: query_params,
2112 headers: header_params,
2113 cookies: cookie_params,
2114 body: body_params,
2115 config,
2116 };
2117
2118 trace!(
2119 tool_name = %tool_metadata.name,
2120 extracted_parameters = ?extracted,
2121 "Parameter extraction completed"
2122 );
2123
2124 Self::validate_parameters(tool_metadata, arguments)?;
2126
2127 Ok(extracted)
2128 }
2129
2130 fn get_original_parameter_name(
2132 tool_metadata: &ToolMetadata,
2133 param_name: &str,
2134 ) -> Option<String> {
2135 tool_metadata
2136 .parameters
2137 .get("properties")
2138 .and_then(|p| p.as_object())
2139 .and_then(|props| props.get(param_name))
2140 .and_then(|schema| schema.get(X_ORIGINAL_NAME))
2141 .and_then(|v| v.as_str())
2142 .map(|s| s.to_string())
2143 }
2144
2145 fn get_parameter_explode(tool_metadata: &ToolMetadata, param_name: &str) -> bool {
2147 tool_metadata
2148 .parameters
2149 .get("properties")
2150 .and_then(|p| p.as_object())
2151 .and_then(|props| props.get(param_name))
2152 .and_then(|schema| schema.get(X_PARAMETER_EXPLODE))
2153 .and_then(|v| v.as_bool())
2154 .unwrap_or(true) }
2156
2157 fn get_parameter_location(
2159 tool_metadata: &ToolMetadata,
2160 param_name: &str,
2161 ) -> Result<String, Error> {
2162 let properties = tool_metadata
2163 .parameters
2164 .get("properties")
2165 .and_then(|p| p.as_object())
2166 .ok_or_else(|| Error::ToolGeneration("Invalid tool parameters schema".to_string()))?;
2167
2168 if let Some(param_schema) = properties.get(param_name)
2169 && let Some(location) = param_schema
2170 .get(X_PARAMETER_LOCATION)
2171 .and_then(|v| v.as_str())
2172 {
2173 return Ok(location.to_string());
2174 }
2175
2176 if param_name.starts_with("header_") {
2178 Ok("header".to_string())
2179 } else if param_name.starts_with("cookie_") {
2180 Ok("cookie".to_string())
2181 } else if param_name.starts_with("body_") {
2182 Ok("body".to_string())
2183 } else {
2184 Ok("query".to_string())
2186 }
2187 }
2188
2189 fn validate_parameters(
2191 tool_metadata: &ToolMetadata,
2192 arguments: &Value,
2193 ) -> Result<(), ToolCallValidationError> {
2194 let schema = &tool_metadata.parameters;
2195
2196 let required_params = schema
2198 .get("required")
2199 .and_then(|r| r.as_array())
2200 .map(|arr| {
2201 arr.iter()
2202 .filter_map(|v| v.as_str())
2203 .collect::<std::collections::HashSet<_>>()
2204 })
2205 .unwrap_or_default();
2206
2207 let properties = schema
2208 .get("properties")
2209 .and_then(|p| p.as_object())
2210 .ok_or_else(|| ToolCallValidationError::RequestConstructionError {
2211 reason: "Tool schema missing properties".to_string(),
2212 })?;
2213
2214 let args = arguments.as_object().ok_or_else(|| {
2215 ToolCallValidationError::RequestConstructionError {
2216 reason: "Arguments must be an object".to_string(),
2217 }
2218 })?;
2219
2220 let mut all_errors = Vec::new();
2222
2223 all_errors.extend(Self::check_unknown_parameters(args, properties));
2225
2226 all_errors.extend(Self::check_missing_required(
2228 args,
2229 properties,
2230 &required_params,
2231 ));
2232
2233 all_errors.extend(Self::validate_parameter_values(
2235 args,
2236 properties,
2237 &required_params,
2238 ));
2239
2240 if !all_errors.is_empty() {
2242 return Err(ToolCallValidationError::InvalidParameters {
2243 violations: all_errors,
2244 });
2245 }
2246
2247 Ok(())
2248 }
2249
2250 fn check_unknown_parameters(
2252 args: &serde_json::Map<String, Value>,
2253 properties: &serde_json::Map<String, Value>,
2254 ) -> Vec<ValidationError> {
2255 let mut errors = Vec::new();
2256
2257 let valid_params: Vec<String> = properties.keys().map(|s| s.to_string()).collect();
2259
2260 for (arg_name, _) in args.iter() {
2262 if !properties.contains_key(arg_name) {
2263 errors.push(ValidationError::invalid_parameter(
2265 arg_name.clone(),
2266 &valid_params,
2267 ));
2268 }
2269 }
2270
2271 errors
2272 }
2273
2274 fn check_missing_required(
2276 args: &serde_json::Map<String, Value>,
2277 properties: &serde_json::Map<String, Value>,
2278 required_params: &HashSet<&str>,
2279 ) -> Vec<ValidationError> {
2280 let mut errors = Vec::new();
2281
2282 for required_param in required_params {
2283 if !args.contains_key(*required_param) {
2284 let param_schema = properties.get(*required_param);
2286
2287 let description = param_schema
2288 .and_then(|schema| schema.get("description"))
2289 .and_then(|d| d.as_str())
2290 .map(|s| s.to_string());
2291
2292 let expected_type = param_schema
2293 .and_then(Self::get_expected_type)
2294 .unwrap_or_else(|| "unknown".to_string());
2295
2296 errors.push(ValidationError::MissingRequiredParameter {
2297 parameter: (*required_param).to_string(),
2298 description,
2299 expected_type,
2300 });
2301 }
2302 }
2303
2304 errors
2305 }
2306
2307 fn validate_parameter_values(
2309 args: &serde_json::Map<String, Value>,
2310 properties: &serde_json::Map<String, Value>,
2311 required_params: &std::collections::HashSet<&str>,
2312 ) -> Vec<ValidationError> {
2313 let mut errors = Vec::new();
2314
2315 for (param_name, param_value) in args {
2316 if let Some(param_schema) = properties.get(param_name) {
2317 let is_null_value = param_value.is_null();
2319 let is_required = required_params.contains(param_name.as_str());
2320
2321 let schema = json!({
2323 "type": "object",
2324 "properties": {
2325 param_name: param_schema
2326 }
2327 });
2328
2329 let compiled = match jsonschema::validator_for(&schema) {
2331 Ok(compiled) => compiled,
2332 Err(e) => {
2333 errors.push(ValidationError::ConstraintViolation {
2334 parameter: param_name.clone(),
2335 message: format!(
2336 "Failed to compile schema for parameter '{param_name}': {e}"
2337 ),
2338 field_path: None,
2339 actual_value: None,
2340 expected_type: None,
2341 constraints: vec![],
2342 });
2343 continue;
2344 }
2345 };
2346
2347 let instance = json!({ param_name: param_value });
2349
2350 let validation_errors: Vec<_> =
2352 compiled.validate(&instance).err().into_iter().collect();
2353
2354 for validation_error in validation_errors {
2355 let error_message = validation_error.to_string();
2357 let instance_path_str = validation_error.instance_path().to_string();
2358 let field_path = if instance_path_str.is_empty() || instance_path_str == "/" {
2359 Some(param_name.clone())
2360 } else {
2361 Some(instance_path_str.trim_start_matches('/').to_string())
2362 };
2363
2364 let constraints = Self::extract_constraints_from_schema(param_schema);
2366
2367 let expected_type = Self::get_expected_type(param_schema);
2369
2370 let maybe_type_error = match &validation_error.kind() {
2374 ValidationErrorKind::Type { kind } => Some(kind),
2375 _ => None,
2376 };
2377 let is_type_error = maybe_type_error.is_some();
2378 let is_null_error = is_null_value
2379 || (is_type_error && validation_error.instance().as_null().is_some());
2380 let message = if is_null_error && let Some(type_error) = maybe_type_error {
2381 let field_name = field_path.as_ref().unwrap_or(param_name);
2383
2384 let final_expected_type =
2386 expected_type.clone().unwrap_or_else(|| match type_error {
2387 TypeKind::Single(json_type) => json_type.to_string(),
2388 TypeKind::Multiple(json_type_set) => json_type_set
2389 .iter()
2390 .map(|t| t.to_string())
2391 .collect::<Vec<_>>()
2392 .join(", "),
2393 });
2394
2395 let actual_field_name = field_path
2398 .as_ref()
2399 .and_then(|path| path.split('/').next_back())
2400 .unwrap_or(param_name);
2401
2402 let is_nested_field = field_path.as_ref().is_some_and(|p| p.contains('/'));
2405
2406 let field_is_required = if is_nested_field {
2407 constraints.iter().any(|c| {
2408 if let ValidationConstraint::Required { properties } = c {
2409 properties.contains(&actual_field_name.to_string())
2410 } else {
2411 false
2412 }
2413 })
2414 } else {
2415 is_required
2416 };
2417
2418 if field_is_required {
2419 format!(
2420 "Parameter '{field_name}' is required and must not be null (expected: {final_expected_type})"
2421 )
2422 } else {
2423 format!(
2424 "Parameter '{field_name}' is optional but must not be null (expected: {final_expected_type})"
2425 )
2426 }
2427 } else {
2428 error_message
2429 };
2430
2431 errors.push(ValidationError::ConstraintViolation {
2432 parameter: param_name.clone(),
2433 message,
2434 field_path,
2435 actual_value: Some(Box::new(param_value.clone())),
2436 expected_type,
2437 constraints,
2438 });
2439 }
2440 }
2441 }
2442
2443 errors
2444 }
2445
2446 fn extract_constraints_from_schema(schema: &Value) -> Vec<ValidationConstraint> {
2448 let mut constraints = Vec::new();
2449
2450 if let Some(min_value) = schema.get("minimum").and_then(|v| v.as_f64()) {
2452 let exclusive = schema
2453 .get("exclusiveMinimum")
2454 .and_then(|v| v.as_bool())
2455 .unwrap_or(false);
2456 constraints.push(ValidationConstraint::Minimum {
2457 value: min_value,
2458 exclusive,
2459 });
2460 }
2461
2462 if let Some(max_value) = schema.get("maximum").and_then(|v| v.as_f64()) {
2464 let exclusive = schema
2465 .get("exclusiveMaximum")
2466 .and_then(|v| v.as_bool())
2467 .unwrap_or(false);
2468 constraints.push(ValidationConstraint::Maximum {
2469 value: max_value,
2470 exclusive,
2471 });
2472 }
2473
2474 if let Some(min_len) = schema
2476 .get("minLength")
2477 .and_then(|v| v.as_u64())
2478 .map(|v| v as usize)
2479 {
2480 constraints.push(ValidationConstraint::MinLength { value: min_len });
2481 }
2482
2483 if let Some(max_len) = schema
2485 .get("maxLength")
2486 .and_then(|v| v.as_u64())
2487 .map(|v| v as usize)
2488 {
2489 constraints.push(ValidationConstraint::MaxLength { value: max_len });
2490 }
2491
2492 if let Some(pattern) = schema
2494 .get("pattern")
2495 .and_then(|v| v.as_str())
2496 .map(|s| s.to_string())
2497 {
2498 constraints.push(ValidationConstraint::Pattern { pattern });
2499 }
2500
2501 if let Some(enum_values) = schema.get("enum").and_then(|v| v.as_array()).cloned() {
2503 constraints.push(ValidationConstraint::EnumValues {
2504 values: enum_values,
2505 });
2506 }
2507
2508 if let Some(format) = schema
2510 .get("format")
2511 .and_then(|v| v.as_str())
2512 .map(|s| s.to_string())
2513 {
2514 constraints.push(ValidationConstraint::Format { format });
2515 }
2516
2517 if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
2519 constraints.push(ValidationConstraint::MultipleOf { value: multiple_of });
2520 }
2521
2522 if let Some(min_items) = schema
2524 .get("minItems")
2525 .and_then(|v| v.as_u64())
2526 .map(|v| v as usize)
2527 {
2528 constraints.push(ValidationConstraint::MinItems { value: min_items });
2529 }
2530
2531 if let Some(max_items) = schema
2533 .get("maxItems")
2534 .and_then(|v| v.as_u64())
2535 .map(|v| v as usize)
2536 {
2537 constraints.push(ValidationConstraint::MaxItems { value: max_items });
2538 }
2539
2540 if let Some(true) = schema.get("uniqueItems").and_then(|v| v.as_bool()) {
2542 constraints.push(ValidationConstraint::UniqueItems);
2543 }
2544
2545 if let Some(min_props) = schema
2547 .get("minProperties")
2548 .and_then(|v| v.as_u64())
2549 .map(|v| v as usize)
2550 {
2551 constraints.push(ValidationConstraint::MinProperties { value: min_props });
2552 }
2553
2554 if let Some(max_props) = schema
2556 .get("maxProperties")
2557 .and_then(|v| v.as_u64())
2558 .map(|v| v as usize)
2559 {
2560 constraints.push(ValidationConstraint::MaxProperties { value: max_props });
2561 }
2562
2563 if let Some(const_value) = schema.get("const").cloned() {
2565 constraints.push(ValidationConstraint::ConstValue { value: const_value });
2566 }
2567
2568 if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
2570 let properties: Vec<String> = required
2571 .iter()
2572 .filter_map(|v| v.as_str().map(|s| s.to_string()))
2573 .collect();
2574 if !properties.is_empty() {
2575 constraints.push(ValidationConstraint::Required { properties });
2576 }
2577 }
2578
2579 constraints
2580 }
2581
2582 fn get_expected_type(schema: &Value) -> Option<String> {
2584 if let Some(type_value) = schema.get("type") {
2585 if let Some(type_str) = type_value.as_str() {
2586 return Some(type_str.to_string());
2587 } else if let Some(type_array) = type_value.as_array() {
2588 let types: Vec<String> = type_array
2590 .iter()
2591 .filter_map(|v| v.as_str())
2592 .map(|s| s.to_string())
2593 .collect();
2594 if !types.is_empty() {
2595 return Some(types.join(" | "));
2596 }
2597 }
2598 }
2599 None
2600 }
2601
2602 fn wrap_output_schema(
2626 body_schema: &ObjectOrReference<ObjectSchema>,
2627 spec: &Spec,
2628 ) -> Result<Value, Error> {
2629 let mut visited = HashSet::new();
2631 let body_schema_json = match body_schema {
2632 ObjectOrReference::Object(obj_schema) => {
2633 Self::convert_object_schema_to_json_schema(obj_schema, spec, &mut visited)?
2634 }
2635 ObjectOrReference::Ref { ref_path, .. } => {
2636 let resolved = Self::resolve_reference(ref_path, spec, &mut visited)?;
2637 Self::convert_object_schema_to_json_schema(&resolved, spec, &mut visited)?
2638 }
2639 };
2640
2641 let error_schema = create_error_response_schema();
2642
2643 Ok(json!({
2644 "type": "object",
2645 "description": "Unified response structure with success and error variants",
2646 "required": ["status", "body"],
2647 "additionalProperties": false,
2648 "properties": {
2649 "status": {
2650 "type": "integer",
2651 "description": "HTTP status code",
2652 "minimum": 100,
2653 "maximum": 599
2654 },
2655 "body": {
2656 "description": "Response body - either success data or error information",
2657 "oneOf": [
2658 body_schema_json,
2659 error_schema
2660 ]
2661 }
2662 }
2663 }))
2664 }
2665}
2666
2667fn create_error_response_schema() -> Value {
2669 let root_schema = schema_for!(ErrorResponse);
2670 let schema_json = serde_json::to_value(root_schema).expect("Valid error schema");
2671
2672 let definitions = schema_json
2674 .get("$defs")
2675 .or_else(|| schema_json.get("definitions"))
2676 .cloned()
2677 .unwrap_or_else(|| json!({}));
2678
2679 let mut result = schema_json.clone();
2681 if let Some(obj) = result.as_object_mut() {
2682 obj.remove("$schema");
2683 obj.remove("$defs");
2684 obj.remove("definitions");
2685 obj.remove("title");
2686 }
2687
2688 inline_refs(&mut result, &definitions);
2690
2691 result
2692}
2693
2694fn inline_refs(schema: &mut Value, definitions: &Value) {
2696 match schema {
2697 Value::Object(obj) => {
2698 if let Some(ref_value) = obj.get("$ref").cloned()
2700 && let Some(ref_str) = ref_value.as_str()
2701 {
2702 let def_name = ref_str
2704 .strip_prefix("#/$defs/")
2705 .or_else(|| ref_str.strip_prefix("#/definitions/"));
2706
2707 if let Some(name) = def_name
2708 && let Some(definition) = definitions.get(name)
2709 {
2710 *schema = definition.clone();
2712 inline_refs(schema, definitions);
2714 return;
2715 }
2716 }
2717
2718 for (_, value) in obj.iter_mut() {
2720 inline_refs(value, definitions);
2721 }
2722 }
2723 Value::Array(arr) => {
2724 for item in arr.iter_mut() {
2726 inline_refs(item, definitions);
2727 }
2728 }
2729 _ => {} }
2731}
2732
2733#[derive(Debug, Clone)]
2735pub struct QueryParameter {
2736 pub value: Value,
2737 pub explode: bool,
2738}
2739
2740impl QueryParameter {
2741 pub fn new(value: Value, explode: bool) -> Self {
2742 Self { value, explode }
2743 }
2744}
2745
2746#[derive(Debug, Clone)]
2748pub struct ExtractedParameters {
2749 pub path: HashMap<String, Value>,
2750 pub query: HashMap<String, QueryParameter>,
2751 pub headers: HashMap<String, Value>,
2752 pub cookies: HashMap<String, Value>,
2753 pub body: HashMap<String, Value>,
2754 pub config: RequestConfig,
2755}
2756
2757#[derive(Debug, Clone)]
2759pub struct RequestConfig {
2760 pub timeout_seconds: u32,
2761 pub content_type: String,
2762}
2763
2764impl Default for RequestConfig {
2765 fn default() -> Self {
2766 Self {
2767 timeout_seconds: 30,
2768 content_type: mime::APPLICATION_JSON.to_string(),
2769 }
2770 }
2771}
2772
2773#[cfg(test)]
2774mod tests {
2775 use super::*;
2776
2777 use insta::assert_json_snapshot;
2778 use oas3::spec::{
2779 BooleanSchema, Components, MediaType, ObjectOrReference, ObjectSchema, Operation,
2780 Parameter, ParameterIn, RequestBody, Schema, SchemaType, SchemaTypeSet, Spec,
2781 };
2782 use rmcp::model::Tool;
2783 use serde_json::{Value, json};
2784 use std::collections::BTreeMap;
2785
2786 fn create_test_spec() -> Spec {
2788 Spec {
2789 openapi: "3.0.0".to_string(),
2790 info: oas3::spec::Info {
2791 title: "Test API".to_string(),
2792 version: "1.0.0".to_string(),
2793 summary: None,
2794 description: Some("Test API for unit tests".to_string()),
2795 terms_of_service: None,
2796 contact: None,
2797 license: None,
2798 extensions: Default::default(),
2799 },
2800 components: Some(Components {
2801 schemas: BTreeMap::new(),
2802 responses: BTreeMap::new(),
2803 parameters: BTreeMap::new(),
2804 examples: BTreeMap::new(),
2805 request_bodies: BTreeMap::new(),
2806 headers: BTreeMap::new(),
2807 security_schemes: BTreeMap::new(),
2808 links: BTreeMap::new(),
2809 callbacks: BTreeMap::new(),
2810 path_items: BTreeMap::new(),
2811 extensions: Default::default(),
2812 }),
2813 servers: vec![],
2814 paths: None,
2815 external_docs: None,
2816 tags: vec![],
2817 security: vec![],
2818 webhooks: BTreeMap::new(),
2819 extensions: Default::default(),
2820 }
2821 }
2822
2823 fn validate_tool_against_mcp_schema(metadata: &ToolMetadata) {
2824 let schema_content = std::fs::read_to_string("schema/2025-06-18/schema.json")
2825 .expect("Failed to read MCP schema file");
2826 let full_schema: Value =
2827 serde_json::from_str(&schema_content).expect("Failed to parse MCP schema JSON");
2828
2829 let tool_schema = json!({
2831 "$schema": "http://json-schema.org/draft-07/schema#",
2832 "definitions": full_schema.get("definitions"),
2833 "$ref": "#/definitions/Tool"
2834 });
2835
2836 let validator =
2837 jsonschema::validator_for(&tool_schema).expect("Failed to compile MCP Tool schema");
2838
2839 let tool = Tool::from(metadata);
2841
2842 let mcp_tool_json = serde_json::to_value(&tool).expect("Failed to serialize Tool to JSON");
2844
2845 let errors: Vec<String> = validator
2847 .iter_errors(&mcp_tool_json)
2848 .map(|e| e.to_string())
2849 .collect();
2850
2851 if !errors.is_empty() {
2852 panic!("Generated tool failed MCP schema validation: {errors:?}");
2853 }
2854 }
2855
2856 #[test]
2857 fn test_error_schema_structure() {
2858 let error_schema = create_error_response_schema();
2859
2860 assert!(error_schema.get("$schema").is_none());
2862 assert!(error_schema.get("definitions").is_none());
2863
2864 assert_json_snapshot!(error_schema);
2866 }
2867
2868 #[test]
2869 fn test_petstore_get_pet_by_id() {
2870 use oas3::spec::Response;
2871
2872 let mut operation = Operation {
2873 operation_id: Some("getPetById".to_string()),
2874 summary: Some("Find pet by ID".to_string()),
2875 description: Some("Returns a single pet".to_string()),
2876 tags: vec![],
2877 external_docs: None,
2878 parameters: vec![],
2879 request_body: None,
2880 responses: Default::default(),
2881 callbacks: Default::default(),
2882 deprecated: Some(false),
2883 security: vec![],
2884 servers: vec![],
2885 extensions: Default::default(),
2886 };
2887
2888 let param = Parameter {
2890 name: "petId".to_string(),
2891 location: ParameterIn::Path,
2892 description: Some("ID of pet to return".to_string()),
2893 required: Some(true),
2894 deprecated: Some(false),
2895 allow_empty_value: Some(false),
2896 style: None,
2897 explode: None,
2898 allow_reserved: Some(false),
2899 schema: Some(ObjectOrReference::Object(ObjectSchema {
2900 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2901 minimum: Some(serde_json::Number::from(1_i64)),
2902 format: Some("int64".to_string()),
2903 ..Default::default()
2904 })),
2905 example: None,
2906 examples: Default::default(),
2907 content: None,
2908 extensions: Default::default(),
2909 };
2910
2911 operation.parameters.push(ObjectOrReference::Object(param));
2912
2913 let mut responses = BTreeMap::new();
2915 let mut content = BTreeMap::new();
2916 content.insert(
2917 "application/json".to_string(),
2918 MediaType {
2919 extensions: Default::default(),
2920 schema: Some(ObjectOrReference::Object(ObjectSchema {
2921 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2922 properties: {
2923 let mut props = BTreeMap::new();
2924 props.insert(
2925 "id".to_string(),
2926 ObjectOrReference::Object(ObjectSchema {
2927 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2928 format: Some("int64".to_string()),
2929 ..Default::default()
2930 }),
2931 );
2932 props.insert(
2933 "name".to_string(),
2934 ObjectOrReference::Object(ObjectSchema {
2935 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2936 ..Default::default()
2937 }),
2938 );
2939 props.insert(
2940 "status".to_string(),
2941 ObjectOrReference::Object(ObjectSchema {
2942 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2943 ..Default::default()
2944 }),
2945 );
2946 props
2947 },
2948 required: vec!["id".to_string(), "name".to_string()],
2949 ..Default::default()
2950 })),
2951 examples: None,
2952 encoding: Default::default(),
2953 },
2954 );
2955
2956 responses.insert(
2957 "200".to_string(),
2958 ObjectOrReference::Object(Response {
2959 description: Some("successful operation".to_string()),
2960 headers: Default::default(),
2961 content,
2962 links: Default::default(),
2963 extensions: Default::default(),
2964 }),
2965 );
2966 operation.responses = Some(responses);
2967
2968 let spec = create_test_spec();
2969 let metadata = ToolGenerator::generate_tool_metadata(
2970 &operation,
2971 "get".to_string(),
2972 "/pet/{petId}".to_string(),
2973 &spec,
2974 false,
2975 false,
2976 )
2977 .unwrap();
2978
2979 assert_eq!(metadata.name, "getPetById");
2980 assert_eq!(metadata.method, "get");
2981 assert_eq!(metadata.path, "/pet/{petId}");
2982 assert!(
2983 metadata
2984 .description
2985 .clone()
2986 .unwrap()
2987 .contains("Find pet by ID")
2988 );
2989
2990 assert!(metadata.output_schema.is_some());
2992 let output_schema = metadata.output_schema.as_ref().unwrap();
2993
2994 insta::assert_json_snapshot!("test_petstore_get_pet_by_id_output_schema", output_schema);
2996
2997 validate_tool_against_mcp_schema(&metadata);
2999 }
3000
3001 #[test]
3002 fn test_convert_prefix_items_to_draft07_mixed_types() {
3003 let prefix_items = vec![
3006 ObjectOrReference::Object(ObjectSchema {
3007 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3008 format: Some("int32".to_string()),
3009 ..Default::default()
3010 }),
3011 ObjectOrReference::Object(ObjectSchema {
3012 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3013 ..Default::default()
3014 }),
3015 ];
3016
3017 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
3019
3020 let mut result = serde_json::Map::new();
3021 let spec = create_test_spec();
3022 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
3023 .unwrap();
3024
3025 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_mixed_types", result);
3027 }
3028
3029 #[test]
3030 fn test_convert_prefix_items_to_draft07_uniform_types() {
3031 let prefix_items = vec![
3033 ObjectOrReference::Object(ObjectSchema {
3034 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3035 ..Default::default()
3036 }),
3037 ObjectOrReference::Object(ObjectSchema {
3038 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3039 ..Default::default()
3040 }),
3041 ];
3042
3043 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
3045
3046 let mut result = serde_json::Map::new();
3047 let spec = create_test_spec();
3048 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
3049 .unwrap();
3050
3051 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_uniform_types", result);
3053 }
3054
3055 #[test]
3056 fn test_array_with_prefix_items_integration() {
3057 let param = Parameter {
3059 name: "coordinates".to_string(),
3060 location: ParameterIn::Query,
3061 description: Some("X,Y coordinates as tuple".to_string()),
3062 required: Some(true),
3063 deprecated: Some(false),
3064 allow_empty_value: Some(false),
3065 style: None,
3066 explode: None,
3067 allow_reserved: Some(false),
3068 schema: Some(ObjectOrReference::Object(ObjectSchema {
3069 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3070 prefix_items: vec![
3071 ObjectOrReference::Object(ObjectSchema {
3072 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
3073 format: Some("double".to_string()),
3074 ..Default::default()
3075 }),
3076 ObjectOrReference::Object(ObjectSchema {
3077 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
3078 format: Some("double".to_string()),
3079 ..Default::default()
3080 }),
3081 ],
3082 items: Some(Box::new(Schema::Boolean(BooleanSchema(false)))),
3083 ..Default::default()
3084 })),
3085 example: None,
3086 examples: Default::default(),
3087 content: None,
3088 extensions: Default::default(),
3089 };
3090
3091 let spec = create_test_spec();
3092 let (result, _annotations) =
3093 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, false)
3094 .unwrap();
3095
3096 insta::assert_json_snapshot!("test_array_with_prefix_items_integration", result);
3098 }
3099
3100 #[test]
3101 fn test_skip_tool_description() {
3102 let operation = Operation {
3103 operation_id: Some("getPetById".to_string()),
3104 summary: Some("Find pet by ID".to_string()),
3105 description: Some("Returns a single pet".to_string()),
3106 tags: vec![],
3107 external_docs: None,
3108 parameters: vec![],
3109 request_body: None,
3110 responses: Default::default(),
3111 callbacks: Default::default(),
3112 deprecated: Some(false),
3113 security: vec![],
3114 servers: vec![],
3115 extensions: Default::default(),
3116 };
3117
3118 let spec = create_test_spec();
3119 let metadata = ToolGenerator::generate_tool_metadata(
3120 &operation,
3121 "get".to_string(),
3122 "/pet/{petId}".to_string(),
3123 &spec,
3124 true,
3125 false,
3126 )
3127 .unwrap();
3128
3129 assert_eq!(metadata.name, "getPetById");
3130 assert_eq!(metadata.method, "get");
3131 assert_eq!(metadata.path, "/pet/{petId}");
3132 assert!(metadata.description.is_none());
3133
3134 insta::assert_json_snapshot!("test_skip_tool_description", metadata);
3136
3137 validate_tool_against_mcp_schema(&metadata);
3139 }
3140
3141 #[test]
3142 fn test_keep_tool_description() {
3143 let description = Some("Returns a single pet".to_string());
3144 let operation = Operation {
3145 operation_id: Some("getPetById".to_string()),
3146 summary: Some("Find pet by ID".to_string()),
3147 description: description.clone(),
3148 tags: vec![],
3149 external_docs: None,
3150 parameters: vec![],
3151 request_body: None,
3152 responses: Default::default(),
3153 callbacks: Default::default(),
3154 deprecated: Some(false),
3155 security: vec![],
3156 servers: vec![],
3157 extensions: Default::default(),
3158 };
3159
3160 let spec = create_test_spec();
3161 let metadata = ToolGenerator::generate_tool_metadata(
3162 &operation,
3163 "get".to_string(),
3164 "/pet/{petId}".to_string(),
3165 &spec,
3166 false,
3167 false,
3168 )
3169 .unwrap();
3170
3171 assert_eq!(metadata.name, "getPetById");
3172 assert_eq!(metadata.method, "get");
3173 assert_eq!(metadata.path, "/pet/{petId}");
3174 assert!(metadata.description.is_some());
3175
3176 insta::assert_json_snapshot!("test_keep_tool_description", metadata);
3178
3179 validate_tool_against_mcp_schema(&metadata);
3181 }
3182
3183 #[test]
3184 fn test_skip_parameter_descriptions() {
3185 let param = Parameter {
3186 name: "status".to_string(),
3187 location: ParameterIn::Query,
3188 description: Some("Filter by status".to_string()),
3189 required: Some(false),
3190 deprecated: Some(false),
3191 allow_empty_value: Some(false),
3192 style: None,
3193 explode: None,
3194 allow_reserved: Some(false),
3195 schema: Some(ObjectOrReference::Object(ObjectSchema {
3196 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3197 enum_values: vec![json!("available"), json!("pending"), json!("sold")],
3198 ..Default::default()
3199 })),
3200 example: Some(json!("available")),
3201 examples: Default::default(),
3202 content: None,
3203 extensions: Default::default(),
3204 };
3205
3206 let spec = create_test_spec();
3207 let (schema, _) =
3208 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, true)
3209 .unwrap();
3210
3211 assert!(schema.get("description").is_none());
3213
3214 assert_eq!(schema.get("type").unwrap(), "string");
3216 assert_eq!(schema.get("example").unwrap(), "available");
3217
3218 insta::assert_json_snapshot!("test_skip_parameter_descriptions", schema);
3219 }
3220
3221 #[test]
3222 fn test_keep_parameter_descriptions() {
3223 let param = Parameter {
3224 name: "status".to_string(),
3225 location: ParameterIn::Query,
3226 description: Some("Filter by status".to_string()),
3227 required: Some(false),
3228 deprecated: Some(false),
3229 allow_empty_value: Some(false),
3230 style: None,
3231 explode: None,
3232 allow_reserved: Some(false),
3233 schema: Some(ObjectOrReference::Object(ObjectSchema {
3234 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3235 enum_values: vec![json!("available"), json!("pending"), json!("sold")],
3236 ..Default::default()
3237 })),
3238 example: Some(json!("available")),
3239 examples: Default::default(),
3240 content: None,
3241 extensions: Default::default(),
3242 };
3243
3244 let spec = create_test_spec();
3245 let (schema, _) =
3246 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, false)
3247 .unwrap();
3248
3249 assert!(schema.get("description").is_some());
3251 let description = schema.get("description").unwrap().as_str().unwrap();
3252 assert!(description.contains("Filter by status"));
3253 assert!(description.contains("Example: `\"available\"`"));
3254
3255 assert_eq!(schema.get("type").unwrap(), "string");
3257 assert_eq!(schema.get("example").unwrap(), "available");
3258
3259 insta::assert_json_snapshot!("test_keep_parameter_descriptions", schema);
3260 }
3261
3262 #[test]
3263 fn test_array_with_regular_items_schema() {
3264 let param = Parameter {
3266 name: "tags".to_string(),
3267 location: ParameterIn::Query,
3268 description: Some("List of tags".to_string()),
3269 required: Some(false),
3270 deprecated: Some(false),
3271 allow_empty_value: Some(false),
3272 style: None,
3273 explode: None,
3274 allow_reserved: Some(false),
3275 schema: Some(ObjectOrReference::Object(ObjectSchema {
3276 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3277 items: Some(Box::new(Schema::Object(Box::new(
3278 ObjectOrReference::Object(ObjectSchema {
3279 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3280 min_length: Some(1),
3281 max_length: Some(50),
3282 ..Default::default()
3283 }),
3284 )))),
3285 ..Default::default()
3286 })),
3287 example: None,
3288 examples: Default::default(),
3289 content: None,
3290 extensions: Default::default(),
3291 };
3292
3293 let spec = create_test_spec();
3294 let (result, _annotations) =
3295 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, false)
3296 .unwrap();
3297
3298 insta::assert_json_snapshot!("test_array_with_regular_items_schema", result);
3300 }
3301
3302 #[test]
3303 fn test_request_body_object_schema() {
3304 let operation = Operation {
3306 operation_id: Some("createPet".to_string()),
3307 summary: Some("Create a new pet".to_string()),
3308 description: Some("Creates a new pet in the store".to_string()),
3309 tags: vec![],
3310 external_docs: None,
3311 parameters: vec![],
3312 request_body: Some(ObjectOrReference::Object(RequestBody {
3313 description: Some("Pet object that needs to be added to the store".to_string()),
3314 content: {
3315 let mut content = BTreeMap::new();
3316 content.insert(
3317 "application/json".to_string(),
3318 MediaType {
3319 extensions: Default::default(),
3320 schema: Some(ObjectOrReference::Object(ObjectSchema {
3321 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3322 ..Default::default()
3323 })),
3324 examples: None,
3325 encoding: Default::default(),
3326 },
3327 );
3328 content
3329 },
3330 required: Some(true),
3331 })),
3332 responses: Default::default(),
3333 callbacks: Default::default(),
3334 deprecated: Some(false),
3335 security: vec![],
3336 servers: vec![],
3337 extensions: Default::default(),
3338 };
3339
3340 let spec = create_test_spec();
3341 let metadata = ToolGenerator::generate_tool_metadata(
3342 &operation,
3343 "post".to_string(),
3344 "/pets".to_string(),
3345 &spec,
3346 false,
3347 false,
3348 )
3349 .unwrap();
3350
3351 let properties = metadata
3353 .parameters
3354 .get("properties")
3355 .unwrap()
3356 .as_object()
3357 .unwrap();
3358 assert!(properties.contains_key("request_body"));
3359
3360 let required = metadata
3362 .parameters
3363 .get("required")
3364 .unwrap()
3365 .as_array()
3366 .unwrap();
3367 assert!(required.contains(&json!("request_body")));
3368
3369 let request_body_schema = properties.get("request_body").unwrap();
3371 insta::assert_json_snapshot!("test_request_body_object_schema", request_body_schema);
3372
3373 validate_tool_against_mcp_schema(&metadata);
3375 }
3376
3377 #[test]
3378 fn test_request_body_array_schema() {
3379 let operation = Operation {
3381 operation_id: Some("createPets".to_string()),
3382 summary: Some("Create multiple pets".to_string()),
3383 description: None,
3384 tags: vec![],
3385 external_docs: None,
3386 parameters: vec![],
3387 request_body: Some(ObjectOrReference::Object(RequestBody {
3388 description: Some("Array of pet objects".to_string()),
3389 content: {
3390 let mut content = BTreeMap::new();
3391 content.insert(
3392 "application/json".to_string(),
3393 MediaType {
3394 extensions: Default::default(),
3395 schema: Some(ObjectOrReference::Object(ObjectSchema {
3396 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3397 items: Some(Box::new(Schema::Object(Box::new(
3398 ObjectOrReference::Object(ObjectSchema {
3399 schema_type: Some(SchemaTypeSet::Single(
3400 SchemaType::Object,
3401 )),
3402 ..Default::default()
3403 }),
3404 )))),
3405 ..Default::default()
3406 })),
3407 examples: None,
3408 encoding: Default::default(),
3409 },
3410 );
3411 content
3412 },
3413 required: Some(false),
3414 })),
3415 responses: Default::default(),
3416 callbacks: Default::default(),
3417 deprecated: Some(false),
3418 security: vec![],
3419 servers: vec![],
3420 extensions: Default::default(),
3421 };
3422
3423 let spec = create_test_spec();
3424 let metadata = ToolGenerator::generate_tool_metadata(
3425 &operation,
3426 "post".to_string(),
3427 "/pets/batch".to_string(),
3428 &spec,
3429 false,
3430 false,
3431 )
3432 .unwrap();
3433
3434 let properties = metadata
3436 .parameters
3437 .get("properties")
3438 .unwrap()
3439 .as_object()
3440 .unwrap();
3441 assert!(properties.contains_key("request_body"));
3442
3443 let required = metadata
3445 .parameters
3446 .get("required")
3447 .unwrap()
3448 .as_array()
3449 .unwrap();
3450 assert!(!required.contains(&json!("request_body")));
3451
3452 let request_body_schema = properties.get("request_body").unwrap();
3454 insta::assert_json_snapshot!("test_request_body_array_schema", request_body_schema);
3455
3456 validate_tool_against_mcp_schema(&metadata);
3458 }
3459
3460 #[test]
3461 fn test_request_body_string_schema() {
3462 let operation = Operation {
3464 operation_id: Some("updatePetName".to_string()),
3465 summary: Some("Update pet name".to_string()),
3466 description: None,
3467 tags: vec![],
3468 external_docs: None,
3469 parameters: vec![],
3470 request_body: Some(ObjectOrReference::Object(RequestBody {
3471 description: None,
3472 content: {
3473 let mut content = BTreeMap::new();
3474 content.insert(
3475 "text/plain".to_string(),
3476 MediaType {
3477 extensions: Default::default(),
3478 schema: Some(ObjectOrReference::Object(ObjectSchema {
3479 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3480 min_length: Some(1),
3481 max_length: Some(100),
3482 ..Default::default()
3483 })),
3484 examples: None,
3485 encoding: Default::default(),
3486 },
3487 );
3488 content
3489 },
3490 required: Some(true),
3491 })),
3492 responses: Default::default(),
3493 callbacks: Default::default(),
3494 deprecated: Some(false),
3495 security: vec![],
3496 servers: vec![],
3497 extensions: Default::default(),
3498 };
3499
3500 let spec = create_test_spec();
3501 let metadata = ToolGenerator::generate_tool_metadata(
3502 &operation,
3503 "put".to_string(),
3504 "/pets/{petId}/name".to_string(),
3505 &spec,
3506 false,
3507 false,
3508 )
3509 .unwrap();
3510
3511 let properties = metadata
3513 .parameters
3514 .get("properties")
3515 .unwrap()
3516 .as_object()
3517 .unwrap();
3518 let request_body_schema = properties.get("request_body").unwrap();
3519 insta::assert_json_snapshot!("test_request_body_string_schema", request_body_schema);
3520
3521 validate_tool_against_mcp_schema(&metadata);
3523 }
3524
3525 #[test]
3526 fn test_request_body_ref_schema() {
3527 let operation = Operation {
3529 operation_id: Some("updatePet".to_string()),
3530 summary: Some("Update existing pet".to_string()),
3531 description: None,
3532 tags: vec![],
3533 external_docs: None,
3534 parameters: vec![],
3535 request_body: Some(ObjectOrReference::Ref {
3536 ref_path: "#/components/requestBodies/PetBody".to_string(),
3537 summary: None,
3538 description: None,
3539 }),
3540 responses: Default::default(),
3541 callbacks: Default::default(),
3542 deprecated: Some(false),
3543 security: vec![],
3544 servers: vec![],
3545 extensions: Default::default(),
3546 };
3547
3548 let spec = create_test_spec();
3549 let metadata = ToolGenerator::generate_tool_metadata(
3550 &operation,
3551 "put".to_string(),
3552 "/pets/{petId}".to_string(),
3553 &spec,
3554 false,
3555 false,
3556 )
3557 .unwrap();
3558
3559 let properties = metadata
3561 .parameters
3562 .get("properties")
3563 .unwrap()
3564 .as_object()
3565 .unwrap();
3566 let request_body_schema = properties.get("request_body").unwrap();
3567 insta::assert_json_snapshot!("test_request_body_ref_schema", request_body_schema);
3568
3569 validate_tool_against_mcp_schema(&metadata);
3571 }
3572
3573 #[test]
3574 fn test_no_request_body_for_get() {
3575 let operation = Operation {
3577 operation_id: Some("listPets".to_string()),
3578 summary: Some("List all pets".to_string()),
3579 description: None,
3580 tags: vec![],
3581 external_docs: None,
3582 parameters: vec![],
3583 request_body: None,
3584 responses: Default::default(),
3585 callbacks: Default::default(),
3586 deprecated: Some(false),
3587 security: vec![],
3588 servers: vec![],
3589 extensions: Default::default(),
3590 };
3591
3592 let spec = create_test_spec();
3593 let metadata = ToolGenerator::generate_tool_metadata(
3594 &operation,
3595 "get".to_string(),
3596 "/pets".to_string(),
3597 &spec,
3598 false,
3599 false,
3600 )
3601 .unwrap();
3602
3603 let properties = metadata
3605 .parameters
3606 .get("properties")
3607 .unwrap()
3608 .as_object()
3609 .unwrap();
3610 assert!(!properties.contains_key("request_body"));
3611
3612 validate_tool_against_mcp_schema(&metadata);
3614 }
3615
3616 #[test]
3617 fn test_request_body_simple_object_with_properties() {
3618 let operation = Operation {
3620 operation_id: Some("updatePetStatus".to_string()),
3621 summary: Some("Update pet status".to_string()),
3622 description: None,
3623 tags: vec![],
3624 external_docs: None,
3625 parameters: vec![],
3626 request_body: Some(ObjectOrReference::Object(RequestBody {
3627 description: Some("Pet status update".to_string()),
3628 content: {
3629 let mut content = BTreeMap::new();
3630 content.insert(
3631 "application/json".to_string(),
3632 MediaType {
3633 extensions: Default::default(),
3634 schema: Some(ObjectOrReference::Object(ObjectSchema {
3635 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3636 properties: {
3637 let mut props = BTreeMap::new();
3638 props.insert(
3639 "status".to_string(),
3640 ObjectOrReference::Object(ObjectSchema {
3641 schema_type: Some(SchemaTypeSet::Single(
3642 SchemaType::String,
3643 )),
3644 ..Default::default()
3645 }),
3646 );
3647 props.insert(
3648 "reason".to_string(),
3649 ObjectOrReference::Object(ObjectSchema {
3650 schema_type: Some(SchemaTypeSet::Single(
3651 SchemaType::String,
3652 )),
3653 ..Default::default()
3654 }),
3655 );
3656 props
3657 },
3658 required: vec!["status".to_string()],
3659 ..Default::default()
3660 })),
3661 examples: None,
3662 encoding: Default::default(),
3663 },
3664 );
3665 content
3666 },
3667 required: Some(false),
3668 })),
3669 responses: Default::default(),
3670 callbacks: Default::default(),
3671 deprecated: Some(false),
3672 security: vec![],
3673 servers: vec![],
3674 extensions: Default::default(),
3675 };
3676
3677 let spec = create_test_spec();
3678 let metadata = ToolGenerator::generate_tool_metadata(
3679 &operation,
3680 "patch".to_string(),
3681 "/pets/{petId}/status".to_string(),
3682 &spec,
3683 false,
3684 false,
3685 )
3686 .unwrap();
3687
3688 let properties = metadata
3690 .parameters
3691 .get("properties")
3692 .unwrap()
3693 .as_object()
3694 .unwrap();
3695 let request_body_schema = properties.get("request_body").unwrap();
3696 insta::assert_json_snapshot!(
3697 "test_request_body_simple_object_with_properties",
3698 request_body_schema
3699 );
3700
3701 let required = metadata
3703 .parameters
3704 .get("required")
3705 .unwrap()
3706 .as_array()
3707 .unwrap();
3708 assert!(!required.contains(&json!("request_body")));
3709
3710 validate_tool_against_mcp_schema(&metadata);
3712 }
3713
3714 #[test]
3715 fn test_request_body_with_nested_properties() {
3716 let operation = Operation {
3718 operation_id: Some("createUser".to_string()),
3719 summary: Some("Create a new user".to_string()),
3720 description: None,
3721 tags: vec![],
3722 external_docs: None,
3723 parameters: vec![],
3724 request_body: Some(ObjectOrReference::Object(RequestBody {
3725 description: Some("User creation data".to_string()),
3726 content: {
3727 let mut content = BTreeMap::new();
3728 content.insert(
3729 "application/json".to_string(),
3730 MediaType {
3731 extensions: Default::default(),
3732 schema: Some(ObjectOrReference::Object(ObjectSchema {
3733 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3734 properties: {
3735 let mut props = BTreeMap::new();
3736 props.insert(
3737 "name".to_string(),
3738 ObjectOrReference::Object(ObjectSchema {
3739 schema_type: Some(SchemaTypeSet::Single(
3740 SchemaType::String,
3741 )),
3742 ..Default::default()
3743 }),
3744 );
3745 props.insert(
3746 "age".to_string(),
3747 ObjectOrReference::Object(ObjectSchema {
3748 schema_type: Some(SchemaTypeSet::Single(
3749 SchemaType::Integer,
3750 )),
3751 minimum: Some(serde_json::Number::from(0)),
3752 maximum: Some(serde_json::Number::from(150)),
3753 ..Default::default()
3754 }),
3755 );
3756 props
3757 },
3758 required: vec!["name".to_string()],
3759 ..Default::default()
3760 })),
3761 examples: None,
3762 encoding: Default::default(),
3763 },
3764 );
3765 content
3766 },
3767 required: Some(true),
3768 })),
3769 responses: Default::default(),
3770 callbacks: Default::default(),
3771 deprecated: Some(false),
3772 security: vec![],
3773 servers: vec![],
3774 extensions: Default::default(),
3775 };
3776
3777 let spec = create_test_spec();
3778 let metadata = ToolGenerator::generate_tool_metadata(
3779 &operation,
3780 "post".to_string(),
3781 "/users".to_string(),
3782 &spec,
3783 false,
3784 false,
3785 )
3786 .unwrap();
3787
3788 let properties = metadata
3790 .parameters
3791 .get("properties")
3792 .unwrap()
3793 .as_object()
3794 .unwrap();
3795 let request_body_schema = properties.get("request_body").unwrap();
3796 insta::assert_json_snapshot!(
3797 "test_request_body_with_nested_properties",
3798 request_body_schema
3799 );
3800
3801 validate_tool_against_mcp_schema(&metadata);
3803 }
3804
3805 #[test]
3806 fn test_operation_without_responses_has_no_output_schema() {
3807 let operation = Operation {
3808 operation_id: Some("testOperation".to_string()),
3809 summary: Some("Test operation".to_string()),
3810 description: None,
3811 tags: vec![],
3812 external_docs: None,
3813 parameters: vec![],
3814 request_body: None,
3815 responses: None,
3816 callbacks: Default::default(),
3817 deprecated: Some(false),
3818 security: vec![],
3819 servers: vec![],
3820 extensions: Default::default(),
3821 };
3822
3823 let spec = create_test_spec();
3824 let metadata = ToolGenerator::generate_tool_metadata(
3825 &operation,
3826 "get".to_string(),
3827 "/test".to_string(),
3828 &spec,
3829 false,
3830 false,
3831 )
3832 .unwrap();
3833
3834 assert!(metadata.output_schema.is_none());
3836
3837 validate_tool_against_mcp_schema(&metadata);
3839 }
3840
3841 #[test]
3842 fn test_extract_output_schema_with_200_response() {
3843 use oas3::spec::Response;
3844
3845 let mut responses = BTreeMap::new();
3847 let mut content = BTreeMap::new();
3848 content.insert(
3849 "application/json".to_string(),
3850 MediaType {
3851 extensions: Default::default(),
3852 schema: Some(ObjectOrReference::Object(ObjectSchema {
3853 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3854 properties: {
3855 let mut props = BTreeMap::new();
3856 props.insert(
3857 "id".to_string(),
3858 ObjectOrReference::Object(ObjectSchema {
3859 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3860 ..Default::default()
3861 }),
3862 );
3863 props.insert(
3864 "name".to_string(),
3865 ObjectOrReference::Object(ObjectSchema {
3866 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3867 ..Default::default()
3868 }),
3869 );
3870 props
3871 },
3872 required: vec!["id".to_string(), "name".to_string()],
3873 ..Default::default()
3874 })),
3875 examples: None,
3876 encoding: Default::default(),
3877 },
3878 );
3879
3880 responses.insert(
3881 "200".to_string(),
3882 ObjectOrReference::Object(Response {
3883 description: Some("Successful response".to_string()),
3884 headers: Default::default(),
3885 content,
3886 links: Default::default(),
3887 extensions: Default::default(),
3888 }),
3889 );
3890
3891 let spec = create_test_spec();
3892 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
3893
3894 insta::assert_json_snapshot!(result);
3896 }
3897
3898 #[test]
3899 fn test_extract_output_schema_with_201_response() {
3900 use oas3::spec::Response;
3901
3902 let mut responses = BTreeMap::new();
3904 let mut content = BTreeMap::new();
3905 content.insert(
3906 "application/json".to_string(),
3907 MediaType {
3908 extensions: Default::default(),
3909 schema: Some(ObjectOrReference::Object(ObjectSchema {
3910 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3911 properties: {
3912 let mut props = BTreeMap::new();
3913 props.insert(
3914 "created".to_string(),
3915 ObjectOrReference::Object(ObjectSchema {
3916 schema_type: Some(SchemaTypeSet::Single(SchemaType::Boolean)),
3917 ..Default::default()
3918 }),
3919 );
3920 props
3921 },
3922 ..Default::default()
3923 })),
3924 examples: None,
3925 encoding: Default::default(),
3926 },
3927 );
3928
3929 responses.insert(
3930 "201".to_string(),
3931 ObjectOrReference::Object(Response {
3932 description: Some("Created".to_string()),
3933 headers: Default::default(),
3934 content,
3935 links: Default::default(),
3936 extensions: Default::default(),
3937 }),
3938 );
3939
3940 let spec = create_test_spec();
3941 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
3942
3943 insta::assert_json_snapshot!(result);
3945 }
3946
3947 #[test]
3948 fn test_extract_output_schema_with_2xx_response() {
3949 use oas3::spec::Response;
3950
3951 let mut responses = BTreeMap::new();
3953 let mut content = BTreeMap::new();
3954 content.insert(
3955 "application/json".to_string(),
3956 MediaType {
3957 extensions: Default::default(),
3958 schema: Some(ObjectOrReference::Object(ObjectSchema {
3959 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3960 items: Some(Box::new(Schema::Object(Box::new(
3961 ObjectOrReference::Object(ObjectSchema {
3962 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3963 ..Default::default()
3964 }),
3965 )))),
3966 ..Default::default()
3967 })),
3968 examples: None,
3969 encoding: Default::default(),
3970 },
3971 );
3972
3973 responses.insert(
3974 "2XX".to_string(),
3975 ObjectOrReference::Object(Response {
3976 description: Some("Success".to_string()),
3977 headers: Default::default(),
3978 content,
3979 links: Default::default(),
3980 extensions: Default::default(),
3981 }),
3982 );
3983
3984 let spec = create_test_spec();
3985 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
3986
3987 insta::assert_json_snapshot!(result);
3989 }
3990
3991 #[test]
3992 fn test_extract_output_schema_no_responses() {
3993 let spec = create_test_spec();
3994 let result = ToolGenerator::extract_output_schema(&None, &spec).unwrap();
3995
3996 insta::assert_json_snapshot!(result);
3998 }
3999
4000 #[test]
4001 fn test_extract_output_schema_only_error_responses() {
4002 use oas3::spec::Response;
4003
4004 let mut responses = BTreeMap::new();
4006 responses.insert(
4007 "404".to_string(),
4008 ObjectOrReference::Object(Response {
4009 description: Some("Not found".to_string()),
4010 headers: Default::default(),
4011 content: Default::default(),
4012 links: Default::default(),
4013 extensions: Default::default(),
4014 }),
4015 );
4016 responses.insert(
4017 "500".to_string(),
4018 ObjectOrReference::Object(Response {
4019 description: Some("Server error".to_string()),
4020 headers: Default::default(),
4021 content: Default::default(),
4022 links: Default::default(),
4023 extensions: Default::default(),
4024 }),
4025 );
4026
4027 let spec = create_test_spec();
4028 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4029
4030 insta::assert_json_snapshot!(result);
4032 }
4033
4034 #[test]
4035 fn test_extract_output_schema_with_ref() {
4036 use oas3::spec::Response;
4037
4038 let mut spec = create_test_spec();
4040 let mut schemas = BTreeMap::new();
4041 schemas.insert(
4042 "Pet".to_string(),
4043 ObjectOrReference::Object(ObjectSchema {
4044 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4045 properties: {
4046 let mut props = BTreeMap::new();
4047 props.insert(
4048 "name".to_string(),
4049 ObjectOrReference::Object(ObjectSchema {
4050 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4051 ..Default::default()
4052 }),
4053 );
4054 props
4055 },
4056 ..Default::default()
4057 }),
4058 );
4059 spec.components.as_mut().unwrap().schemas = schemas;
4060
4061 let mut responses = BTreeMap::new();
4063 let mut content = BTreeMap::new();
4064 content.insert(
4065 "application/json".to_string(),
4066 MediaType {
4067 extensions: Default::default(),
4068 schema: Some(ObjectOrReference::Ref {
4069 ref_path: "#/components/schemas/Pet".to_string(),
4070 summary: None,
4071 description: None,
4072 }),
4073 examples: None,
4074 encoding: Default::default(),
4075 },
4076 );
4077
4078 responses.insert(
4079 "200".to_string(),
4080 ObjectOrReference::Object(Response {
4081 description: Some("Success".to_string()),
4082 headers: Default::default(),
4083 content,
4084 links: Default::default(),
4085 extensions: Default::default(),
4086 }),
4087 );
4088
4089 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
4090
4091 insta::assert_json_snapshot!(result);
4093 }
4094
4095 #[test]
4096 fn test_generate_tool_metadata_includes_output_schema() {
4097 use oas3::spec::Response;
4098
4099 let mut operation = Operation {
4100 operation_id: Some("getPet".to_string()),
4101 summary: Some("Get a pet".to_string()),
4102 description: None,
4103 tags: vec![],
4104 external_docs: None,
4105 parameters: vec![],
4106 request_body: None,
4107 responses: Default::default(),
4108 callbacks: Default::default(),
4109 deprecated: Some(false),
4110 security: vec![],
4111 servers: vec![],
4112 extensions: Default::default(),
4113 };
4114
4115 let mut responses = BTreeMap::new();
4117 let mut content = BTreeMap::new();
4118 content.insert(
4119 "application/json".to_string(),
4120 MediaType {
4121 extensions: Default::default(),
4122 schema: Some(ObjectOrReference::Object(ObjectSchema {
4123 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4124 properties: {
4125 let mut props = BTreeMap::new();
4126 props.insert(
4127 "id".to_string(),
4128 ObjectOrReference::Object(ObjectSchema {
4129 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4130 ..Default::default()
4131 }),
4132 );
4133 props
4134 },
4135 ..Default::default()
4136 })),
4137 examples: None,
4138 encoding: Default::default(),
4139 },
4140 );
4141
4142 responses.insert(
4143 "200".to_string(),
4144 ObjectOrReference::Object(Response {
4145 description: Some("Success".to_string()),
4146 headers: Default::default(),
4147 content,
4148 links: Default::default(),
4149 extensions: Default::default(),
4150 }),
4151 );
4152 operation.responses = Some(responses);
4153
4154 let spec = create_test_spec();
4155 let metadata = ToolGenerator::generate_tool_metadata(
4156 &operation,
4157 "get".to_string(),
4158 "/pets/{id}".to_string(),
4159 &spec,
4160 false,
4161 false,
4162 )
4163 .unwrap();
4164
4165 assert!(metadata.output_schema.is_some());
4167 let output_schema = metadata.output_schema.as_ref().unwrap();
4168
4169 insta::assert_json_snapshot!(
4171 "test_generate_tool_metadata_includes_output_schema",
4172 output_schema
4173 );
4174
4175 validate_tool_against_mcp_schema(&metadata);
4177 }
4178
4179 #[test]
4180 fn test_sanitize_property_name() {
4181 assert_eq!(sanitize_property_name("user name"), "user_name");
4183 assert_eq!(
4184 sanitize_property_name("first name last name"),
4185 "first_name_last_name"
4186 );
4187
4188 assert_eq!(sanitize_property_name("user(admin)"), "user_admin");
4190 assert_eq!(sanitize_property_name("user[admin]"), "user_admin");
4191 assert_eq!(sanitize_property_name("price($)"), "price");
4192 assert_eq!(sanitize_property_name("email@address"), "email_address");
4193 assert_eq!(sanitize_property_name("item#1"), "item_1");
4194 assert_eq!(sanitize_property_name("a/b/c"), "a_b_c");
4195
4196 assert_eq!(sanitize_property_name("user_name"), "user_name");
4198 assert_eq!(sanitize_property_name("userName123"), "userName123");
4199 assert_eq!(sanitize_property_name("user.name"), "user.name");
4200 assert_eq!(sanitize_property_name("user-name"), "user-name");
4201
4202 assert_eq!(sanitize_property_name("123name"), "param_123name");
4204 assert_eq!(sanitize_property_name("1st_place"), "param_1st_place");
4205
4206 assert_eq!(sanitize_property_name(""), "param_");
4208
4209 let long_name = "a".repeat(100);
4211 assert_eq!(sanitize_property_name(&long_name).len(), 64);
4212
4213 assert_eq!(sanitize_property_name("!@#$%^&*()"), "param_");
4216 }
4217
4218 #[test]
4219 fn test_sanitize_property_name_trailing_underscores() {
4220 assert_eq!(sanitize_property_name("page[size]"), "page_size");
4222 assert_eq!(sanitize_property_name("user[id]"), "user_id");
4223 assert_eq!(sanitize_property_name("field[]"), "field");
4224
4225 assert_eq!(sanitize_property_name("field___"), "field");
4227 assert_eq!(sanitize_property_name("test[[["), "test");
4228 }
4229
4230 #[test]
4231 fn test_sanitize_property_name_consecutive_underscores() {
4232 assert_eq!(sanitize_property_name("user__name"), "user_name");
4234 assert_eq!(sanitize_property_name("first___last"), "first_last");
4235 assert_eq!(sanitize_property_name("a____b____c"), "a_b_c");
4236
4237 assert_eq!(sanitize_property_name("user[[name]]"), "user_name");
4239 assert_eq!(sanitize_property_name("field@#$value"), "field_value");
4240 }
4241
4242 #[test]
4243 fn test_sanitize_property_name_edge_cases() {
4244 assert_eq!(sanitize_property_name("_private"), "_private");
4246 assert_eq!(sanitize_property_name("__dunder"), "_dunder");
4247
4248 assert_eq!(sanitize_property_name("[[["), "param_");
4250 assert_eq!(sanitize_property_name("@@@"), "param_");
4251
4252 assert_eq!(sanitize_property_name(""), "param_");
4254
4255 assert_eq!(sanitize_property_name("_field[size]"), "_field_size");
4257 assert_eq!(sanitize_property_name("__test__"), "_test");
4258 }
4259
4260 #[test]
4261 fn test_sanitize_property_name_complex_cases() {
4262 assert_eq!(sanitize_property_name("page[size]"), "page_size");
4264 assert_eq!(sanitize_property_name("filter[status]"), "filter_status");
4265 assert_eq!(
4266 sanitize_property_name("sort[-created_at]"),
4267 "sort_-created_at"
4268 );
4269 assert_eq!(
4270 sanitize_property_name("include[author.posts]"),
4271 "include_author.posts"
4272 );
4273
4274 let long_name = "very_long_field_name_with_special[characters]_that_needs_truncation_____";
4276 let expected = "very_long_field_name_with_special_characters_that_needs_truncat";
4277 assert_eq!(sanitize_property_name(long_name), expected);
4278 }
4279
4280 #[test]
4281 fn test_property_sanitization_with_annotations() {
4282 let spec = create_test_spec();
4283 let mut visited = HashSet::new();
4284
4285 let obj_schema = ObjectSchema {
4287 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4288 properties: {
4289 let mut props = BTreeMap::new();
4290 props.insert(
4292 "user name".to_string(),
4293 ObjectOrReference::Object(ObjectSchema {
4294 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4295 ..Default::default()
4296 }),
4297 );
4298 props.insert(
4300 "price($)".to_string(),
4301 ObjectOrReference::Object(ObjectSchema {
4302 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
4303 ..Default::default()
4304 }),
4305 );
4306 props.insert(
4308 "validName".to_string(),
4309 ObjectOrReference::Object(ObjectSchema {
4310 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4311 ..Default::default()
4312 }),
4313 );
4314 props
4315 },
4316 ..Default::default()
4317 };
4318
4319 let result =
4320 ToolGenerator::convert_object_schema_to_json_schema(&obj_schema, &spec, &mut visited)
4321 .unwrap();
4322
4323 insta::assert_json_snapshot!("test_property_sanitization_with_annotations", result);
4325 }
4326
4327 #[test]
4328 fn test_parameter_sanitization_and_extraction() {
4329 let spec = create_test_spec();
4330
4331 let operation = Operation {
4333 operation_id: Some("testOp".to_string()),
4334 parameters: vec![
4335 ObjectOrReference::Object(Parameter {
4337 name: "user(id)".to_string(),
4338 location: ParameterIn::Path,
4339 description: Some("User ID".to_string()),
4340 required: Some(true),
4341 deprecated: Some(false),
4342 allow_empty_value: Some(false),
4343 style: None,
4344 explode: None,
4345 allow_reserved: Some(false),
4346 schema: Some(ObjectOrReference::Object(ObjectSchema {
4347 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4348 ..Default::default()
4349 })),
4350 example: None,
4351 examples: Default::default(),
4352 content: None,
4353 extensions: Default::default(),
4354 }),
4355 ObjectOrReference::Object(Parameter {
4357 name: "page size".to_string(),
4358 location: ParameterIn::Query,
4359 description: Some("Page size".to_string()),
4360 required: Some(false),
4361 deprecated: Some(false),
4362 allow_empty_value: Some(false),
4363 style: None,
4364 explode: None,
4365 allow_reserved: Some(false),
4366 schema: Some(ObjectOrReference::Object(ObjectSchema {
4367 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4368 ..Default::default()
4369 })),
4370 example: None,
4371 examples: Default::default(),
4372 content: None,
4373 extensions: Default::default(),
4374 }),
4375 ObjectOrReference::Object(Parameter {
4377 name: "auth-token!".to_string(),
4378 location: ParameterIn::Header,
4379 description: Some("Auth token".to_string()),
4380 required: Some(false),
4381 deprecated: Some(false),
4382 allow_empty_value: Some(false),
4383 style: None,
4384 explode: None,
4385 allow_reserved: Some(false),
4386 schema: Some(ObjectOrReference::Object(ObjectSchema {
4387 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4388 ..Default::default()
4389 })),
4390 example: None,
4391 examples: Default::default(),
4392 content: None,
4393 extensions: Default::default(),
4394 }),
4395 ],
4396 ..Default::default()
4397 };
4398
4399 let tool_metadata = ToolGenerator::generate_tool_metadata(
4400 &operation,
4401 "get".to_string(),
4402 "/users/{user(id)}".to_string(),
4403 &spec,
4404 false,
4405 false,
4406 )
4407 .unwrap();
4408
4409 let properties = tool_metadata
4411 .parameters
4412 .get("properties")
4413 .unwrap()
4414 .as_object()
4415 .unwrap();
4416
4417 assert!(properties.contains_key("user_id"));
4418 assert!(properties.contains_key("page_size"));
4419 assert!(properties.contains_key("header_auth-token"));
4420
4421 let required = tool_metadata
4423 .parameters
4424 .get("required")
4425 .unwrap()
4426 .as_array()
4427 .unwrap();
4428 assert!(required.contains(&json!("user_id")));
4429
4430 let arguments = json!({
4432 "user_id": "123",
4433 "page_size": 10,
4434 "header_auth-token": "secret"
4435 });
4436
4437 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
4438
4439 assert_eq!(extracted.path.get("user(id)"), Some(&json!("123")));
4441
4442 assert_eq!(
4444 extracted.query.get("page size").map(|q| &q.value),
4445 Some(&json!(10))
4446 );
4447
4448 assert_eq!(extracted.headers.get("auth-token!"), Some(&json!("secret")));
4450 }
4451
4452 #[test]
4453 fn test_check_unknown_parameters() {
4454 let mut properties = serde_json::Map::new();
4456 properties.insert("page_size".to_string(), json!({"type": "integer"}));
4457 properties.insert("user_id".to_string(), json!({"type": "string"}));
4458
4459 let mut args = serde_json::Map::new();
4460 args.insert("page_sixe".to_string(), json!(10)); let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4463 assert!(!result.is_empty());
4464 assert_eq!(result.len(), 1);
4465
4466 match &result[0] {
4467 ValidationError::InvalidParameter {
4468 parameter,
4469 suggestions,
4470 valid_parameters,
4471 } => {
4472 assert_eq!(parameter, "page_sixe");
4473 assert_eq!(suggestions, &vec!["page_size".to_string()]);
4474 assert_eq!(
4475 valid_parameters,
4476 &vec!["page_size".to_string(), "user_id".to_string()]
4477 );
4478 }
4479 _ => panic!("Expected InvalidParameter variant"),
4480 }
4481 }
4482
4483 #[test]
4484 fn test_check_unknown_parameters_no_suggestions() {
4485 let mut properties = serde_json::Map::new();
4487 properties.insert("limit".to_string(), json!({"type": "integer"}));
4488 properties.insert("offset".to_string(), json!({"type": "integer"}));
4489
4490 let mut args = serde_json::Map::new();
4491 args.insert("xyz123".to_string(), json!("value"));
4492
4493 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4494 assert!(!result.is_empty());
4495 assert_eq!(result.len(), 1);
4496
4497 match &result[0] {
4498 ValidationError::InvalidParameter {
4499 parameter,
4500 suggestions,
4501 valid_parameters,
4502 } => {
4503 assert_eq!(parameter, "xyz123");
4504 assert!(suggestions.is_empty());
4505 assert!(valid_parameters.contains(&"limit".to_string()));
4506 assert!(valid_parameters.contains(&"offset".to_string()));
4507 }
4508 _ => panic!("Expected InvalidParameter variant"),
4509 }
4510 }
4511
4512 #[test]
4513 fn test_check_unknown_parameters_multiple_suggestions() {
4514 let mut properties = serde_json::Map::new();
4516 properties.insert("user_id".to_string(), json!({"type": "string"}));
4517 properties.insert("user_iid".to_string(), json!({"type": "string"}));
4518 properties.insert("user_name".to_string(), json!({"type": "string"}));
4519
4520 let mut args = serde_json::Map::new();
4521 args.insert("usr_id".to_string(), json!("123"));
4522
4523 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4524 assert!(!result.is_empty());
4525 assert_eq!(result.len(), 1);
4526
4527 match &result[0] {
4528 ValidationError::InvalidParameter {
4529 parameter,
4530 suggestions,
4531 valid_parameters,
4532 } => {
4533 assert_eq!(parameter, "usr_id");
4534 assert!(!suggestions.is_empty());
4535 assert!(suggestions.contains(&"user_id".to_string()));
4536 assert_eq!(valid_parameters.len(), 3);
4537 }
4538 _ => panic!("Expected InvalidParameter variant"),
4539 }
4540 }
4541
4542 #[test]
4543 fn test_check_unknown_parameters_valid() {
4544 let mut properties = serde_json::Map::new();
4546 properties.insert("name".to_string(), json!({"type": "string"}));
4547 properties.insert("email".to_string(), json!({"type": "string"}));
4548
4549 let mut args = serde_json::Map::new();
4550 args.insert("name".to_string(), json!("John"));
4551 args.insert("email".to_string(), json!("john@example.com"));
4552
4553 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4554 assert!(result.is_empty());
4555 }
4556
4557 #[test]
4558 fn test_check_unknown_parameters_empty() {
4559 let properties = serde_json::Map::new();
4561
4562 let mut args = serde_json::Map::new();
4563 args.insert("any_param".to_string(), json!("value"));
4564
4565 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4566 assert!(!result.is_empty());
4567 assert_eq!(result.len(), 1);
4568
4569 match &result[0] {
4570 ValidationError::InvalidParameter {
4571 parameter,
4572 suggestions,
4573 valid_parameters,
4574 } => {
4575 assert_eq!(parameter, "any_param");
4576 assert!(suggestions.is_empty());
4577 assert!(valid_parameters.is_empty());
4578 }
4579 _ => panic!("Expected InvalidParameter variant"),
4580 }
4581 }
4582
4583 #[test]
4584 fn test_check_unknown_parameters_gltf_pagination() {
4585 let mut properties = serde_json::Map::new();
4587 properties.insert(
4588 "page_number".to_string(),
4589 json!({
4590 "type": "integer",
4591 "x-original-name": "page[number]"
4592 }),
4593 );
4594 properties.insert(
4595 "page_size".to_string(),
4596 json!({
4597 "type": "integer",
4598 "x-original-name": "page[size]"
4599 }),
4600 );
4601
4602 let mut args = serde_json::Map::new();
4604 args.insert("page".to_string(), json!(1));
4605 args.insert("per_page".to_string(), json!(10));
4606
4607 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4608 assert_eq!(result.len(), 2, "Should have 2 unknown parameters");
4609
4610 let page_error = result
4612 .iter()
4613 .find(|e| {
4614 if let ValidationError::InvalidParameter { parameter, .. } = e {
4615 parameter == "page"
4616 } else {
4617 false
4618 }
4619 })
4620 .expect("Should have error for 'page'");
4621
4622 let per_page_error = result
4623 .iter()
4624 .find(|e| {
4625 if let ValidationError::InvalidParameter { parameter, .. } = e {
4626 parameter == "per_page"
4627 } else {
4628 false
4629 }
4630 })
4631 .expect("Should have error for 'per_page'");
4632
4633 match page_error {
4635 ValidationError::InvalidParameter {
4636 suggestions,
4637 valid_parameters,
4638 ..
4639 } => {
4640 assert!(
4641 suggestions.contains(&"page_number".to_string()),
4642 "Should suggest 'page_number' for 'page'"
4643 );
4644 assert_eq!(valid_parameters.len(), 2);
4645 assert!(valid_parameters.contains(&"page_number".to_string()));
4646 assert!(valid_parameters.contains(&"page_size".to_string()));
4647 }
4648 _ => panic!("Expected InvalidParameter"),
4649 }
4650
4651 match per_page_error {
4653 ValidationError::InvalidParameter {
4654 parameter,
4655 suggestions,
4656 valid_parameters,
4657 ..
4658 } => {
4659 assert_eq!(parameter, "per_page");
4660 assert_eq!(valid_parameters.len(), 2);
4661 if !suggestions.is_empty() {
4664 assert!(suggestions.contains(&"page_size".to_string()));
4665 }
4666 }
4667 _ => panic!("Expected InvalidParameter"),
4668 }
4669 }
4670
4671 #[test]
4672 fn test_validate_parameters_with_invalid_params() {
4673 let tool_metadata = ToolMetadata {
4675 name: "listItems".to_string(),
4676 title: None,
4677 description: Some("List items".to_string()),
4678 parameters: json!({
4679 "type": "object",
4680 "properties": {
4681 "page_number": {
4682 "type": "integer",
4683 "x-original-name": "page[number]"
4684 },
4685 "page_size": {
4686 "type": "integer",
4687 "x-original-name": "page[size]"
4688 }
4689 },
4690 "required": []
4691 }),
4692 output_schema: None,
4693 method: "GET".to_string(),
4694 path: "/items".to_string(),
4695 security: None,
4696 parameter_mappings: std::collections::HashMap::new(),
4697 };
4698
4699 let arguments = json!({
4701 "page": 1,
4702 "per_page": 10
4703 });
4704
4705 let result = ToolGenerator::validate_parameters(&tool_metadata, &arguments);
4706 assert!(
4707 result.is_err(),
4708 "Should fail validation with unknown parameters"
4709 );
4710
4711 let error = result.unwrap_err();
4712 match error {
4713 ToolCallValidationError::InvalidParameters { violations } => {
4714 assert_eq!(violations.len(), 2, "Should have 2 validation errors");
4715
4716 let has_page_error = violations.iter().any(|v| {
4718 if let ValidationError::InvalidParameter { parameter, .. } = v {
4719 parameter == "page"
4720 } else {
4721 false
4722 }
4723 });
4724
4725 let has_per_page_error = violations.iter().any(|v| {
4726 if let ValidationError::InvalidParameter { parameter, .. } = v {
4727 parameter == "per_page"
4728 } else {
4729 false
4730 }
4731 });
4732
4733 assert!(has_page_error, "Should have error for 'page' parameter");
4734 assert!(
4735 has_per_page_error,
4736 "Should have error for 'per_page' parameter"
4737 );
4738 }
4739 _ => panic!("Expected InvalidParameters"),
4740 }
4741 }
4742
4743 #[test]
4744 fn test_cookie_parameter_sanitization() {
4745 let spec = create_test_spec();
4746
4747 let operation = Operation {
4748 operation_id: Some("testCookie".to_string()),
4749 parameters: vec![ObjectOrReference::Object(Parameter {
4750 name: "session[id]".to_string(),
4751 location: ParameterIn::Cookie,
4752 description: Some("Session ID".to_string()),
4753 required: Some(false),
4754 deprecated: Some(false),
4755 allow_empty_value: Some(false),
4756 style: None,
4757 explode: None,
4758 allow_reserved: Some(false),
4759 schema: Some(ObjectOrReference::Object(ObjectSchema {
4760 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4761 ..Default::default()
4762 })),
4763 example: None,
4764 examples: Default::default(),
4765 content: None,
4766 extensions: Default::default(),
4767 })],
4768 ..Default::default()
4769 };
4770
4771 let tool_metadata = ToolGenerator::generate_tool_metadata(
4772 &operation,
4773 "get".to_string(),
4774 "/data".to_string(),
4775 &spec,
4776 false,
4777 false,
4778 )
4779 .unwrap();
4780
4781 let properties = tool_metadata
4782 .parameters
4783 .get("properties")
4784 .unwrap()
4785 .as_object()
4786 .unwrap();
4787
4788 assert!(properties.contains_key("cookie_session_id"));
4790
4791 let arguments = json!({
4793 "cookie_session_id": "abc123"
4794 });
4795
4796 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
4797
4798 assert_eq!(extracted.cookies.get("session[id]"), Some(&json!("abc123")));
4800 }
4801
4802 #[test]
4803 fn test_parameter_description_with_examples() {
4804 let spec = create_test_spec();
4805
4806 let param_with_example = Parameter {
4808 name: "status".to_string(),
4809 location: ParameterIn::Query,
4810 description: Some("Filter by status".to_string()),
4811 required: Some(false),
4812 deprecated: Some(false),
4813 allow_empty_value: Some(false),
4814 style: None,
4815 explode: None,
4816 allow_reserved: Some(false),
4817 schema: Some(ObjectOrReference::Object(ObjectSchema {
4818 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4819 ..Default::default()
4820 })),
4821 example: Some(json!("active")),
4822 examples: Default::default(),
4823 content: None,
4824 extensions: Default::default(),
4825 };
4826
4827 let (schema, _) = ToolGenerator::convert_parameter_schema(
4828 ¶m_with_example,
4829 ParameterIn::Query,
4830 &spec,
4831 false,
4832 )
4833 .unwrap();
4834 let description = schema.get("description").unwrap().as_str().unwrap();
4835 assert_eq!(description, "Filter by status. Example: `\"active\"`");
4836
4837 let mut examples_map = std::collections::BTreeMap::new();
4839 examples_map.insert(
4840 "example1".to_string(),
4841 ObjectOrReference::Object(oas3::spec::Example {
4842 value: Some(json!("pending")),
4843 ..Default::default()
4844 }),
4845 );
4846 examples_map.insert(
4847 "example2".to_string(),
4848 ObjectOrReference::Object(oas3::spec::Example {
4849 value: Some(json!("completed")),
4850 ..Default::default()
4851 }),
4852 );
4853
4854 let param_with_examples = Parameter {
4855 name: "status".to_string(),
4856 location: ParameterIn::Query,
4857 description: Some("Filter by status".to_string()),
4858 required: Some(false),
4859 deprecated: Some(false),
4860 allow_empty_value: Some(false),
4861 style: None,
4862 explode: None,
4863 allow_reserved: Some(false),
4864 schema: Some(ObjectOrReference::Object(ObjectSchema {
4865 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4866 ..Default::default()
4867 })),
4868 example: None,
4869 examples: examples_map,
4870 content: None,
4871 extensions: Default::default(),
4872 };
4873
4874 let (schema, _) = ToolGenerator::convert_parameter_schema(
4875 ¶m_with_examples,
4876 ParameterIn::Query,
4877 &spec,
4878 false,
4879 )
4880 .unwrap();
4881 let description = schema.get("description").unwrap().as_str().unwrap();
4882 assert!(description.starts_with("Filter by status. Examples:\n"));
4883 assert!(description.contains("`\"pending\"`"));
4884 assert!(description.contains("`\"completed\"`"));
4885
4886 let param_no_desc = Parameter {
4888 name: "limit".to_string(),
4889 location: ParameterIn::Query,
4890 description: None,
4891 required: Some(false),
4892 deprecated: Some(false),
4893 allow_empty_value: Some(false),
4894 style: None,
4895 explode: None,
4896 allow_reserved: Some(false),
4897 schema: Some(ObjectOrReference::Object(ObjectSchema {
4898 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4899 ..Default::default()
4900 })),
4901 example: Some(json!(100)),
4902 examples: Default::default(),
4903 content: None,
4904 extensions: Default::default(),
4905 };
4906
4907 let (schema, _) = ToolGenerator::convert_parameter_schema(
4908 ¶m_no_desc,
4909 ParameterIn::Query,
4910 &spec,
4911 false,
4912 )
4913 .unwrap();
4914 let description = schema.get("description").unwrap().as_str().unwrap();
4915 assert_eq!(description, "limit parameter. Example: `100`");
4916 }
4917
4918 #[test]
4919 fn test_format_examples_for_description() {
4920 let examples = vec![json!("active")];
4922 let result = ToolGenerator::format_examples_for_description(&examples);
4923 assert_eq!(result, Some("Example: `\"active\"`".to_string()));
4924
4925 let examples = vec![json!(42)];
4927 let result = ToolGenerator::format_examples_for_description(&examples);
4928 assert_eq!(result, Some("Example: `42`".to_string()));
4929
4930 let examples = vec![json!(true)];
4932 let result = ToolGenerator::format_examples_for_description(&examples);
4933 assert_eq!(result, Some("Example: `true`".to_string()));
4934
4935 let examples = vec![json!("active"), json!("pending"), json!("completed")];
4937 let result = ToolGenerator::format_examples_for_description(&examples);
4938 assert_eq!(
4939 result,
4940 Some("Examples:\n- `\"active\"`\n- `\"pending\"`\n- `\"completed\"`".to_string())
4941 );
4942
4943 let examples = vec![json!(["a", "b", "c"])];
4945 let result = ToolGenerator::format_examples_for_description(&examples);
4946 assert_eq!(result, Some("Example: `[\"a\",\"b\",\"c\"]`".to_string()));
4947
4948 let examples = vec![json!({"key": "value"})];
4950 let result = ToolGenerator::format_examples_for_description(&examples);
4951 assert_eq!(result, Some("Example: `{\"key\":\"value\"}`".to_string()));
4952
4953 let examples = vec![];
4955 let result = ToolGenerator::format_examples_for_description(&examples);
4956 assert_eq!(result, None);
4957
4958 let examples = vec![json!(null)];
4960 let result = ToolGenerator::format_examples_for_description(&examples);
4961 assert_eq!(result, Some("Example: `null`".to_string()));
4962
4963 let examples = vec![json!("text"), json!(123), json!(true)];
4965 let result = ToolGenerator::format_examples_for_description(&examples);
4966 assert_eq!(
4967 result,
4968 Some("Examples:\n- `\"text\"`\n- `123`\n- `true`".to_string())
4969 );
4970
4971 let examples = vec![json!(["a", "b", "c", "d", "e", "f"])];
4973 let result = ToolGenerator::format_examples_for_description(&examples);
4974 assert_eq!(
4975 result,
4976 Some("Example: `[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\"]`".to_string())
4977 );
4978
4979 let examples = vec![json!([1, 2])];
4981 let result = ToolGenerator::format_examples_for_description(&examples);
4982 assert_eq!(result, Some("Example: `[1,2]`".to_string()));
4983
4984 let examples = vec![json!({"user": {"name": "John", "age": 30}})];
4986 let result = ToolGenerator::format_examples_for_description(&examples);
4987 assert_eq!(
4988 result,
4989 Some("Example: `{\"user\":{\"name\":\"John\",\"age\":30}}`".to_string())
4990 );
4991
4992 let examples = vec![json!("a"), json!("b"), json!("c"), json!("d"), json!("e")];
4994 let result = ToolGenerator::format_examples_for_description(&examples);
4995 assert_eq!(
4996 result,
4997 Some("Examples:\n- `\"a\"`\n- `\"b\"`\n- `\"c\"`\n- `\"d\"`\n- `\"e\"`".to_string())
4998 );
4999
5000 let examples = vec![json!(3.5)];
5002 let result = ToolGenerator::format_examples_for_description(&examples);
5003 assert_eq!(result, Some("Example: `3.5`".to_string()));
5004
5005 let examples = vec![json!(-42)];
5007 let result = ToolGenerator::format_examples_for_description(&examples);
5008 assert_eq!(result, Some("Example: `-42`".to_string()));
5009
5010 let examples = vec![json!(false)];
5012 let result = ToolGenerator::format_examples_for_description(&examples);
5013 assert_eq!(result, Some("Example: `false`".to_string()));
5014
5015 let examples = vec![json!("hello \"world\"")];
5017 let result = ToolGenerator::format_examples_for_description(&examples);
5018 assert_eq!(result, Some(r#"Example: `"hello \"world\""`"#.to_string()));
5020
5021 let examples = vec![json!("")];
5023 let result = ToolGenerator::format_examples_for_description(&examples);
5024 assert_eq!(result, Some("Example: `\"\"`".to_string()));
5025
5026 let examples = vec![json!([])];
5028 let result = ToolGenerator::format_examples_for_description(&examples);
5029 assert_eq!(result, Some("Example: `[]`".to_string()));
5030
5031 let examples = vec![json!({})];
5033 let result = ToolGenerator::format_examples_for_description(&examples);
5034 assert_eq!(result, Some("Example: `{}`".to_string()));
5035 }
5036
5037 #[test]
5038 fn test_reference_metadata_functionality() {
5039 let metadata = ReferenceMetadata::new(
5041 Some("User Reference".to_string()),
5042 Some("A reference to user data with additional context".to_string()),
5043 );
5044
5045 assert!(!metadata.is_empty());
5046 assert_eq!(metadata.summary(), Some("User Reference"));
5047 assert_eq!(
5048 metadata.best_description(),
5049 Some("A reference to user data with additional context")
5050 );
5051
5052 let summary_only = ReferenceMetadata::new(Some("Pet Summary".to_string()), None);
5054 assert_eq!(summary_only.best_description(), Some("Pet Summary"));
5055
5056 let empty_metadata = ReferenceMetadata::new(None, None);
5058 assert!(empty_metadata.is_empty());
5059 assert_eq!(empty_metadata.best_description(), None);
5060
5061 let metadata = ReferenceMetadata::new(
5063 Some("Reference Summary".to_string()),
5064 Some("Reference Description".to_string()),
5065 );
5066
5067 let result = metadata.merge_with_description(None, false);
5069 assert_eq!(result, Some("Reference Description".to_string()));
5070
5071 let result = metadata.merge_with_description(Some("Existing desc"), false);
5073 assert_eq!(result, Some("Reference Description".to_string()));
5074
5075 let result = metadata.merge_with_description(Some("Existing desc"), true);
5077 assert_eq!(result, Some("Reference Description".to_string()));
5078
5079 let result = metadata.enhance_parameter_description("userId", Some("User ID parameter"));
5081 assert_eq!(result, Some("userId: Reference Description".to_string()));
5082
5083 let result = metadata.enhance_parameter_description("userId", None);
5084 assert_eq!(result, Some("userId: Reference Description".to_string()));
5085
5086 let summary_only = ReferenceMetadata::new(Some("API Token".to_string()), None);
5088
5089 let result = summary_only.merge_with_description(Some("Generic token"), false);
5090 assert_eq!(result, Some("API Token".to_string()));
5091
5092 let result = summary_only.merge_with_description(Some("Different desc"), true);
5093 assert_eq!(result, Some("API Token".to_string())); let result = summary_only.enhance_parameter_description("token", Some("Token field"));
5096 assert_eq!(result, Some("token: API Token".to_string()));
5097
5098 let empty_meta = ReferenceMetadata::new(None, None);
5100
5101 let result = empty_meta.merge_with_description(Some("Schema description"), false);
5102 assert_eq!(result, Some("Schema description".to_string()));
5103
5104 let result = empty_meta.enhance_parameter_description("param", Some("Schema param"));
5105 assert_eq!(result, Some("Schema param".to_string()));
5106
5107 let result = empty_meta.enhance_parameter_description("param", None);
5108 assert_eq!(result, Some("param parameter".to_string()));
5109 }
5110
5111 #[test]
5112 fn test_parameter_schema_with_reference_metadata() {
5113 let mut spec = create_test_spec();
5114
5115 spec.components.as_mut().unwrap().schemas.insert(
5117 "Pet".to_string(),
5118 ObjectOrReference::Object(ObjectSchema {
5119 description: None, schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
5121 ..Default::default()
5122 }),
5123 );
5124
5125 let param_with_ref = Parameter {
5127 name: "user".to_string(),
5128 location: ParameterIn::Query,
5129 description: None,
5130 required: Some(true),
5131 deprecated: Some(false),
5132 allow_empty_value: Some(false),
5133 style: None,
5134 explode: None,
5135 allow_reserved: Some(false),
5136 schema: Some(ObjectOrReference::Ref {
5137 ref_path: "#/components/schemas/Pet".to_string(),
5138 summary: Some("Pet Reference".to_string()),
5139 description: Some("A reference to pet schema with additional context".to_string()),
5140 }),
5141 example: None,
5142 examples: BTreeMap::new(),
5143 content: None,
5144 extensions: Default::default(),
5145 };
5146
5147 let result = ToolGenerator::convert_parameter_schema(
5149 ¶m_with_ref,
5150 ParameterIn::Query,
5151 &spec,
5152 false,
5153 );
5154
5155 assert!(result.is_ok());
5156 let (schema, _annotations) = result.unwrap();
5157
5158 let description = schema.get("description").and_then(|v| v.as_str());
5160 assert!(description.is_some());
5161 assert!(
5163 description.unwrap().contains("Pet Reference")
5164 || description
5165 .unwrap()
5166 .contains("A reference to pet schema with additional context")
5167 );
5168 }
5169
5170 #[test]
5171 fn test_request_body_with_reference_metadata() {
5172 let spec = create_test_spec();
5173
5174 let request_body_ref = ObjectOrReference::Ref {
5176 ref_path: "#/components/requestBodies/PetBody".to_string(),
5177 summary: Some("Pet Request Body".to_string()),
5178 description: Some(
5179 "Request body containing pet information for API operations".to_string(),
5180 ),
5181 };
5182
5183 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body_ref, &spec);
5184
5185 assert!(result.is_ok());
5186 let schema_result = result.unwrap();
5187 assert!(schema_result.is_some());
5188
5189 let (schema, _annotations, _required) = schema_result.unwrap();
5190 let description = schema.get("description").and_then(|v| v.as_str());
5191
5192 assert!(description.is_some());
5193 assert_eq!(
5195 description.unwrap(),
5196 "Request body containing pet information for API operations"
5197 );
5198 }
5199
5200 #[test]
5201 fn test_response_schema_with_reference_metadata() {
5202 let spec = create_test_spec();
5203
5204 let mut responses = BTreeMap::new();
5206 responses.insert(
5207 "200".to_string(),
5208 ObjectOrReference::Ref {
5209 ref_path: "#/components/responses/PetResponse".to_string(),
5210 summary: Some("Successful Pet Response".to_string()),
5211 description: Some(
5212 "Response containing pet data on successful operation".to_string(),
5213 ),
5214 },
5215 );
5216 let responses_option = Some(responses);
5217
5218 let result = ToolGenerator::extract_output_schema(&responses_option, &spec);
5219
5220 assert!(result.is_ok());
5221 let schema = result.unwrap();
5222 assert!(schema.is_some());
5223
5224 let schema_value = schema.unwrap();
5225 let body_desc = schema_value
5226 .get("properties")
5227 .and_then(|props| props.get("body"))
5228 .and_then(|body| body.get("description"))
5229 .and_then(|desc| desc.as_str());
5230
5231 assert!(body_desc.is_some());
5232 assert_eq!(
5234 body_desc.unwrap(),
5235 "Response containing pet data on successful operation"
5236 );
5237 }
5238}