1use schemars::schema_for;
108use serde::{Serialize, Serializer};
109use serde_json::{Value, json};
110use std::collections::{BTreeMap, HashMap, HashSet};
111
112use crate::error::{
113 Error, ErrorResponse, ToolCallValidationError, ValidationConstraint, ValidationError,
114};
115use crate::tool::ToolMetadata;
116use oas3::spec::{
117 BooleanSchema, ObjectOrReference, ObjectSchema, Operation, Parameter, ParameterIn,
118 ParameterStyle, RequestBody, Response, Schema, SchemaType, SchemaTypeSet, Spec,
119};
120use tracing::{trace, warn};
121
122const X_LOCATION: &str = "x-location";
124const X_PARAMETER_LOCATION: &str = "x-parameter-location";
125const X_PARAMETER_REQUIRED: &str = "x-parameter-required";
126const X_CONTENT_TYPE: &str = "x-content-type";
127const X_ORIGINAL_NAME: &str = "x-original-name";
128const X_PARAMETER_EXPLODE: &str = "x-parameter-explode";
129
130#[derive(Debug, Clone, Copy, PartialEq)]
132pub enum Location {
133 Parameter(ParameterIn),
135 Body,
137}
138
139impl Serialize for Location {
140 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
141 where
142 S: Serializer,
143 {
144 let str_value = match self {
145 Location::Parameter(param_in) => match param_in {
146 ParameterIn::Query => "query",
147 ParameterIn::Header => "header",
148 ParameterIn::Path => "path",
149 ParameterIn::Cookie => "cookie",
150 },
151 Location::Body => "body",
152 };
153 serializer.serialize_str(str_value)
154 }
155}
156
157#[derive(Debug, Clone, PartialEq)]
159pub enum Annotation {
160 Location(Location),
162 Required(bool),
164 ContentType(String),
166 OriginalName(String),
168 Explode(bool),
170}
171
172#[derive(Debug, Clone, Default)]
174pub struct Annotations {
175 annotations: Vec<Annotation>,
176}
177
178impl Annotations {
179 pub fn new() -> Self {
181 Self {
182 annotations: Vec::new(),
183 }
184 }
185
186 pub fn with_location(mut self, location: Location) -> Self {
188 self.annotations.push(Annotation::Location(location));
189 self
190 }
191
192 pub fn with_required(mut self, required: bool) -> Self {
194 self.annotations.push(Annotation::Required(required));
195 self
196 }
197
198 pub fn with_content_type(mut self, content_type: String) -> Self {
200 self.annotations.push(Annotation::ContentType(content_type));
201 self
202 }
203
204 pub fn with_original_name(mut self, original_name: String) -> Self {
206 self.annotations
207 .push(Annotation::OriginalName(original_name));
208 self
209 }
210
211 pub fn with_explode(mut self, explode: bool) -> Self {
213 self.annotations.push(Annotation::Explode(explode));
214 self
215 }
216}
217
218impl Serialize for Annotations {
219 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
220 where
221 S: Serializer,
222 {
223 use serde::ser::SerializeMap;
224
225 let mut map = serializer.serialize_map(Some(self.annotations.len()))?;
226
227 for annotation in &self.annotations {
228 match annotation {
229 Annotation::Location(location) => {
230 let key = match location {
232 Location::Parameter(param_in) => match param_in {
233 ParameterIn::Header | ParameterIn::Cookie => X_LOCATION,
234 _ => X_PARAMETER_LOCATION,
235 },
236 Location::Body => X_LOCATION,
237 };
238 map.serialize_entry(key, &location)?;
239
240 if let Location::Parameter(_) = location {
242 map.serialize_entry(X_PARAMETER_LOCATION, &location)?;
243 }
244 }
245 Annotation::Required(required) => {
246 map.serialize_entry(X_PARAMETER_REQUIRED, required)?;
247 }
248 Annotation::ContentType(content_type) => {
249 map.serialize_entry(X_CONTENT_TYPE, content_type)?;
250 }
251 Annotation::OriginalName(original_name) => {
252 map.serialize_entry(X_ORIGINAL_NAME, original_name)?;
253 }
254 Annotation::Explode(explode) => {
255 map.serialize_entry(X_PARAMETER_EXPLODE, explode)?;
256 }
257 }
258 }
259
260 map.end()
261 }
262}
263
264fn sanitize_property_name(name: &str) -> String {
273 let sanitized = name
275 .chars()
276 .map(|c| match c {
277 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '.' | '-' => c,
278 _ => '_',
279 })
280 .take(64)
281 .collect::<String>();
282
283 let mut collapsed = String::with_capacity(sanitized.len());
285 let mut prev_was_underscore = false;
286
287 for ch in sanitized.chars() {
288 if ch == '_' {
289 if !prev_was_underscore {
290 collapsed.push(ch);
291 }
292 prev_was_underscore = true;
293 } else {
294 collapsed.push(ch);
295 prev_was_underscore = false;
296 }
297 }
298
299 let trimmed = collapsed.trim_end_matches('_');
301
302 if trimmed.is_empty() || trimmed.chars().next().unwrap_or('0').is_numeric() {
304 format!("param_{trimmed}")
305 } else {
306 trimmed.to_string()
307 }
308}
309
310#[derive(Debug, Clone, Default)]
374pub struct ReferenceMetadata {
375 pub summary: Option<String>,
382
383 pub description: Option<String>,
390}
391
392impl ReferenceMetadata {
393 pub fn new(summary: Option<String>, description: Option<String>) -> Self {
395 Self {
396 summary,
397 description,
398 }
399 }
400
401 pub fn is_empty(&self) -> bool {
403 self.summary.is_none() && self.description.is_none()
404 }
405
406 pub fn best_description(&self) -> Option<&str> {
456 self.description.as_deref().or(self.summary.as_deref())
457 }
458
459 pub fn summary(&self) -> Option<&str> {
506 self.summary.as_deref()
507 }
508
509 pub fn merge_with_description(
595 &self,
596 existing_desc: Option<&str>,
597 prepend_summary: bool,
598 ) -> Option<String> {
599 match (self.best_description(), self.summary(), existing_desc) {
600 (Some(ref_desc), _, _) => Some(ref_desc.to_string()),
602
603 (None, Some(ref_summary), Some(existing)) if prepend_summary => {
605 if ref_summary != existing {
606 Some(format!("{}\n\n{}", ref_summary, existing))
607 } else {
608 Some(existing.to_string())
609 }
610 }
611 (None, Some(ref_summary), _) => Some(ref_summary.to_string()),
612
613 (None, None, Some(existing)) => Some(existing.to_string()),
615
616 (None, None, None) => None,
618 }
619 }
620
621 pub fn enhance_parameter_description(
707 &self,
708 param_name: &str,
709 existing_desc: Option<&str>,
710 ) -> Option<String> {
711 match (self.best_description(), self.summary(), existing_desc) {
712 (Some(ref_desc), _, _) => Some(format!("{}: {}", param_name, ref_desc)),
714
715 (None, Some(ref_summary), _) => Some(format!("{}: {}", param_name, ref_summary)),
717
718 (None, None, Some(existing)) => Some(existing.to_string()),
720
721 (None, None, None) => Some(format!("{} parameter", param_name)),
723 }
724 }
725}
726
727pub struct ToolGenerator;
729
730impl ToolGenerator {
731 pub fn generate_tool_metadata(
737 operation: &Operation,
738 method: String,
739 path: String,
740 spec: &Spec,
741 skip_tool_description: bool,
742 skip_parameter_descriptions: bool,
743 ) -> Result<ToolMetadata, Error> {
744 let name = operation.operation_id.clone().unwrap_or_else(|| {
745 format!(
746 "{}_{}",
747 method,
748 path.replace('/', "_").replace(['{', '}'], "")
749 )
750 });
751
752 let parameters = Self::generate_parameter_schema(
754 &operation.parameters,
755 &method,
756 &operation.request_body,
757 spec,
758 skip_parameter_descriptions,
759 )?;
760
761 let description =
763 (!skip_tool_description).then(|| Self::build_description(operation, &method, &path));
764
765 let output_schema = Self::extract_output_schema(&operation.responses, spec)?;
767
768 Ok(ToolMetadata {
769 name,
770 title: operation.summary.clone(),
771 description,
772 parameters,
773 output_schema,
774 method,
775 path,
776 security: None, })
778 }
779
780 pub fn generate_openapi_tools(
786 tools_metadata: Vec<ToolMetadata>,
787 base_url: Option<url::Url>,
788 default_headers: Option<reqwest::header::HeaderMap>,
789 ) -> Result<Vec<crate::tool::Tool>, Error> {
790 let mut openapi_tools = Vec::with_capacity(tools_metadata.len());
791
792 for metadata in tools_metadata {
793 let tool = crate::tool::Tool::new(metadata, base_url.clone(), default_headers.clone())?;
794 openapi_tools.push(tool);
795 }
796
797 Ok(openapi_tools)
798 }
799
800 fn build_description(operation: &Operation, method: &str, path: &str) -> String {
802 match (&operation.summary, &operation.description) {
803 (Some(summary), Some(desc)) => {
804 format!(
805 "{}\n\n{}\n\nEndpoint: {} {}",
806 summary,
807 desc,
808 method.to_uppercase(),
809 path
810 )
811 }
812 (Some(summary), None) => {
813 format!(
814 "{}\n\nEndpoint: {} {}",
815 summary,
816 method.to_uppercase(),
817 path
818 )
819 }
820 (None, Some(desc)) => {
821 format!("{}\n\nEndpoint: {} {}", desc, method.to_uppercase(), path)
822 }
823 (None, None) => {
824 format!("API endpoint: {} {}", method.to_uppercase(), path)
825 }
826 }
827 }
828
829 fn extract_output_schema(
833 responses: &Option<BTreeMap<String, ObjectOrReference<Response>>>,
834 spec: &Spec,
835 ) -> Result<Option<Value>, Error> {
836 let responses = match responses {
837 Some(r) => r,
838 None => return Ok(None),
839 };
840 let priority_codes = vec![
842 "200", "201", "202", "203", "204", "2XX", "default", ];
850
851 for status_code in priority_codes {
852 if let Some(response_or_ref) = responses.get(status_code) {
853 let response = match response_or_ref {
855 ObjectOrReference::Object(response) => response,
856 ObjectOrReference::Ref {
857 ref_path,
858 summary,
859 description,
860 } => {
861 let ref_metadata =
864 ReferenceMetadata::new(summary.clone(), description.clone());
865
866 if let Some(ref_desc) = ref_metadata.best_description() {
867 let response_schema = json!({
869 "type": "object",
870 "description": "Unified response structure with success and error variants",
871 "properties": {
872 "status_code": {
873 "type": "integer",
874 "description": "HTTP status code"
875 },
876 "body": {
877 "type": "object",
878 "description": ref_desc,
879 "additionalProperties": true
880 }
881 },
882 "required": ["status_code", "body"]
883 });
884
885 trace!(
886 reference_path = %ref_path,
887 reference_description = %ref_desc,
888 "Created response schema using reference metadata"
889 );
890
891 return Ok(Some(response_schema));
892 }
893
894 continue;
896 }
897 };
898
899 if status_code == "204" {
901 continue;
902 }
903
904 if !response.content.is_empty() {
906 let content = &response.content;
907 let json_media_types = vec![
909 "application/json",
910 "application/ld+json",
911 "application/vnd.api+json",
912 ];
913
914 for media_type_str in json_media_types {
915 if let Some(media_type) = content.get(media_type_str)
916 && let Some(schema_or_ref) = &media_type.schema
917 {
918 let wrapped_schema = Self::wrap_output_schema(schema_or_ref, spec)?;
920 return Ok(Some(wrapped_schema));
921 }
922 }
923
924 for media_type in content.values() {
926 if let Some(schema_or_ref) = &media_type.schema {
927 let wrapped_schema = Self::wrap_output_schema(schema_or_ref, spec)?;
929 return Ok(Some(wrapped_schema));
930 }
931 }
932 }
933 }
934 }
935
936 Ok(None)
938 }
939
940 fn convert_schema_to_json_schema(
950 schema: &Schema,
951 spec: &Spec,
952 visited: &mut HashSet<String>,
953 ) -> Result<Value, Error> {
954 match schema {
955 Schema::Object(obj_schema_or_ref) => match obj_schema_or_ref.as_ref() {
956 ObjectOrReference::Object(obj_schema) => {
957 Self::convert_object_schema_to_json_schema(obj_schema, spec, visited)
958 }
959 ObjectOrReference::Ref { ref_path, .. } => {
960 let resolved = Self::resolve_reference(ref_path, spec, visited)?;
961 Self::convert_object_schema_to_json_schema(&resolved, spec, visited)
962 }
963 },
964 Schema::Boolean(bool_schema) => {
965 if bool_schema.0 {
967 Ok(json!({})) } else {
969 Ok(json!({"not": {}})) }
971 }
972 }
973 }
974
975 fn convert_object_schema_to_json_schema(
985 obj_schema: &ObjectSchema,
986 spec: &Spec,
987 visited: &mut HashSet<String>,
988 ) -> Result<Value, Error> {
989 let mut schema_obj = serde_json::Map::new();
990
991 if let Some(schema_type) = &obj_schema.schema_type {
993 match schema_type {
994 SchemaTypeSet::Single(single_type) => {
995 schema_obj.insert(
996 "type".to_string(),
997 json!(Self::schema_type_to_string(single_type)),
998 );
999 }
1000 SchemaTypeSet::Multiple(type_set) => {
1001 let types: Vec<String> =
1002 type_set.iter().map(Self::schema_type_to_string).collect();
1003 schema_obj.insert("type".to_string(), json!(types));
1004 }
1005 }
1006 }
1007
1008 if let Some(desc) = &obj_schema.description {
1010 schema_obj.insert("description".to_string(), json!(desc));
1011 }
1012
1013 if !obj_schema.one_of.is_empty() {
1015 let mut one_of_schemas = Vec::new();
1016 for schema_ref in &obj_schema.one_of {
1017 let schema_json = match schema_ref {
1018 ObjectOrReference::Object(schema) => {
1019 Self::convert_object_schema_to_json_schema(schema, spec, visited)?
1020 }
1021 ObjectOrReference::Ref { ref_path, .. } => {
1022 let resolved = Self::resolve_reference(ref_path, spec, visited)?;
1023 Self::convert_object_schema_to_json_schema(&resolved, spec, visited)?
1024 }
1025 };
1026 one_of_schemas.push(schema_json);
1027 }
1028 schema_obj.insert("oneOf".to_string(), json!(one_of_schemas));
1029 return Ok(Value::Object(schema_obj));
1032 }
1033
1034 if !obj_schema.properties.is_empty() {
1036 let properties = &obj_schema.properties;
1037 let mut props_map = serde_json::Map::new();
1038 for (prop_name, prop_schema_or_ref) in properties {
1039 let prop_schema = match prop_schema_or_ref {
1040 ObjectOrReference::Object(schema) => {
1041 Self::convert_schema_to_json_schema(
1043 &Schema::Object(Box::new(ObjectOrReference::Object(schema.clone()))),
1044 spec,
1045 visited,
1046 )?
1047 }
1048 ObjectOrReference::Ref { ref_path, .. } => {
1049 let resolved = Self::resolve_reference(ref_path, spec, visited)?;
1050 Self::convert_object_schema_to_json_schema(&resolved, spec, visited)?
1051 }
1052 };
1053
1054 let sanitized_name = sanitize_property_name(prop_name);
1056 if sanitized_name != *prop_name {
1057 let annotations = Annotations::new().with_original_name(prop_name.clone());
1059 let prop_with_annotation =
1060 Self::apply_annotations_to_schema(prop_schema, annotations);
1061 props_map.insert(sanitized_name, prop_with_annotation);
1062 } else {
1063 props_map.insert(prop_name.clone(), prop_schema);
1064 }
1065 }
1066 schema_obj.insert("properties".to_string(), Value::Object(props_map));
1067 }
1068
1069 if !obj_schema.required.is_empty() {
1071 schema_obj.insert("required".to_string(), json!(&obj_schema.required));
1072 }
1073
1074 if let Some(schema_type) = &obj_schema.schema_type
1076 && matches!(schema_type, SchemaTypeSet::Single(SchemaType::Object))
1077 {
1078 match &obj_schema.additional_properties {
1080 None => {
1081 schema_obj.insert("additionalProperties".to_string(), json!(true));
1083 }
1084 Some(Schema::Boolean(BooleanSchema(value))) => {
1085 schema_obj.insert("additionalProperties".to_string(), json!(value));
1087 }
1088 Some(Schema::Object(schema_ref)) => {
1089 let mut visited = HashSet::new();
1091 let additional_props_schema = Self::convert_schema_to_json_schema(
1092 &Schema::Object(schema_ref.clone()),
1093 spec,
1094 &mut visited,
1095 )?;
1096 schema_obj.insert("additionalProperties".to_string(), additional_props_schema);
1097 }
1098 }
1099 }
1100
1101 if let Some(schema_type) = &obj_schema.schema_type {
1103 if matches!(schema_type, SchemaTypeSet::Single(SchemaType::Array)) {
1104 if !obj_schema.prefix_items.is_empty() {
1106 Self::convert_prefix_items_to_draft07(
1108 &obj_schema.prefix_items,
1109 &obj_schema.items,
1110 &mut schema_obj,
1111 spec,
1112 )?;
1113 } else if let Some(items_schema) = &obj_schema.items {
1114 let items_json =
1116 Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
1117 schema_obj.insert("items".to_string(), items_json);
1118 }
1119
1120 if let Some(min_items) = obj_schema.min_items {
1122 schema_obj.insert("minItems".to_string(), json!(min_items));
1123 }
1124 if let Some(max_items) = obj_schema.max_items {
1125 schema_obj.insert("maxItems".to_string(), json!(max_items));
1126 }
1127 } else if let Some(items_schema) = &obj_schema.items {
1128 let items_json = Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
1130 schema_obj.insert("items".to_string(), items_json);
1131 }
1132 }
1133
1134 if let Some(format) = &obj_schema.format {
1136 schema_obj.insert("format".to_string(), json!(format));
1137 }
1138
1139 if let Some(example) = &obj_schema.example {
1140 schema_obj.insert("example".to_string(), example.clone());
1141 }
1142
1143 if let Some(default) = &obj_schema.default {
1144 schema_obj.insert("default".to_string(), default.clone());
1145 }
1146
1147 if !obj_schema.enum_values.is_empty() {
1148 schema_obj.insert("enum".to_string(), json!(&obj_schema.enum_values));
1149 }
1150
1151 if let Some(min) = &obj_schema.minimum {
1152 schema_obj.insert("minimum".to_string(), json!(min));
1153 }
1154
1155 if let Some(max) = &obj_schema.maximum {
1156 schema_obj.insert("maximum".to_string(), json!(max));
1157 }
1158
1159 if let Some(min_length) = &obj_schema.min_length {
1160 schema_obj.insert("minLength".to_string(), json!(min_length));
1161 }
1162
1163 if let Some(max_length) = &obj_schema.max_length {
1164 schema_obj.insert("maxLength".to_string(), json!(max_length));
1165 }
1166
1167 if let Some(pattern) = &obj_schema.pattern {
1168 schema_obj.insert("pattern".to_string(), json!(pattern));
1169 }
1170
1171 Ok(Value::Object(schema_obj))
1172 }
1173
1174 fn schema_type_to_string(schema_type: &SchemaType) -> String {
1176 match schema_type {
1177 SchemaType::Boolean => "boolean",
1178 SchemaType::Integer => "integer",
1179 SchemaType::Number => "number",
1180 SchemaType::String => "string",
1181 SchemaType::Array => "array",
1182 SchemaType::Object => "object",
1183 SchemaType::Null => "null",
1184 }
1185 .to_string()
1186 }
1187
1188 fn resolve_reference(
1198 ref_path: &str,
1199 spec: &Spec,
1200 visited: &mut HashSet<String>,
1201 ) -> Result<ObjectSchema, Error> {
1202 if visited.contains(ref_path) {
1204 return Err(Error::ToolGeneration(format!(
1205 "Circular reference detected: {ref_path}"
1206 )));
1207 }
1208
1209 visited.insert(ref_path.to_string());
1211
1212 if !ref_path.starts_with("#/components/schemas/") {
1215 return Err(Error::ToolGeneration(format!(
1216 "Unsupported reference format: {ref_path}. Only #/components/schemas/ references are supported"
1217 )));
1218 }
1219
1220 let schema_name = ref_path.strip_prefix("#/components/schemas/").unwrap();
1221
1222 let components = spec.components.as_ref().ok_or_else(|| {
1224 Error::ToolGeneration(format!(
1225 "Reference {ref_path} points to components, but spec has no components section"
1226 ))
1227 })?;
1228
1229 let schema_ref = components.schemas.get(schema_name).ok_or_else(|| {
1230 Error::ToolGeneration(format!(
1231 "Schema '{schema_name}' not found in components/schemas"
1232 ))
1233 })?;
1234
1235 let resolved_schema = match schema_ref {
1237 ObjectOrReference::Object(obj_schema) => obj_schema.clone(),
1238 ObjectOrReference::Ref {
1239 ref_path: nested_ref,
1240 ..
1241 } => {
1242 Self::resolve_reference(nested_ref, spec, visited)?
1244 }
1245 };
1246
1247 visited.remove(ref_path);
1249
1250 Ok(resolved_schema)
1251 }
1252
1253 fn resolve_reference_with_metadata(
1258 ref_path: &str,
1259 summary: Option<String>,
1260 description: Option<String>,
1261 spec: &Spec,
1262 visited: &mut HashSet<String>,
1263 ) -> Result<(ObjectSchema, ReferenceMetadata), Error> {
1264 let resolved_schema = Self::resolve_reference(ref_path, spec, visited)?;
1265 let metadata = ReferenceMetadata::new(summary, description);
1266 Ok((resolved_schema, metadata))
1267 }
1268
1269 fn generate_parameter_schema(
1271 parameters: &[ObjectOrReference<Parameter>],
1272 _method: &str,
1273 request_body: &Option<ObjectOrReference<RequestBody>>,
1274 spec: &Spec,
1275 skip_parameter_descriptions: bool,
1276 ) -> Result<Value, Error> {
1277 let mut properties = serde_json::Map::new();
1278 let mut required = Vec::new();
1279
1280 let mut path_params = Vec::new();
1282 let mut query_params = Vec::new();
1283 let mut header_params = Vec::new();
1284 let mut cookie_params = Vec::new();
1285
1286 for param_ref in parameters {
1287 let param = match param_ref {
1288 ObjectOrReference::Object(param) => param,
1289 ObjectOrReference::Ref { ref_path, .. } => {
1290 warn!(
1294 reference_path = %ref_path,
1295 "Parameter reference not resolved"
1296 );
1297 continue;
1298 }
1299 };
1300
1301 match ¶m.location {
1302 ParameterIn::Query => query_params.push(param),
1303 ParameterIn::Header => header_params.push(param),
1304 ParameterIn::Path => path_params.push(param),
1305 ParameterIn::Cookie => cookie_params.push(param),
1306 }
1307 }
1308
1309 for param in path_params {
1311 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1312 param,
1313 ParameterIn::Path,
1314 spec,
1315 skip_parameter_descriptions,
1316 )?;
1317
1318 let sanitized_name = sanitize_property_name(¶m.name);
1320 if sanitized_name != param.name {
1321 annotations = annotations.with_original_name(param.name.clone());
1322 }
1323
1324 let param_schema_with_annotations =
1325 Self::apply_annotations_to_schema(param_schema, annotations);
1326 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
1327 required.push(sanitized_name);
1328 }
1329
1330 for param in &query_params {
1332 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1333 param,
1334 ParameterIn::Query,
1335 spec,
1336 skip_parameter_descriptions,
1337 )?;
1338
1339 let sanitized_name = sanitize_property_name(¶m.name);
1341 if sanitized_name != param.name {
1342 annotations = annotations.with_original_name(param.name.clone());
1343 }
1344
1345 let param_schema_with_annotations =
1346 Self::apply_annotations_to_schema(param_schema, annotations);
1347 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
1348 if param.required.unwrap_or(false) {
1349 required.push(sanitized_name);
1350 }
1351 }
1352
1353 for param in &header_params {
1355 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1356 param,
1357 ParameterIn::Header,
1358 spec,
1359 skip_parameter_descriptions,
1360 )?;
1361
1362 let prefixed_name = format!("header_{}", param.name);
1364 let sanitized_name = sanitize_property_name(&prefixed_name);
1365 if sanitized_name != prefixed_name {
1366 annotations = annotations.with_original_name(param.name.clone());
1367 }
1368
1369 let param_schema_with_annotations =
1370 Self::apply_annotations_to_schema(param_schema, annotations);
1371
1372 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
1373 if param.required.unwrap_or(false) {
1374 required.push(sanitized_name);
1375 }
1376 }
1377
1378 for param in &cookie_params {
1380 let (param_schema, mut annotations) = Self::convert_parameter_schema(
1381 param,
1382 ParameterIn::Cookie,
1383 spec,
1384 skip_parameter_descriptions,
1385 )?;
1386
1387 let prefixed_name = format!("cookie_{}", param.name);
1389 let sanitized_name = sanitize_property_name(&prefixed_name);
1390 if sanitized_name != prefixed_name {
1391 annotations = annotations.with_original_name(param.name.clone());
1392 }
1393
1394 let param_schema_with_annotations =
1395 Self::apply_annotations_to_schema(param_schema, annotations);
1396
1397 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
1398 if param.required.unwrap_or(false) {
1399 required.push(sanitized_name);
1400 }
1401 }
1402
1403 if let Some(request_body) = request_body
1405 && let Some((body_schema, annotations, is_required)) =
1406 Self::convert_request_body_to_json_schema(request_body, spec)?
1407 {
1408 let body_schema_with_annotations =
1409 Self::apply_annotations_to_schema(body_schema, annotations);
1410 properties.insert("request_body".to_string(), body_schema_with_annotations);
1411 if is_required {
1412 required.push("request_body".to_string());
1413 }
1414 }
1415
1416 if !query_params.is_empty() || !header_params.is_empty() || !cookie_params.is_empty() {
1418 properties.insert(
1420 "timeout_seconds".to_string(),
1421 json!({
1422 "type": "integer",
1423 "description": "Request timeout in seconds",
1424 "minimum": 1,
1425 "maximum": 300,
1426 "default": 30
1427 }),
1428 );
1429 }
1430
1431 Ok(json!({
1432 "type": "object",
1433 "properties": properties,
1434 "required": required,
1435 "additionalProperties": false
1436 }))
1437 }
1438
1439 fn convert_parameter_schema(
1441 param: &Parameter,
1442 location: ParameterIn,
1443 spec: &Spec,
1444 skip_parameter_descriptions: bool,
1445 ) -> Result<(Value, Annotations), Error> {
1446 let base_schema = if let Some(schema_ref) = ¶m.schema {
1448 match schema_ref {
1449 ObjectOrReference::Object(obj_schema) => {
1450 let mut visited = HashSet::new();
1451 Self::convert_schema_to_json_schema(
1452 &Schema::Object(Box::new(ObjectOrReference::Object(obj_schema.clone()))),
1453 spec,
1454 &mut visited,
1455 )?
1456 }
1457 ObjectOrReference::Ref {
1458 ref_path,
1459 summary,
1460 description,
1461 } => {
1462 let mut visited = HashSet::new();
1464 match Self::resolve_reference_with_metadata(
1465 ref_path,
1466 summary.clone(),
1467 description.clone(),
1468 spec,
1469 &mut visited,
1470 ) {
1471 Ok((resolved_schema, ref_metadata)) => {
1472 let mut schema_json = Self::convert_schema_to_json_schema(
1473 &Schema::Object(Box::new(ObjectOrReference::Object(
1474 resolved_schema,
1475 ))),
1476 spec,
1477 &mut visited,
1478 )?;
1479
1480 if let Value::Object(ref mut schema_obj) = schema_json {
1482 if let Some(ref_desc) = ref_metadata.best_description() {
1484 schema_obj.insert("description".to_string(), json!(ref_desc));
1485 }
1486 }
1489
1490 schema_json
1491 }
1492 Err(_) => {
1493 json!({"type": "string"})
1495 }
1496 }
1497 }
1498 }
1499 } else {
1500 json!({"type": "string"})
1502 };
1503
1504 let mut result = match base_schema {
1506 Value::Object(obj) => obj,
1507 _ => {
1508 return Err(Error::ToolGeneration(format!(
1510 "Internal error: schema converter returned non-object for parameter '{}'",
1511 param.name
1512 )));
1513 }
1514 };
1515
1516 let mut collected_examples = Vec::new();
1518
1519 if let Some(example) = ¶m.example {
1521 collected_examples.push(example.clone());
1522 } else if !param.examples.is_empty() {
1523 for example_ref in param.examples.values() {
1525 match example_ref {
1526 ObjectOrReference::Object(example_obj) => {
1527 if let Some(value) = &example_obj.value {
1528 collected_examples.push(value.clone());
1529 }
1530 }
1531 ObjectOrReference::Ref { .. } => {
1532 }
1534 }
1535 }
1536 } else if let Some(Value::String(ex_str)) = result.get("example") {
1537 collected_examples.push(json!(ex_str));
1539 } else if let Some(ex) = result.get("example") {
1540 collected_examples.push(ex.clone());
1541 }
1542
1543 let base_description = param
1545 .description
1546 .as_ref()
1547 .map(|d| d.to_string())
1548 .or_else(|| {
1549 result
1550 .get("description")
1551 .and_then(|d| d.as_str())
1552 .map(|d| d.to_string())
1553 })
1554 .unwrap_or_else(|| format!("{} parameter", param.name));
1555
1556 let description_with_examples = if let Some(examples_str) =
1557 Self::format_examples_for_description(&collected_examples)
1558 {
1559 format!("{base_description}. {examples_str}")
1560 } else {
1561 base_description
1562 };
1563
1564 if !skip_parameter_descriptions {
1565 result.insert("description".to_string(), json!(description_with_examples));
1566 }
1567
1568 if let Some(example) = ¶m.example {
1573 result.insert("example".to_string(), example.clone());
1574 } else if !param.examples.is_empty() {
1575 let mut examples_array = Vec::new();
1578 for (example_name, example_ref) in ¶m.examples {
1579 match example_ref {
1580 ObjectOrReference::Object(example_obj) => {
1581 if let Some(value) = &example_obj.value {
1582 examples_array.push(json!({
1583 "name": example_name,
1584 "value": value
1585 }));
1586 }
1587 }
1588 ObjectOrReference::Ref { .. } => {
1589 }
1592 }
1593 }
1594
1595 if !examples_array.is_empty() {
1596 if let Some(first_example) = examples_array.first()
1598 && let Some(value) = first_example.get("value")
1599 {
1600 result.insert("example".to_string(), value.clone());
1601 }
1602 result.insert("x-examples".to_string(), json!(examples_array));
1604 }
1605 }
1606
1607 let mut annotations = Annotations::new()
1609 .with_location(Location::Parameter(location))
1610 .with_required(param.required.unwrap_or(false));
1611
1612 if let Some(explode) = param.explode {
1614 annotations = annotations.with_explode(explode);
1615 } else {
1616 let default_explode = match ¶m.style {
1620 Some(ParameterStyle::Form) | None => true, _ => false,
1622 };
1623 annotations = annotations.with_explode(default_explode);
1624 }
1625
1626 Ok((Value::Object(result), annotations))
1627 }
1628
1629 fn apply_annotations_to_schema(schema: Value, annotations: Annotations) -> Value {
1631 match schema {
1632 Value::Object(mut obj) => {
1633 if let Ok(Value::Object(ann_map)) = serde_json::to_value(&annotations) {
1635 for (key, value) in ann_map {
1636 obj.insert(key, value);
1637 }
1638 }
1639 Value::Object(obj)
1640 }
1641 _ => schema,
1642 }
1643 }
1644
1645 fn format_examples_for_description(examples: &[Value]) -> Option<String> {
1647 if examples.is_empty() {
1648 return None;
1649 }
1650
1651 if examples.len() == 1 {
1652 let example_str =
1653 serde_json::to_string(&examples[0]).unwrap_or_else(|_| "null".to_string());
1654 Some(format!("Example: `{example_str}`"))
1655 } else {
1656 let mut result = String::from("Examples:\n");
1657 for ex in examples {
1658 let json_str = serde_json::to_string(ex).unwrap_or_else(|_| "null".to_string());
1659 result.push_str(&format!("- `{json_str}`\n"));
1660 }
1661 result.pop();
1663 Some(result)
1664 }
1665 }
1666
1667 fn convert_prefix_items_to_draft07(
1678 prefix_items: &[ObjectOrReference<ObjectSchema>],
1679 items: &Option<Box<Schema>>,
1680 result: &mut serde_json::Map<String, Value>,
1681 spec: &Spec,
1682 ) -> Result<(), Error> {
1683 let prefix_count = prefix_items.len();
1684
1685 let mut item_types = Vec::new();
1687 for prefix_item in prefix_items {
1688 match prefix_item {
1689 ObjectOrReference::Object(obj_schema) => {
1690 if let Some(schema_type) = &obj_schema.schema_type {
1691 match schema_type {
1692 SchemaTypeSet::Single(SchemaType::String) => item_types.push("string"),
1693 SchemaTypeSet::Single(SchemaType::Integer) => {
1694 item_types.push("integer")
1695 }
1696 SchemaTypeSet::Single(SchemaType::Number) => item_types.push("number"),
1697 SchemaTypeSet::Single(SchemaType::Boolean) => {
1698 item_types.push("boolean")
1699 }
1700 SchemaTypeSet::Single(SchemaType::Array) => item_types.push("array"),
1701 SchemaTypeSet::Single(SchemaType::Object) => item_types.push("object"),
1702 _ => item_types.push("string"), }
1704 } else {
1705 item_types.push("string"); }
1707 }
1708 ObjectOrReference::Ref { ref_path, .. } => {
1709 let mut visited = HashSet::new();
1711 match Self::resolve_reference(ref_path, spec, &mut visited) {
1712 Ok(resolved_schema) => {
1713 if let Some(schema_type_set) = &resolved_schema.schema_type {
1715 match schema_type_set {
1716 SchemaTypeSet::Single(SchemaType::String) => {
1717 item_types.push("string")
1718 }
1719 SchemaTypeSet::Single(SchemaType::Integer) => {
1720 item_types.push("integer")
1721 }
1722 SchemaTypeSet::Single(SchemaType::Number) => {
1723 item_types.push("number")
1724 }
1725 SchemaTypeSet::Single(SchemaType::Boolean) => {
1726 item_types.push("boolean")
1727 }
1728 SchemaTypeSet::Single(SchemaType::Array) => {
1729 item_types.push("array")
1730 }
1731 SchemaTypeSet::Single(SchemaType::Object) => {
1732 item_types.push("object")
1733 }
1734 _ => item_types.push("string"), }
1736 } else {
1737 item_types.push("string"); }
1739 }
1740 Err(_) => {
1741 item_types.push("string");
1743 }
1744 }
1745 }
1746 }
1747 }
1748
1749 let items_is_false =
1751 matches!(items.as_ref().map(|i| i.as_ref()), Some(Schema::Boolean(b)) if !b.0);
1752
1753 if items_is_false {
1754 result.insert("minItems".to_string(), json!(prefix_count));
1756 result.insert("maxItems".to_string(), json!(prefix_count));
1757 }
1758
1759 let unique_types: std::collections::BTreeSet<_> = item_types.into_iter().collect();
1761
1762 if unique_types.len() == 1 {
1763 let item_type = unique_types.into_iter().next().unwrap();
1765 result.insert("items".to_string(), json!({"type": item_type}));
1766 } else if unique_types.len() > 1 {
1767 let one_of: Vec<Value> = unique_types
1769 .into_iter()
1770 .map(|t| json!({"type": t}))
1771 .collect();
1772 result.insert("items".to_string(), json!({"oneOf": one_of}));
1773 }
1774
1775 Ok(())
1776 }
1777
1778 fn convert_request_body_to_json_schema(
1790 request_body_ref: &ObjectOrReference<RequestBody>,
1791 spec: &Spec,
1792 ) -> Result<Option<(Value, Annotations, bool)>, Error> {
1793 match request_body_ref {
1794 ObjectOrReference::Object(request_body) => {
1795 let schema_info = request_body
1798 .content
1799 .get(mime::APPLICATION_JSON.as_ref())
1800 .or_else(|| request_body.content.get("application/json"))
1801 .or_else(|| {
1802 request_body.content.values().next()
1804 });
1805
1806 if let Some(media_type) = schema_info {
1807 if let Some(schema_ref) = &media_type.schema {
1808 let schema = Schema::Object(Box::new(schema_ref.clone()));
1810
1811 let mut visited = HashSet::new();
1813 let converted_schema =
1814 Self::convert_schema_to_json_schema(&schema, spec, &mut visited)?;
1815
1816 let mut schema_obj = match converted_schema {
1818 Value::Object(obj) => obj,
1819 _ => {
1820 let mut obj = serde_json::Map::new();
1822 obj.insert("type".to_string(), json!("object"));
1823 obj.insert("additionalProperties".to_string(), json!(true));
1824 obj
1825 }
1826 };
1827
1828 if !schema_obj.contains_key("description") {
1830 let description = request_body
1831 .description
1832 .clone()
1833 .unwrap_or_else(|| "Request body data".to_string());
1834 schema_obj.insert("description".to_string(), json!(description));
1835 }
1836
1837 let annotations = Annotations::new()
1839 .with_location(Location::Body)
1840 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
1841
1842 let required = request_body.required.unwrap_or(false);
1843 Ok(Some((Value::Object(schema_obj), annotations, required)))
1844 } else {
1845 Ok(None)
1846 }
1847 } else {
1848 Ok(None)
1849 }
1850 }
1851 ObjectOrReference::Ref {
1852 ref_path: _,
1853 summary,
1854 description,
1855 } => {
1856 let ref_metadata = ReferenceMetadata::new(summary.clone(), description.clone());
1858 let enhanced_description = ref_metadata
1859 .best_description()
1860 .map(|desc| desc.to_string())
1861 .unwrap_or_else(|| "Request body data".to_string());
1862
1863 let mut result = serde_json::Map::new();
1864 result.insert("type".to_string(), json!("object"));
1865 result.insert("additionalProperties".to_string(), json!(true));
1866 result.insert("description".to_string(), json!(enhanced_description));
1867
1868 let annotations = Annotations::new()
1870 .with_location(Location::Body)
1871 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
1872
1873 Ok(Some((Value::Object(result), annotations, false)))
1874 }
1875 }
1876 }
1877
1878 pub fn extract_parameters(
1884 tool_metadata: &ToolMetadata,
1885 arguments: &Value,
1886 ) -> Result<ExtractedParameters, ToolCallValidationError> {
1887 let args = arguments.as_object().ok_or_else(|| {
1888 ToolCallValidationError::RequestConstructionError {
1889 reason: "Arguments must be an object".to_string(),
1890 }
1891 })?;
1892
1893 trace!(
1894 tool_name = %tool_metadata.name,
1895 raw_arguments = ?arguments,
1896 "Starting parameter extraction"
1897 );
1898
1899 let mut path_params = HashMap::new();
1900 let mut query_params = HashMap::new();
1901 let mut header_params = HashMap::new();
1902 let mut cookie_params = HashMap::new();
1903 let mut body_params = HashMap::new();
1904 let mut config = RequestConfig::default();
1905
1906 if let Some(timeout) = args.get("timeout_seconds").and_then(Value::as_u64) {
1908 config.timeout_seconds = u32::try_from(timeout).unwrap_or(u32::MAX);
1909 }
1910
1911 for (key, value) in args {
1913 if key == "timeout_seconds" {
1914 continue; }
1916
1917 if key == "request_body" {
1919 body_params.insert("request_body".to_string(), value.clone());
1920 continue;
1921 }
1922
1923 let location = Self::get_parameter_location(tool_metadata, key).map_err(|e| {
1925 ToolCallValidationError::RequestConstructionError {
1926 reason: e.to_string(),
1927 }
1928 })?;
1929
1930 let original_name = Self::get_original_parameter_name(tool_metadata, key);
1932
1933 match location.as_str() {
1934 "path" => {
1935 path_params.insert(original_name.unwrap_or_else(|| key.clone()), value.clone());
1936 }
1937 "query" => {
1938 let param_name = original_name.unwrap_or_else(|| key.clone());
1939 let explode = Self::get_parameter_explode(tool_metadata, key);
1940 query_params.insert(param_name, QueryParameter::new(value.clone(), explode));
1941 }
1942 "header" => {
1943 let header_name = if let Some(orig) = original_name {
1945 orig
1946 } else if key.starts_with("header_") {
1947 key.strip_prefix("header_").unwrap_or(key).to_string()
1948 } else {
1949 key.clone()
1950 };
1951 header_params.insert(header_name, value.clone());
1952 }
1953 "cookie" => {
1954 let cookie_name = if let Some(orig) = original_name {
1956 orig
1957 } else if key.starts_with("cookie_") {
1958 key.strip_prefix("cookie_").unwrap_or(key).to_string()
1959 } else {
1960 key.clone()
1961 };
1962 cookie_params.insert(cookie_name, value.clone());
1963 }
1964 "body" => {
1965 let body_name = if key.starts_with("body_") {
1967 key.strip_prefix("body_").unwrap_or(key).to_string()
1968 } else {
1969 key.clone()
1970 };
1971 body_params.insert(body_name, value.clone());
1972 }
1973 _ => {
1974 return Err(ToolCallValidationError::RequestConstructionError {
1975 reason: format!("Unknown parameter location for parameter: {key}"),
1976 });
1977 }
1978 }
1979 }
1980
1981 let extracted = ExtractedParameters {
1982 path: path_params,
1983 query: query_params,
1984 headers: header_params,
1985 cookies: cookie_params,
1986 body: body_params,
1987 config,
1988 };
1989
1990 trace!(
1991 tool_name = %tool_metadata.name,
1992 extracted_parameters = ?extracted,
1993 "Parameter extraction completed"
1994 );
1995
1996 Self::validate_parameters(tool_metadata, arguments)?;
1998
1999 Ok(extracted)
2000 }
2001
2002 fn get_original_parameter_name(
2004 tool_metadata: &ToolMetadata,
2005 param_name: &str,
2006 ) -> Option<String> {
2007 tool_metadata
2008 .parameters
2009 .get("properties")
2010 .and_then(|p| p.as_object())
2011 .and_then(|props| props.get(param_name))
2012 .and_then(|schema| schema.get(X_ORIGINAL_NAME))
2013 .and_then(|v| v.as_str())
2014 .map(|s| s.to_string())
2015 }
2016
2017 fn get_parameter_explode(tool_metadata: &ToolMetadata, param_name: &str) -> bool {
2019 tool_metadata
2020 .parameters
2021 .get("properties")
2022 .and_then(|p| p.as_object())
2023 .and_then(|props| props.get(param_name))
2024 .and_then(|schema| schema.get(X_PARAMETER_EXPLODE))
2025 .and_then(|v| v.as_bool())
2026 .unwrap_or(true) }
2028
2029 fn get_parameter_location(
2031 tool_metadata: &ToolMetadata,
2032 param_name: &str,
2033 ) -> Result<String, Error> {
2034 let properties = tool_metadata
2035 .parameters
2036 .get("properties")
2037 .and_then(|p| p.as_object())
2038 .ok_or_else(|| Error::ToolGeneration("Invalid tool parameters schema".to_string()))?;
2039
2040 if let Some(param_schema) = properties.get(param_name)
2041 && let Some(location) = param_schema
2042 .get(X_PARAMETER_LOCATION)
2043 .and_then(|v| v.as_str())
2044 {
2045 return Ok(location.to_string());
2046 }
2047
2048 if param_name.starts_with("header_") {
2050 Ok("header".to_string())
2051 } else if param_name.starts_with("cookie_") {
2052 Ok("cookie".to_string())
2053 } else if param_name.starts_with("body_") {
2054 Ok("body".to_string())
2055 } else {
2056 Ok("query".to_string())
2058 }
2059 }
2060
2061 fn validate_parameters(
2063 tool_metadata: &ToolMetadata,
2064 arguments: &Value,
2065 ) -> Result<(), ToolCallValidationError> {
2066 let schema = &tool_metadata.parameters;
2067
2068 let required_params = schema
2070 .get("required")
2071 .and_then(|r| r.as_array())
2072 .map(|arr| {
2073 arr.iter()
2074 .filter_map(|v| v.as_str())
2075 .collect::<std::collections::HashSet<_>>()
2076 })
2077 .unwrap_or_default();
2078
2079 let properties = schema
2080 .get("properties")
2081 .and_then(|p| p.as_object())
2082 .ok_or_else(|| ToolCallValidationError::RequestConstructionError {
2083 reason: "Tool schema missing properties".to_string(),
2084 })?;
2085
2086 let args = arguments.as_object().ok_or_else(|| {
2087 ToolCallValidationError::RequestConstructionError {
2088 reason: "Arguments must be an object".to_string(),
2089 }
2090 })?;
2091
2092 let mut all_errors = Vec::new();
2094
2095 all_errors.extend(Self::check_unknown_parameters(args, properties));
2097
2098 all_errors.extend(Self::check_missing_required(
2100 args,
2101 properties,
2102 &required_params,
2103 ));
2104
2105 all_errors.extend(Self::validate_parameter_values(args, properties));
2107
2108 if !all_errors.is_empty() {
2110 return Err(ToolCallValidationError::InvalidParameters {
2111 violations: all_errors,
2112 });
2113 }
2114
2115 Ok(())
2116 }
2117
2118 fn check_unknown_parameters(
2120 args: &serde_json::Map<String, Value>,
2121 properties: &serde_json::Map<String, Value>,
2122 ) -> Vec<ValidationError> {
2123 let mut errors = Vec::new();
2124
2125 let valid_params: Vec<String> = properties.keys().map(|s| s.to_string()).collect();
2127
2128 for (arg_name, _) in args.iter() {
2130 if !properties.contains_key(arg_name) {
2131 errors.push(ValidationError::invalid_parameter(
2133 arg_name.clone(),
2134 &valid_params,
2135 ));
2136 }
2137 }
2138
2139 errors
2140 }
2141
2142 fn check_missing_required(
2144 args: &serde_json::Map<String, Value>,
2145 properties: &serde_json::Map<String, Value>,
2146 required_params: &HashSet<&str>,
2147 ) -> Vec<ValidationError> {
2148 let mut errors = Vec::new();
2149
2150 for required_param in required_params {
2151 if !args.contains_key(*required_param) {
2152 let param_schema = properties.get(*required_param);
2154
2155 let description = param_schema
2156 .and_then(|schema| schema.get("description"))
2157 .and_then(|d| d.as_str())
2158 .map(|s| s.to_string());
2159
2160 let expected_type = param_schema
2161 .and_then(Self::get_expected_type)
2162 .unwrap_or_else(|| "unknown".to_string());
2163
2164 errors.push(ValidationError::MissingRequiredParameter {
2165 parameter: (*required_param).to_string(),
2166 description,
2167 expected_type,
2168 });
2169 }
2170 }
2171
2172 errors
2173 }
2174
2175 fn validate_parameter_values(
2177 args: &serde_json::Map<String, Value>,
2178 properties: &serde_json::Map<String, Value>,
2179 ) -> Vec<ValidationError> {
2180 let mut errors = Vec::new();
2181
2182 for (param_name, param_value) in args {
2183 if let Some(param_schema) = properties.get(param_name) {
2184 let schema = json!({
2186 "type": "object",
2187 "properties": {
2188 param_name: param_schema
2189 }
2190 });
2191
2192 let compiled = match jsonschema::validator_for(&schema) {
2194 Ok(compiled) => compiled,
2195 Err(e) => {
2196 errors.push(ValidationError::ConstraintViolation {
2197 parameter: param_name.clone(),
2198 message: format!(
2199 "Failed to compile schema for parameter '{param_name}': {e}"
2200 ),
2201 field_path: None,
2202 actual_value: None,
2203 expected_type: None,
2204 constraints: vec![],
2205 });
2206 continue;
2207 }
2208 };
2209
2210 let instance = json!({ param_name: param_value });
2212
2213 let validation_errors: Vec<_> =
2215 compiled.validate(&instance).err().into_iter().collect();
2216
2217 for validation_error in validation_errors {
2218 let error_message = validation_error.to_string();
2220 let instance_path_str = validation_error.instance_path.to_string();
2221 let field_path = if instance_path_str.is_empty() || instance_path_str == "/" {
2222 Some(param_name.clone())
2223 } else {
2224 Some(instance_path_str.trim_start_matches('/').to_string())
2225 };
2226
2227 let constraints = Self::extract_constraints_from_schema(param_schema);
2229
2230 let expected_type = Self::get_expected_type(param_schema);
2232
2233 errors.push(ValidationError::ConstraintViolation {
2234 parameter: param_name.clone(),
2235 message: error_message,
2236 field_path,
2237 actual_value: Some(Box::new(param_value.clone())),
2238 expected_type,
2239 constraints,
2240 });
2241 }
2242 }
2243 }
2244
2245 errors
2246 }
2247
2248 fn extract_constraints_from_schema(schema: &Value) -> Vec<ValidationConstraint> {
2250 let mut constraints = Vec::new();
2251
2252 if let Some(min_value) = schema.get("minimum").and_then(|v| v.as_f64()) {
2254 let exclusive = schema
2255 .get("exclusiveMinimum")
2256 .and_then(|v| v.as_bool())
2257 .unwrap_or(false);
2258 constraints.push(ValidationConstraint::Minimum {
2259 value: min_value,
2260 exclusive,
2261 });
2262 }
2263
2264 if let Some(max_value) = schema.get("maximum").and_then(|v| v.as_f64()) {
2266 let exclusive = schema
2267 .get("exclusiveMaximum")
2268 .and_then(|v| v.as_bool())
2269 .unwrap_or(false);
2270 constraints.push(ValidationConstraint::Maximum {
2271 value: max_value,
2272 exclusive,
2273 });
2274 }
2275
2276 if let Some(min_len) = schema
2278 .get("minLength")
2279 .and_then(|v| v.as_u64())
2280 .map(|v| v as usize)
2281 {
2282 constraints.push(ValidationConstraint::MinLength { value: min_len });
2283 }
2284
2285 if let Some(max_len) = schema
2287 .get("maxLength")
2288 .and_then(|v| v.as_u64())
2289 .map(|v| v as usize)
2290 {
2291 constraints.push(ValidationConstraint::MaxLength { value: max_len });
2292 }
2293
2294 if let Some(pattern) = schema
2296 .get("pattern")
2297 .and_then(|v| v.as_str())
2298 .map(|s| s.to_string())
2299 {
2300 constraints.push(ValidationConstraint::Pattern { pattern });
2301 }
2302
2303 if let Some(enum_values) = schema.get("enum").and_then(|v| v.as_array()).cloned() {
2305 constraints.push(ValidationConstraint::EnumValues {
2306 values: enum_values,
2307 });
2308 }
2309
2310 if let Some(format) = schema
2312 .get("format")
2313 .and_then(|v| v.as_str())
2314 .map(|s| s.to_string())
2315 {
2316 constraints.push(ValidationConstraint::Format { format });
2317 }
2318
2319 if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
2321 constraints.push(ValidationConstraint::MultipleOf { value: multiple_of });
2322 }
2323
2324 if let Some(min_items) = schema
2326 .get("minItems")
2327 .and_then(|v| v.as_u64())
2328 .map(|v| v as usize)
2329 {
2330 constraints.push(ValidationConstraint::MinItems { value: min_items });
2331 }
2332
2333 if let Some(max_items) = schema
2335 .get("maxItems")
2336 .and_then(|v| v.as_u64())
2337 .map(|v| v as usize)
2338 {
2339 constraints.push(ValidationConstraint::MaxItems { value: max_items });
2340 }
2341
2342 if let Some(true) = schema.get("uniqueItems").and_then(|v| v.as_bool()) {
2344 constraints.push(ValidationConstraint::UniqueItems);
2345 }
2346
2347 if let Some(min_props) = schema
2349 .get("minProperties")
2350 .and_then(|v| v.as_u64())
2351 .map(|v| v as usize)
2352 {
2353 constraints.push(ValidationConstraint::MinProperties { value: min_props });
2354 }
2355
2356 if let Some(max_props) = schema
2358 .get("maxProperties")
2359 .and_then(|v| v.as_u64())
2360 .map(|v| v as usize)
2361 {
2362 constraints.push(ValidationConstraint::MaxProperties { value: max_props });
2363 }
2364
2365 if let Some(const_value) = schema.get("const").cloned() {
2367 constraints.push(ValidationConstraint::ConstValue { value: const_value });
2368 }
2369
2370 if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
2372 let properties: Vec<String> = required
2373 .iter()
2374 .filter_map(|v| v.as_str().map(|s| s.to_string()))
2375 .collect();
2376 if !properties.is_empty() {
2377 constraints.push(ValidationConstraint::Required { properties });
2378 }
2379 }
2380
2381 constraints
2382 }
2383
2384 fn get_expected_type(schema: &Value) -> Option<String> {
2386 if let Some(type_value) = schema.get("type") {
2387 if let Some(type_str) = type_value.as_str() {
2388 return Some(type_str.to_string());
2389 } else if let Some(type_array) = type_value.as_array() {
2390 let types: Vec<String> = type_array
2392 .iter()
2393 .filter_map(|v| v.as_str())
2394 .map(|s| s.to_string())
2395 .collect();
2396 if !types.is_empty() {
2397 return Some(types.join(" | "));
2398 }
2399 }
2400 }
2401 None
2402 }
2403
2404 fn wrap_output_schema(
2428 body_schema: &ObjectOrReference<ObjectSchema>,
2429 spec: &Spec,
2430 ) -> Result<Value, Error> {
2431 let mut visited = HashSet::new();
2433 let body_schema_json = match body_schema {
2434 ObjectOrReference::Object(obj_schema) => {
2435 Self::convert_object_schema_to_json_schema(obj_schema, spec, &mut visited)?
2436 }
2437 ObjectOrReference::Ref { ref_path, .. } => {
2438 let resolved = Self::resolve_reference(ref_path, spec, &mut visited)?;
2439 Self::convert_object_schema_to_json_schema(&resolved, spec, &mut visited)?
2440 }
2441 };
2442
2443 let error_schema = create_error_response_schema();
2444
2445 Ok(json!({
2446 "type": "object",
2447 "description": "Unified response structure with success and error variants",
2448 "required": ["status", "body"],
2449 "additionalProperties": false,
2450 "properties": {
2451 "status": {
2452 "type": "integer",
2453 "description": "HTTP status code",
2454 "minimum": 100,
2455 "maximum": 599
2456 },
2457 "body": {
2458 "description": "Response body - either success data or error information",
2459 "oneOf": [
2460 body_schema_json,
2461 error_schema
2462 ]
2463 }
2464 }
2465 }))
2466 }
2467}
2468
2469fn create_error_response_schema() -> Value {
2471 let root_schema = schema_for!(ErrorResponse);
2472 let schema_json = serde_json::to_value(root_schema).expect("Valid error schema");
2473
2474 let definitions = schema_json
2476 .get("$defs")
2477 .or_else(|| schema_json.get("definitions"))
2478 .cloned()
2479 .unwrap_or_else(|| json!({}));
2480
2481 let mut result = schema_json.clone();
2483 if let Some(obj) = result.as_object_mut() {
2484 obj.remove("$schema");
2485 obj.remove("$defs");
2486 obj.remove("definitions");
2487 obj.remove("title");
2488 }
2489
2490 inline_refs(&mut result, &definitions);
2492
2493 result
2494}
2495
2496fn inline_refs(schema: &mut Value, definitions: &Value) {
2498 match schema {
2499 Value::Object(obj) => {
2500 if let Some(ref_value) = obj.get("$ref").cloned()
2502 && let Some(ref_str) = ref_value.as_str()
2503 {
2504 let def_name = ref_str
2506 .strip_prefix("#/$defs/")
2507 .or_else(|| ref_str.strip_prefix("#/definitions/"));
2508
2509 if let Some(name) = def_name
2510 && let Some(definition) = definitions.get(name)
2511 {
2512 *schema = definition.clone();
2514 inline_refs(schema, definitions);
2516 return;
2517 }
2518 }
2519
2520 for (_, value) in obj.iter_mut() {
2522 inline_refs(value, definitions);
2523 }
2524 }
2525 Value::Array(arr) => {
2526 for item in arr.iter_mut() {
2528 inline_refs(item, definitions);
2529 }
2530 }
2531 _ => {} }
2533}
2534
2535#[derive(Debug, Clone)]
2537pub struct QueryParameter {
2538 pub value: Value,
2539 pub explode: bool,
2540}
2541
2542impl QueryParameter {
2543 pub fn new(value: Value, explode: bool) -> Self {
2544 Self { value, explode }
2545 }
2546}
2547
2548#[derive(Debug, Clone)]
2550pub struct ExtractedParameters {
2551 pub path: HashMap<String, Value>,
2552 pub query: HashMap<String, QueryParameter>,
2553 pub headers: HashMap<String, Value>,
2554 pub cookies: HashMap<String, Value>,
2555 pub body: HashMap<String, Value>,
2556 pub config: RequestConfig,
2557}
2558
2559#[derive(Debug, Clone)]
2561pub struct RequestConfig {
2562 pub timeout_seconds: u32,
2563 pub content_type: String,
2564}
2565
2566impl Default for RequestConfig {
2567 fn default() -> Self {
2568 Self {
2569 timeout_seconds: 30,
2570 content_type: mime::APPLICATION_JSON.to_string(),
2571 }
2572 }
2573}
2574
2575#[cfg(test)]
2576mod tests {
2577 use super::*;
2578
2579 use insta::assert_json_snapshot;
2580 use oas3::spec::{
2581 BooleanSchema, Components, MediaType, ObjectOrReference, ObjectSchema, Operation,
2582 Parameter, ParameterIn, RequestBody, Schema, SchemaType, SchemaTypeSet, Spec,
2583 };
2584 use rmcp::model::Tool;
2585 use serde_json::{Value, json};
2586 use std::collections::BTreeMap;
2587
2588 fn create_test_spec() -> Spec {
2590 Spec {
2591 openapi: "3.0.0".to_string(),
2592 info: oas3::spec::Info {
2593 title: "Test API".to_string(),
2594 version: "1.0.0".to_string(),
2595 summary: None,
2596 description: Some("Test API for unit tests".to_string()),
2597 terms_of_service: None,
2598 contact: None,
2599 license: None,
2600 extensions: Default::default(),
2601 },
2602 components: Some(Components {
2603 schemas: BTreeMap::new(),
2604 responses: BTreeMap::new(),
2605 parameters: BTreeMap::new(),
2606 examples: BTreeMap::new(),
2607 request_bodies: BTreeMap::new(),
2608 headers: BTreeMap::new(),
2609 security_schemes: BTreeMap::new(),
2610 links: BTreeMap::new(),
2611 callbacks: BTreeMap::new(),
2612 path_items: BTreeMap::new(),
2613 extensions: Default::default(),
2614 }),
2615 servers: vec![],
2616 paths: None,
2617 external_docs: None,
2618 tags: vec![],
2619 security: vec![],
2620 webhooks: BTreeMap::new(),
2621 extensions: Default::default(),
2622 }
2623 }
2624
2625 fn validate_tool_against_mcp_schema(metadata: &ToolMetadata) {
2626 let schema_content = std::fs::read_to_string("schema/2025-06-18/schema.json")
2627 .expect("Failed to read MCP schema file");
2628 let full_schema: Value =
2629 serde_json::from_str(&schema_content).expect("Failed to parse MCP schema JSON");
2630
2631 let tool_schema = json!({
2633 "$schema": "http://json-schema.org/draft-07/schema#",
2634 "definitions": full_schema.get("definitions"),
2635 "$ref": "#/definitions/Tool"
2636 });
2637
2638 let validator =
2639 jsonschema::validator_for(&tool_schema).expect("Failed to compile MCP Tool schema");
2640
2641 let tool = Tool::from(metadata);
2643
2644 let mcp_tool_json = serde_json::to_value(&tool).expect("Failed to serialize Tool to JSON");
2646
2647 let errors: Vec<String> = validator
2649 .iter_errors(&mcp_tool_json)
2650 .map(|e| e.to_string())
2651 .collect();
2652
2653 if !errors.is_empty() {
2654 panic!("Generated tool failed MCP schema validation: {errors:?}");
2655 }
2656 }
2657
2658 #[test]
2659 fn test_error_schema_structure() {
2660 let error_schema = create_error_response_schema();
2661
2662 assert!(error_schema.get("$schema").is_none());
2664 assert!(error_schema.get("definitions").is_none());
2665
2666 assert_json_snapshot!(error_schema);
2668 }
2669
2670 #[test]
2671 fn test_petstore_get_pet_by_id() {
2672 use oas3::spec::Response;
2673
2674 let mut operation = Operation {
2675 operation_id: Some("getPetById".to_string()),
2676 summary: Some("Find pet by ID".to_string()),
2677 description: Some("Returns a single pet".to_string()),
2678 tags: vec![],
2679 external_docs: None,
2680 parameters: vec![],
2681 request_body: None,
2682 responses: Default::default(),
2683 callbacks: Default::default(),
2684 deprecated: Some(false),
2685 security: vec![],
2686 servers: vec![],
2687 extensions: Default::default(),
2688 };
2689
2690 let param = Parameter {
2692 name: "petId".to_string(),
2693 location: ParameterIn::Path,
2694 description: Some("ID of pet to return".to_string()),
2695 required: Some(true),
2696 deprecated: Some(false),
2697 allow_empty_value: Some(false),
2698 style: None,
2699 explode: None,
2700 allow_reserved: Some(false),
2701 schema: Some(ObjectOrReference::Object(ObjectSchema {
2702 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2703 minimum: Some(serde_json::Number::from(1_i64)),
2704 format: Some("int64".to_string()),
2705 ..Default::default()
2706 })),
2707 example: None,
2708 examples: Default::default(),
2709 content: None,
2710 extensions: Default::default(),
2711 };
2712
2713 operation.parameters.push(ObjectOrReference::Object(param));
2714
2715 let mut responses = BTreeMap::new();
2717 let mut content = BTreeMap::new();
2718 content.insert(
2719 "application/json".to_string(),
2720 MediaType {
2721 extensions: Default::default(),
2722 schema: Some(ObjectOrReference::Object(ObjectSchema {
2723 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2724 properties: {
2725 let mut props = BTreeMap::new();
2726 props.insert(
2727 "id".to_string(),
2728 ObjectOrReference::Object(ObjectSchema {
2729 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2730 format: Some("int64".to_string()),
2731 ..Default::default()
2732 }),
2733 );
2734 props.insert(
2735 "name".to_string(),
2736 ObjectOrReference::Object(ObjectSchema {
2737 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2738 ..Default::default()
2739 }),
2740 );
2741 props.insert(
2742 "status".to_string(),
2743 ObjectOrReference::Object(ObjectSchema {
2744 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2745 ..Default::default()
2746 }),
2747 );
2748 props
2749 },
2750 required: vec!["id".to_string(), "name".to_string()],
2751 ..Default::default()
2752 })),
2753 examples: None,
2754 encoding: Default::default(),
2755 },
2756 );
2757
2758 responses.insert(
2759 "200".to_string(),
2760 ObjectOrReference::Object(Response {
2761 description: Some("successful operation".to_string()),
2762 headers: Default::default(),
2763 content,
2764 links: Default::default(),
2765 extensions: Default::default(),
2766 }),
2767 );
2768 operation.responses = Some(responses);
2769
2770 let spec = create_test_spec();
2771 let metadata = ToolGenerator::generate_tool_metadata(
2772 &operation,
2773 "get".to_string(),
2774 "/pet/{petId}".to_string(),
2775 &spec,
2776 false,
2777 false,
2778 )
2779 .unwrap();
2780
2781 assert_eq!(metadata.name, "getPetById");
2782 assert_eq!(metadata.method, "get");
2783 assert_eq!(metadata.path, "/pet/{petId}");
2784 assert!(
2785 metadata
2786 .description
2787 .clone()
2788 .unwrap()
2789 .contains("Find pet by ID")
2790 );
2791
2792 assert!(metadata.output_schema.is_some());
2794 let output_schema = metadata.output_schema.as_ref().unwrap();
2795
2796 insta::assert_json_snapshot!("test_petstore_get_pet_by_id_output_schema", output_schema);
2798
2799 validate_tool_against_mcp_schema(&metadata);
2801 }
2802
2803 #[test]
2804 fn test_convert_prefix_items_to_draft07_mixed_types() {
2805 let prefix_items = vec![
2808 ObjectOrReference::Object(ObjectSchema {
2809 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2810 format: Some("int32".to_string()),
2811 ..Default::default()
2812 }),
2813 ObjectOrReference::Object(ObjectSchema {
2814 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2815 ..Default::default()
2816 }),
2817 ];
2818
2819 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
2821
2822 let mut result = serde_json::Map::new();
2823 let spec = create_test_spec();
2824 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
2825 .unwrap();
2826
2827 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_mixed_types", result);
2829 }
2830
2831 #[test]
2832 fn test_convert_prefix_items_to_draft07_uniform_types() {
2833 let prefix_items = vec![
2835 ObjectOrReference::Object(ObjectSchema {
2836 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2837 ..Default::default()
2838 }),
2839 ObjectOrReference::Object(ObjectSchema {
2840 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2841 ..Default::default()
2842 }),
2843 ];
2844
2845 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
2847
2848 let mut result = serde_json::Map::new();
2849 let spec = create_test_spec();
2850 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
2851 .unwrap();
2852
2853 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_uniform_types", result);
2855 }
2856
2857 #[test]
2858 fn test_array_with_prefix_items_integration() {
2859 let param = Parameter {
2861 name: "coordinates".to_string(),
2862 location: ParameterIn::Query,
2863 description: Some("X,Y coordinates as tuple".to_string()),
2864 required: Some(true),
2865 deprecated: Some(false),
2866 allow_empty_value: Some(false),
2867 style: None,
2868 explode: None,
2869 allow_reserved: Some(false),
2870 schema: Some(ObjectOrReference::Object(ObjectSchema {
2871 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
2872 prefix_items: vec![
2873 ObjectOrReference::Object(ObjectSchema {
2874 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
2875 format: Some("double".to_string()),
2876 ..Default::default()
2877 }),
2878 ObjectOrReference::Object(ObjectSchema {
2879 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
2880 format: Some("double".to_string()),
2881 ..Default::default()
2882 }),
2883 ],
2884 items: Some(Box::new(Schema::Boolean(BooleanSchema(false)))),
2885 ..Default::default()
2886 })),
2887 example: None,
2888 examples: Default::default(),
2889 content: None,
2890 extensions: Default::default(),
2891 };
2892
2893 let spec = create_test_spec();
2894 let (result, _annotations) =
2895 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, false)
2896 .unwrap();
2897
2898 insta::assert_json_snapshot!("test_array_with_prefix_items_integration", result);
2900 }
2901
2902 #[test]
2903 fn test_skip_tool_description() {
2904 let operation = Operation {
2905 operation_id: Some("getPetById".to_string()),
2906 summary: Some("Find pet by ID".to_string()),
2907 description: Some("Returns a single pet".to_string()),
2908 tags: vec![],
2909 external_docs: None,
2910 parameters: vec![],
2911 request_body: None,
2912 responses: Default::default(),
2913 callbacks: Default::default(),
2914 deprecated: Some(false),
2915 security: vec![],
2916 servers: vec![],
2917 extensions: Default::default(),
2918 };
2919
2920 let spec = create_test_spec();
2921 let metadata = ToolGenerator::generate_tool_metadata(
2922 &operation,
2923 "get".to_string(),
2924 "/pet/{petId}".to_string(),
2925 &spec,
2926 true,
2927 false,
2928 )
2929 .unwrap();
2930
2931 assert_eq!(metadata.name, "getPetById");
2932 assert_eq!(metadata.method, "get");
2933 assert_eq!(metadata.path, "/pet/{petId}");
2934 assert!(metadata.description.is_none());
2935
2936 insta::assert_json_snapshot!("test_skip_tool_description", metadata);
2938
2939 validate_tool_against_mcp_schema(&metadata);
2941 }
2942
2943 #[test]
2944 fn test_keep_tool_description() {
2945 let description = Some("Returns a single pet".to_string());
2946 let operation = Operation {
2947 operation_id: Some("getPetById".to_string()),
2948 summary: Some("Find pet by ID".to_string()),
2949 description: description.clone(),
2950 tags: vec![],
2951 external_docs: None,
2952 parameters: vec![],
2953 request_body: None,
2954 responses: Default::default(),
2955 callbacks: Default::default(),
2956 deprecated: Some(false),
2957 security: vec![],
2958 servers: vec![],
2959 extensions: Default::default(),
2960 };
2961
2962 let spec = create_test_spec();
2963 let metadata = ToolGenerator::generate_tool_metadata(
2964 &operation,
2965 "get".to_string(),
2966 "/pet/{petId}".to_string(),
2967 &spec,
2968 false,
2969 false,
2970 )
2971 .unwrap();
2972
2973 assert_eq!(metadata.name, "getPetById");
2974 assert_eq!(metadata.method, "get");
2975 assert_eq!(metadata.path, "/pet/{petId}");
2976 assert!(metadata.description.is_some());
2977
2978 insta::assert_json_snapshot!("test_keep_tool_description", metadata);
2980
2981 validate_tool_against_mcp_schema(&metadata);
2983 }
2984
2985 #[test]
2986 fn test_skip_parameter_descriptions() {
2987 let param = Parameter {
2988 name: "status".to_string(),
2989 location: ParameterIn::Query,
2990 description: Some("Filter by status".to_string()),
2991 required: Some(false),
2992 deprecated: Some(false),
2993 allow_empty_value: Some(false),
2994 style: None,
2995 explode: None,
2996 allow_reserved: Some(false),
2997 schema: Some(ObjectOrReference::Object(ObjectSchema {
2998 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2999 enum_values: vec![json!("available"), json!("pending"), json!("sold")],
3000 ..Default::default()
3001 })),
3002 example: Some(json!("available")),
3003 examples: Default::default(),
3004 content: None,
3005 extensions: Default::default(),
3006 };
3007
3008 let spec = create_test_spec();
3009 let (schema, _) =
3010 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, true)
3011 .unwrap();
3012
3013 assert!(schema.get("description").is_none());
3015
3016 assert_eq!(schema.get("type").unwrap(), "string");
3018 assert_eq!(schema.get("example").unwrap(), "available");
3019
3020 insta::assert_json_snapshot!("test_skip_parameter_descriptions", schema);
3021 }
3022
3023 #[test]
3024 fn test_keep_parameter_descriptions() {
3025 let param = Parameter {
3026 name: "status".to_string(),
3027 location: ParameterIn::Query,
3028 description: Some("Filter by status".to_string()),
3029 required: Some(false),
3030 deprecated: Some(false),
3031 allow_empty_value: Some(false),
3032 style: None,
3033 explode: None,
3034 allow_reserved: Some(false),
3035 schema: Some(ObjectOrReference::Object(ObjectSchema {
3036 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3037 enum_values: vec![json!("available"), json!("pending"), json!("sold")],
3038 ..Default::default()
3039 })),
3040 example: Some(json!("available")),
3041 examples: Default::default(),
3042 content: None,
3043 extensions: Default::default(),
3044 };
3045
3046 let spec = create_test_spec();
3047 let (schema, _) =
3048 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, false)
3049 .unwrap();
3050
3051 assert!(schema.get("description").is_some());
3053 let description = schema.get("description").unwrap().as_str().unwrap();
3054 assert!(description.contains("Filter by status"));
3055 assert!(description.contains("Example: `\"available\"`"));
3056
3057 assert_eq!(schema.get("type").unwrap(), "string");
3059 assert_eq!(schema.get("example").unwrap(), "available");
3060
3061 insta::assert_json_snapshot!("test_keep_parameter_descriptions", schema);
3062 }
3063
3064 #[test]
3065 fn test_array_with_regular_items_schema() {
3066 let param = Parameter {
3068 name: "tags".to_string(),
3069 location: ParameterIn::Query,
3070 description: Some("List of tags".to_string()),
3071 required: Some(false),
3072 deprecated: Some(false),
3073 allow_empty_value: Some(false),
3074 style: None,
3075 explode: None,
3076 allow_reserved: Some(false),
3077 schema: Some(ObjectOrReference::Object(ObjectSchema {
3078 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3079 items: Some(Box::new(Schema::Object(Box::new(
3080 ObjectOrReference::Object(ObjectSchema {
3081 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3082 min_length: Some(1),
3083 max_length: Some(50),
3084 ..Default::default()
3085 }),
3086 )))),
3087 ..Default::default()
3088 })),
3089 example: None,
3090 examples: Default::default(),
3091 content: None,
3092 extensions: Default::default(),
3093 };
3094
3095 let spec = create_test_spec();
3096 let (result, _annotations) =
3097 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec, false)
3098 .unwrap();
3099
3100 insta::assert_json_snapshot!("test_array_with_regular_items_schema", result);
3102 }
3103
3104 #[test]
3105 fn test_request_body_object_schema() {
3106 let operation = Operation {
3108 operation_id: Some("createPet".to_string()),
3109 summary: Some("Create a new pet".to_string()),
3110 description: Some("Creates a new pet in the store".to_string()),
3111 tags: vec![],
3112 external_docs: None,
3113 parameters: vec![],
3114 request_body: Some(ObjectOrReference::Object(RequestBody {
3115 description: Some("Pet object that needs to be added to the store".to_string()),
3116 content: {
3117 let mut content = BTreeMap::new();
3118 content.insert(
3119 "application/json".to_string(),
3120 MediaType {
3121 extensions: Default::default(),
3122 schema: Some(ObjectOrReference::Object(ObjectSchema {
3123 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3124 ..Default::default()
3125 })),
3126 examples: None,
3127 encoding: Default::default(),
3128 },
3129 );
3130 content
3131 },
3132 required: Some(true),
3133 })),
3134 responses: Default::default(),
3135 callbacks: Default::default(),
3136 deprecated: Some(false),
3137 security: vec![],
3138 servers: vec![],
3139 extensions: Default::default(),
3140 };
3141
3142 let spec = create_test_spec();
3143 let metadata = ToolGenerator::generate_tool_metadata(
3144 &operation,
3145 "post".to_string(),
3146 "/pets".to_string(),
3147 &spec,
3148 false,
3149 false,
3150 )
3151 .unwrap();
3152
3153 let properties = metadata
3155 .parameters
3156 .get("properties")
3157 .unwrap()
3158 .as_object()
3159 .unwrap();
3160 assert!(properties.contains_key("request_body"));
3161
3162 let required = metadata
3164 .parameters
3165 .get("required")
3166 .unwrap()
3167 .as_array()
3168 .unwrap();
3169 assert!(required.contains(&json!("request_body")));
3170
3171 let request_body_schema = properties.get("request_body").unwrap();
3173 insta::assert_json_snapshot!("test_request_body_object_schema", request_body_schema);
3174
3175 validate_tool_against_mcp_schema(&metadata);
3177 }
3178
3179 #[test]
3180 fn test_request_body_array_schema() {
3181 let operation = Operation {
3183 operation_id: Some("createPets".to_string()),
3184 summary: Some("Create multiple pets".to_string()),
3185 description: None,
3186 tags: vec![],
3187 external_docs: None,
3188 parameters: vec![],
3189 request_body: Some(ObjectOrReference::Object(RequestBody {
3190 description: Some("Array of pet objects".to_string()),
3191 content: {
3192 let mut content = BTreeMap::new();
3193 content.insert(
3194 "application/json".to_string(),
3195 MediaType {
3196 extensions: Default::default(),
3197 schema: Some(ObjectOrReference::Object(ObjectSchema {
3198 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3199 items: Some(Box::new(Schema::Object(Box::new(
3200 ObjectOrReference::Object(ObjectSchema {
3201 schema_type: Some(SchemaTypeSet::Single(
3202 SchemaType::Object,
3203 )),
3204 ..Default::default()
3205 }),
3206 )))),
3207 ..Default::default()
3208 })),
3209 examples: None,
3210 encoding: Default::default(),
3211 },
3212 );
3213 content
3214 },
3215 required: Some(false),
3216 })),
3217 responses: Default::default(),
3218 callbacks: Default::default(),
3219 deprecated: Some(false),
3220 security: vec![],
3221 servers: vec![],
3222 extensions: Default::default(),
3223 };
3224
3225 let spec = create_test_spec();
3226 let metadata = ToolGenerator::generate_tool_metadata(
3227 &operation,
3228 "post".to_string(),
3229 "/pets/batch".to_string(),
3230 &spec,
3231 false,
3232 false,
3233 )
3234 .unwrap();
3235
3236 let properties = metadata
3238 .parameters
3239 .get("properties")
3240 .unwrap()
3241 .as_object()
3242 .unwrap();
3243 assert!(properties.contains_key("request_body"));
3244
3245 let required = metadata
3247 .parameters
3248 .get("required")
3249 .unwrap()
3250 .as_array()
3251 .unwrap();
3252 assert!(!required.contains(&json!("request_body")));
3253
3254 let request_body_schema = properties.get("request_body").unwrap();
3256 insta::assert_json_snapshot!("test_request_body_array_schema", request_body_schema);
3257
3258 validate_tool_against_mcp_schema(&metadata);
3260 }
3261
3262 #[test]
3263 fn test_request_body_string_schema() {
3264 let operation = Operation {
3266 operation_id: Some("updatePetName".to_string()),
3267 summary: Some("Update pet name".to_string()),
3268 description: None,
3269 tags: vec![],
3270 external_docs: None,
3271 parameters: vec![],
3272 request_body: Some(ObjectOrReference::Object(RequestBody {
3273 description: None,
3274 content: {
3275 let mut content = BTreeMap::new();
3276 content.insert(
3277 "text/plain".to_string(),
3278 MediaType {
3279 extensions: Default::default(),
3280 schema: Some(ObjectOrReference::Object(ObjectSchema {
3281 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3282 min_length: Some(1),
3283 max_length: Some(100),
3284 ..Default::default()
3285 })),
3286 examples: None,
3287 encoding: Default::default(),
3288 },
3289 );
3290 content
3291 },
3292 required: Some(true),
3293 })),
3294 responses: Default::default(),
3295 callbacks: Default::default(),
3296 deprecated: Some(false),
3297 security: vec![],
3298 servers: vec![],
3299 extensions: Default::default(),
3300 };
3301
3302 let spec = create_test_spec();
3303 let metadata = ToolGenerator::generate_tool_metadata(
3304 &operation,
3305 "put".to_string(),
3306 "/pets/{petId}/name".to_string(),
3307 &spec,
3308 false,
3309 false,
3310 )
3311 .unwrap();
3312
3313 let properties = metadata
3315 .parameters
3316 .get("properties")
3317 .unwrap()
3318 .as_object()
3319 .unwrap();
3320 let request_body_schema = properties.get("request_body").unwrap();
3321 insta::assert_json_snapshot!("test_request_body_string_schema", request_body_schema);
3322
3323 validate_tool_against_mcp_schema(&metadata);
3325 }
3326
3327 #[test]
3328 fn test_request_body_ref_schema() {
3329 let operation = Operation {
3331 operation_id: Some("updatePet".to_string()),
3332 summary: Some("Update existing pet".to_string()),
3333 description: None,
3334 tags: vec![],
3335 external_docs: None,
3336 parameters: vec![],
3337 request_body: Some(ObjectOrReference::Ref {
3338 ref_path: "#/components/requestBodies/PetBody".to_string(),
3339 summary: None,
3340 description: None,
3341 }),
3342 responses: Default::default(),
3343 callbacks: Default::default(),
3344 deprecated: Some(false),
3345 security: vec![],
3346 servers: vec![],
3347 extensions: Default::default(),
3348 };
3349
3350 let spec = create_test_spec();
3351 let metadata = ToolGenerator::generate_tool_metadata(
3352 &operation,
3353 "put".to_string(),
3354 "/pets/{petId}".to_string(),
3355 &spec,
3356 false,
3357 false,
3358 )
3359 .unwrap();
3360
3361 let properties = metadata
3363 .parameters
3364 .get("properties")
3365 .unwrap()
3366 .as_object()
3367 .unwrap();
3368 let request_body_schema = properties.get("request_body").unwrap();
3369 insta::assert_json_snapshot!("test_request_body_ref_schema", request_body_schema);
3370
3371 validate_tool_against_mcp_schema(&metadata);
3373 }
3374
3375 #[test]
3376 fn test_no_request_body_for_get() {
3377 let operation = Operation {
3379 operation_id: Some("listPets".to_string()),
3380 summary: Some("List all pets".to_string()),
3381 description: None,
3382 tags: vec![],
3383 external_docs: None,
3384 parameters: vec![],
3385 request_body: None,
3386 responses: Default::default(),
3387 callbacks: Default::default(),
3388 deprecated: Some(false),
3389 security: vec![],
3390 servers: vec![],
3391 extensions: Default::default(),
3392 };
3393
3394 let spec = create_test_spec();
3395 let metadata = ToolGenerator::generate_tool_metadata(
3396 &operation,
3397 "get".to_string(),
3398 "/pets".to_string(),
3399 &spec,
3400 false,
3401 false,
3402 )
3403 .unwrap();
3404
3405 let properties = metadata
3407 .parameters
3408 .get("properties")
3409 .unwrap()
3410 .as_object()
3411 .unwrap();
3412 assert!(!properties.contains_key("request_body"));
3413
3414 validate_tool_against_mcp_schema(&metadata);
3416 }
3417
3418 #[test]
3419 fn test_request_body_simple_object_with_properties() {
3420 let operation = Operation {
3422 operation_id: Some("updatePetStatus".to_string()),
3423 summary: Some("Update pet status".to_string()),
3424 description: None,
3425 tags: vec![],
3426 external_docs: None,
3427 parameters: vec![],
3428 request_body: Some(ObjectOrReference::Object(RequestBody {
3429 description: Some("Pet status update".to_string()),
3430 content: {
3431 let mut content = BTreeMap::new();
3432 content.insert(
3433 "application/json".to_string(),
3434 MediaType {
3435 extensions: Default::default(),
3436 schema: Some(ObjectOrReference::Object(ObjectSchema {
3437 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3438 properties: {
3439 let mut props = BTreeMap::new();
3440 props.insert(
3441 "status".to_string(),
3442 ObjectOrReference::Object(ObjectSchema {
3443 schema_type: Some(SchemaTypeSet::Single(
3444 SchemaType::String,
3445 )),
3446 ..Default::default()
3447 }),
3448 );
3449 props.insert(
3450 "reason".to_string(),
3451 ObjectOrReference::Object(ObjectSchema {
3452 schema_type: Some(SchemaTypeSet::Single(
3453 SchemaType::String,
3454 )),
3455 ..Default::default()
3456 }),
3457 );
3458 props
3459 },
3460 required: vec!["status".to_string()],
3461 ..Default::default()
3462 })),
3463 examples: None,
3464 encoding: Default::default(),
3465 },
3466 );
3467 content
3468 },
3469 required: Some(false),
3470 })),
3471 responses: Default::default(),
3472 callbacks: Default::default(),
3473 deprecated: Some(false),
3474 security: vec![],
3475 servers: vec![],
3476 extensions: Default::default(),
3477 };
3478
3479 let spec = create_test_spec();
3480 let metadata = ToolGenerator::generate_tool_metadata(
3481 &operation,
3482 "patch".to_string(),
3483 "/pets/{petId}/status".to_string(),
3484 &spec,
3485 false,
3486 false,
3487 )
3488 .unwrap();
3489
3490 let properties = metadata
3492 .parameters
3493 .get("properties")
3494 .unwrap()
3495 .as_object()
3496 .unwrap();
3497 let request_body_schema = properties.get("request_body").unwrap();
3498 insta::assert_json_snapshot!(
3499 "test_request_body_simple_object_with_properties",
3500 request_body_schema
3501 );
3502
3503 let required = metadata
3505 .parameters
3506 .get("required")
3507 .unwrap()
3508 .as_array()
3509 .unwrap();
3510 assert!(!required.contains(&json!("request_body")));
3511
3512 validate_tool_against_mcp_schema(&metadata);
3514 }
3515
3516 #[test]
3517 fn test_request_body_with_nested_properties() {
3518 let operation = Operation {
3520 operation_id: Some("createUser".to_string()),
3521 summary: Some("Create a new user".to_string()),
3522 description: None,
3523 tags: vec![],
3524 external_docs: None,
3525 parameters: vec![],
3526 request_body: Some(ObjectOrReference::Object(RequestBody {
3527 description: Some("User creation data".to_string()),
3528 content: {
3529 let mut content = BTreeMap::new();
3530 content.insert(
3531 "application/json".to_string(),
3532 MediaType {
3533 extensions: Default::default(),
3534 schema: Some(ObjectOrReference::Object(ObjectSchema {
3535 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3536 properties: {
3537 let mut props = BTreeMap::new();
3538 props.insert(
3539 "name".to_string(),
3540 ObjectOrReference::Object(ObjectSchema {
3541 schema_type: Some(SchemaTypeSet::Single(
3542 SchemaType::String,
3543 )),
3544 ..Default::default()
3545 }),
3546 );
3547 props.insert(
3548 "age".to_string(),
3549 ObjectOrReference::Object(ObjectSchema {
3550 schema_type: Some(SchemaTypeSet::Single(
3551 SchemaType::Integer,
3552 )),
3553 minimum: Some(serde_json::Number::from(0)),
3554 maximum: Some(serde_json::Number::from(150)),
3555 ..Default::default()
3556 }),
3557 );
3558 props
3559 },
3560 required: vec!["name".to_string()],
3561 ..Default::default()
3562 })),
3563 examples: None,
3564 encoding: Default::default(),
3565 },
3566 );
3567 content
3568 },
3569 required: Some(true),
3570 })),
3571 responses: Default::default(),
3572 callbacks: Default::default(),
3573 deprecated: Some(false),
3574 security: vec![],
3575 servers: vec![],
3576 extensions: Default::default(),
3577 };
3578
3579 let spec = create_test_spec();
3580 let metadata = ToolGenerator::generate_tool_metadata(
3581 &operation,
3582 "post".to_string(),
3583 "/users".to_string(),
3584 &spec,
3585 false,
3586 false,
3587 )
3588 .unwrap();
3589
3590 let properties = metadata
3592 .parameters
3593 .get("properties")
3594 .unwrap()
3595 .as_object()
3596 .unwrap();
3597 let request_body_schema = properties.get("request_body").unwrap();
3598 insta::assert_json_snapshot!(
3599 "test_request_body_with_nested_properties",
3600 request_body_schema
3601 );
3602
3603 validate_tool_against_mcp_schema(&metadata);
3605 }
3606
3607 #[test]
3608 fn test_operation_without_responses_has_no_output_schema() {
3609 let operation = Operation {
3610 operation_id: Some("testOperation".to_string()),
3611 summary: Some("Test operation".to_string()),
3612 description: None,
3613 tags: vec![],
3614 external_docs: None,
3615 parameters: vec![],
3616 request_body: None,
3617 responses: None,
3618 callbacks: Default::default(),
3619 deprecated: Some(false),
3620 security: vec![],
3621 servers: vec![],
3622 extensions: Default::default(),
3623 };
3624
3625 let spec = create_test_spec();
3626 let metadata = ToolGenerator::generate_tool_metadata(
3627 &operation,
3628 "get".to_string(),
3629 "/test".to_string(),
3630 &spec,
3631 false,
3632 false,
3633 )
3634 .unwrap();
3635
3636 assert!(metadata.output_schema.is_none());
3638
3639 validate_tool_against_mcp_schema(&metadata);
3641 }
3642
3643 #[test]
3644 fn test_extract_output_schema_with_200_response() {
3645 use oas3::spec::Response;
3646
3647 let mut responses = BTreeMap::new();
3649 let mut content = BTreeMap::new();
3650 content.insert(
3651 "application/json".to_string(),
3652 MediaType {
3653 extensions: Default::default(),
3654 schema: Some(ObjectOrReference::Object(ObjectSchema {
3655 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3656 properties: {
3657 let mut props = BTreeMap::new();
3658 props.insert(
3659 "id".to_string(),
3660 ObjectOrReference::Object(ObjectSchema {
3661 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3662 ..Default::default()
3663 }),
3664 );
3665 props.insert(
3666 "name".to_string(),
3667 ObjectOrReference::Object(ObjectSchema {
3668 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3669 ..Default::default()
3670 }),
3671 );
3672 props
3673 },
3674 required: vec!["id".to_string(), "name".to_string()],
3675 ..Default::default()
3676 })),
3677 examples: None,
3678 encoding: Default::default(),
3679 },
3680 );
3681
3682 responses.insert(
3683 "200".to_string(),
3684 ObjectOrReference::Object(Response {
3685 description: Some("Successful response".to_string()),
3686 headers: Default::default(),
3687 content,
3688 links: Default::default(),
3689 extensions: Default::default(),
3690 }),
3691 );
3692
3693 let spec = create_test_spec();
3694 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
3695
3696 insta::assert_json_snapshot!(result);
3698 }
3699
3700 #[test]
3701 fn test_extract_output_schema_with_201_response() {
3702 use oas3::spec::Response;
3703
3704 let mut responses = BTreeMap::new();
3706 let mut content = BTreeMap::new();
3707 content.insert(
3708 "application/json".to_string(),
3709 MediaType {
3710 extensions: Default::default(),
3711 schema: Some(ObjectOrReference::Object(ObjectSchema {
3712 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3713 properties: {
3714 let mut props = BTreeMap::new();
3715 props.insert(
3716 "created".to_string(),
3717 ObjectOrReference::Object(ObjectSchema {
3718 schema_type: Some(SchemaTypeSet::Single(SchemaType::Boolean)),
3719 ..Default::default()
3720 }),
3721 );
3722 props
3723 },
3724 ..Default::default()
3725 })),
3726 examples: None,
3727 encoding: Default::default(),
3728 },
3729 );
3730
3731 responses.insert(
3732 "201".to_string(),
3733 ObjectOrReference::Object(Response {
3734 description: Some("Created".to_string()),
3735 headers: Default::default(),
3736 content,
3737 links: Default::default(),
3738 extensions: Default::default(),
3739 }),
3740 );
3741
3742 let spec = create_test_spec();
3743 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
3744
3745 insta::assert_json_snapshot!(result);
3747 }
3748
3749 #[test]
3750 fn test_extract_output_schema_with_2xx_response() {
3751 use oas3::spec::Response;
3752
3753 let mut responses = BTreeMap::new();
3755 let mut content = BTreeMap::new();
3756 content.insert(
3757 "application/json".to_string(),
3758 MediaType {
3759 extensions: Default::default(),
3760 schema: Some(ObjectOrReference::Object(ObjectSchema {
3761 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3762 items: Some(Box::new(Schema::Object(Box::new(
3763 ObjectOrReference::Object(ObjectSchema {
3764 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3765 ..Default::default()
3766 }),
3767 )))),
3768 ..Default::default()
3769 })),
3770 examples: None,
3771 encoding: Default::default(),
3772 },
3773 );
3774
3775 responses.insert(
3776 "2XX".to_string(),
3777 ObjectOrReference::Object(Response {
3778 description: Some("Success".to_string()),
3779 headers: Default::default(),
3780 content,
3781 links: Default::default(),
3782 extensions: Default::default(),
3783 }),
3784 );
3785
3786 let spec = create_test_spec();
3787 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
3788
3789 insta::assert_json_snapshot!(result);
3791 }
3792
3793 #[test]
3794 fn test_extract_output_schema_no_responses() {
3795 let spec = create_test_spec();
3796 let result = ToolGenerator::extract_output_schema(&None, &spec).unwrap();
3797
3798 insta::assert_json_snapshot!(result);
3800 }
3801
3802 #[test]
3803 fn test_extract_output_schema_only_error_responses() {
3804 use oas3::spec::Response;
3805
3806 let mut responses = BTreeMap::new();
3808 responses.insert(
3809 "404".to_string(),
3810 ObjectOrReference::Object(Response {
3811 description: Some("Not found".to_string()),
3812 headers: Default::default(),
3813 content: Default::default(),
3814 links: Default::default(),
3815 extensions: Default::default(),
3816 }),
3817 );
3818 responses.insert(
3819 "500".to_string(),
3820 ObjectOrReference::Object(Response {
3821 description: Some("Server error".to_string()),
3822 headers: Default::default(),
3823 content: Default::default(),
3824 links: Default::default(),
3825 extensions: Default::default(),
3826 }),
3827 );
3828
3829 let spec = create_test_spec();
3830 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
3831
3832 insta::assert_json_snapshot!(result);
3834 }
3835
3836 #[test]
3837 fn test_extract_output_schema_with_ref() {
3838 use oas3::spec::Response;
3839
3840 let mut spec = create_test_spec();
3842 let mut schemas = BTreeMap::new();
3843 schemas.insert(
3844 "Pet".to_string(),
3845 ObjectOrReference::Object(ObjectSchema {
3846 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3847 properties: {
3848 let mut props = BTreeMap::new();
3849 props.insert(
3850 "name".to_string(),
3851 ObjectOrReference::Object(ObjectSchema {
3852 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3853 ..Default::default()
3854 }),
3855 );
3856 props
3857 },
3858 ..Default::default()
3859 }),
3860 );
3861 spec.components.as_mut().unwrap().schemas = schemas;
3862
3863 let mut responses = BTreeMap::new();
3865 let mut content = BTreeMap::new();
3866 content.insert(
3867 "application/json".to_string(),
3868 MediaType {
3869 extensions: Default::default(),
3870 schema: Some(ObjectOrReference::Ref {
3871 ref_path: "#/components/schemas/Pet".to_string(),
3872 summary: None,
3873 description: None,
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("Success".to_string()),
3884 headers: Default::default(),
3885 content,
3886 links: Default::default(),
3887 extensions: Default::default(),
3888 }),
3889 );
3890
3891 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
3892
3893 insta::assert_json_snapshot!(result);
3895 }
3896
3897 #[test]
3898 fn test_generate_tool_metadata_includes_output_schema() {
3899 use oas3::spec::Response;
3900
3901 let mut operation = Operation {
3902 operation_id: Some("getPet".to_string()),
3903 summary: Some("Get a pet".to_string()),
3904 description: None,
3905 tags: vec![],
3906 external_docs: None,
3907 parameters: vec![],
3908 request_body: None,
3909 responses: Default::default(),
3910 callbacks: Default::default(),
3911 deprecated: Some(false),
3912 security: vec![],
3913 servers: vec![],
3914 extensions: Default::default(),
3915 };
3916
3917 let mut responses = BTreeMap::new();
3919 let mut content = BTreeMap::new();
3920 content.insert(
3921 "application/json".to_string(),
3922 MediaType {
3923 extensions: Default::default(),
3924 schema: Some(ObjectOrReference::Object(ObjectSchema {
3925 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3926 properties: {
3927 let mut props = BTreeMap::new();
3928 props.insert(
3929 "id".to_string(),
3930 ObjectOrReference::Object(ObjectSchema {
3931 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3932 ..Default::default()
3933 }),
3934 );
3935 props
3936 },
3937 ..Default::default()
3938 })),
3939 examples: None,
3940 encoding: Default::default(),
3941 },
3942 );
3943
3944 responses.insert(
3945 "200".to_string(),
3946 ObjectOrReference::Object(Response {
3947 description: Some("Success".to_string()),
3948 headers: Default::default(),
3949 content,
3950 links: Default::default(),
3951 extensions: Default::default(),
3952 }),
3953 );
3954 operation.responses = Some(responses);
3955
3956 let spec = create_test_spec();
3957 let metadata = ToolGenerator::generate_tool_metadata(
3958 &operation,
3959 "get".to_string(),
3960 "/pets/{id}".to_string(),
3961 &spec,
3962 false,
3963 false,
3964 )
3965 .unwrap();
3966
3967 assert!(metadata.output_schema.is_some());
3969 let output_schema = metadata.output_schema.as_ref().unwrap();
3970
3971 insta::assert_json_snapshot!(
3973 "test_generate_tool_metadata_includes_output_schema",
3974 output_schema
3975 );
3976
3977 validate_tool_against_mcp_schema(&metadata);
3979 }
3980
3981 #[test]
3982 fn test_sanitize_property_name() {
3983 assert_eq!(sanitize_property_name("user name"), "user_name");
3985 assert_eq!(
3986 sanitize_property_name("first name last name"),
3987 "first_name_last_name"
3988 );
3989
3990 assert_eq!(sanitize_property_name("user(admin)"), "user_admin");
3992 assert_eq!(sanitize_property_name("user[admin]"), "user_admin");
3993 assert_eq!(sanitize_property_name("price($)"), "price");
3994 assert_eq!(sanitize_property_name("email@address"), "email_address");
3995 assert_eq!(sanitize_property_name("item#1"), "item_1");
3996 assert_eq!(sanitize_property_name("a/b/c"), "a_b_c");
3997
3998 assert_eq!(sanitize_property_name("user_name"), "user_name");
4000 assert_eq!(sanitize_property_name("userName123"), "userName123");
4001 assert_eq!(sanitize_property_name("user.name"), "user.name");
4002 assert_eq!(sanitize_property_name("user-name"), "user-name");
4003
4004 assert_eq!(sanitize_property_name("123name"), "param_123name");
4006 assert_eq!(sanitize_property_name("1st_place"), "param_1st_place");
4007
4008 assert_eq!(sanitize_property_name(""), "param_");
4010
4011 let long_name = "a".repeat(100);
4013 assert_eq!(sanitize_property_name(&long_name).len(), 64);
4014
4015 assert_eq!(sanitize_property_name("!@#$%^&*()"), "param_");
4018 }
4019
4020 #[test]
4021 fn test_sanitize_property_name_trailing_underscores() {
4022 assert_eq!(sanitize_property_name("page[size]"), "page_size");
4024 assert_eq!(sanitize_property_name("user[id]"), "user_id");
4025 assert_eq!(sanitize_property_name("field[]"), "field");
4026
4027 assert_eq!(sanitize_property_name("field___"), "field");
4029 assert_eq!(sanitize_property_name("test[[["), "test");
4030 }
4031
4032 #[test]
4033 fn test_sanitize_property_name_consecutive_underscores() {
4034 assert_eq!(sanitize_property_name("user__name"), "user_name");
4036 assert_eq!(sanitize_property_name("first___last"), "first_last");
4037 assert_eq!(sanitize_property_name("a____b____c"), "a_b_c");
4038
4039 assert_eq!(sanitize_property_name("user[[name]]"), "user_name");
4041 assert_eq!(sanitize_property_name("field@#$value"), "field_value");
4042 }
4043
4044 #[test]
4045 fn test_sanitize_property_name_edge_cases() {
4046 assert_eq!(sanitize_property_name("_private"), "_private");
4048 assert_eq!(sanitize_property_name("__dunder"), "_dunder");
4049
4050 assert_eq!(sanitize_property_name("[[["), "param_");
4052 assert_eq!(sanitize_property_name("@@@"), "param_");
4053
4054 assert_eq!(sanitize_property_name(""), "param_");
4056
4057 assert_eq!(sanitize_property_name("_field[size]"), "_field_size");
4059 assert_eq!(sanitize_property_name("__test__"), "_test");
4060 }
4061
4062 #[test]
4063 fn test_sanitize_property_name_complex_cases() {
4064 assert_eq!(sanitize_property_name("page[size]"), "page_size");
4066 assert_eq!(sanitize_property_name("filter[status]"), "filter_status");
4067 assert_eq!(
4068 sanitize_property_name("sort[-created_at]"),
4069 "sort_-created_at"
4070 );
4071 assert_eq!(
4072 sanitize_property_name("include[author.posts]"),
4073 "include_author.posts"
4074 );
4075
4076 let long_name = "very_long_field_name_with_special[characters]_that_needs_truncation_____";
4078 let expected = "very_long_field_name_with_special_characters_that_needs_truncat";
4079 assert_eq!(sanitize_property_name(long_name), expected);
4080 }
4081
4082 #[test]
4083 fn test_property_sanitization_with_annotations() {
4084 let spec = create_test_spec();
4085 let mut visited = HashSet::new();
4086
4087 let obj_schema = ObjectSchema {
4089 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
4090 properties: {
4091 let mut props = BTreeMap::new();
4092 props.insert(
4094 "user name".to_string(),
4095 ObjectOrReference::Object(ObjectSchema {
4096 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4097 ..Default::default()
4098 }),
4099 );
4100 props.insert(
4102 "price($)".to_string(),
4103 ObjectOrReference::Object(ObjectSchema {
4104 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
4105 ..Default::default()
4106 }),
4107 );
4108 props.insert(
4110 "validName".to_string(),
4111 ObjectOrReference::Object(ObjectSchema {
4112 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4113 ..Default::default()
4114 }),
4115 );
4116 props
4117 },
4118 ..Default::default()
4119 };
4120
4121 let result =
4122 ToolGenerator::convert_object_schema_to_json_schema(&obj_schema, &spec, &mut visited)
4123 .unwrap();
4124
4125 insta::assert_json_snapshot!("test_property_sanitization_with_annotations", result);
4127 }
4128
4129 #[test]
4130 fn test_parameter_sanitization_and_extraction() {
4131 let spec = create_test_spec();
4132
4133 let operation = Operation {
4135 operation_id: Some("testOp".to_string()),
4136 parameters: vec![
4137 ObjectOrReference::Object(Parameter {
4139 name: "user(id)".to_string(),
4140 location: ParameterIn::Path,
4141 description: Some("User ID".to_string()),
4142 required: Some(true),
4143 deprecated: Some(false),
4144 allow_empty_value: Some(false),
4145 style: None,
4146 explode: None,
4147 allow_reserved: Some(false),
4148 schema: Some(ObjectOrReference::Object(ObjectSchema {
4149 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4150 ..Default::default()
4151 })),
4152 example: None,
4153 examples: Default::default(),
4154 content: None,
4155 extensions: Default::default(),
4156 }),
4157 ObjectOrReference::Object(Parameter {
4159 name: "page size".to_string(),
4160 location: ParameterIn::Query,
4161 description: Some("Page size".to_string()),
4162 required: Some(false),
4163 deprecated: Some(false),
4164 allow_empty_value: Some(false),
4165 style: None,
4166 explode: None,
4167 allow_reserved: Some(false),
4168 schema: Some(ObjectOrReference::Object(ObjectSchema {
4169 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4170 ..Default::default()
4171 })),
4172 example: None,
4173 examples: Default::default(),
4174 content: None,
4175 extensions: Default::default(),
4176 }),
4177 ObjectOrReference::Object(Parameter {
4179 name: "auth-token!".to_string(),
4180 location: ParameterIn::Header,
4181 description: Some("Auth token".to_string()),
4182 required: Some(false),
4183 deprecated: Some(false),
4184 allow_empty_value: Some(false),
4185 style: None,
4186 explode: None,
4187 allow_reserved: Some(false),
4188 schema: Some(ObjectOrReference::Object(ObjectSchema {
4189 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4190 ..Default::default()
4191 })),
4192 example: None,
4193 examples: Default::default(),
4194 content: None,
4195 extensions: Default::default(),
4196 }),
4197 ],
4198 ..Default::default()
4199 };
4200
4201 let tool_metadata = ToolGenerator::generate_tool_metadata(
4202 &operation,
4203 "get".to_string(),
4204 "/users/{user(id)}".to_string(),
4205 &spec,
4206 false,
4207 false,
4208 )
4209 .unwrap();
4210
4211 let properties = tool_metadata
4213 .parameters
4214 .get("properties")
4215 .unwrap()
4216 .as_object()
4217 .unwrap();
4218
4219 assert!(properties.contains_key("user_id"));
4220 assert!(properties.contains_key("page_size"));
4221 assert!(properties.contains_key("header_auth-token"));
4222
4223 let required = tool_metadata
4225 .parameters
4226 .get("required")
4227 .unwrap()
4228 .as_array()
4229 .unwrap();
4230 assert!(required.contains(&json!("user_id")));
4231
4232 let arguments = json!({
4234 "user_id": "123",
4235 "page_size": 10,
4236 "header_auth-token": "secret"
4237 });
4238
4239 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
4240
4241 assert_eq!(extracted.path.get("user(id)"), Some(&json!("123")));
4243
4244 assert_eq!(
4246 extracted.query.get("page size").map(|q| &q.value),
4247 Some(&json!(10))
4248 );
4249
4250 assert_eq!(extracted.headers.get("auth-token!"), Some(&json!("secret")));
4252 }
4253
4254 #[test]
4255 fn test_check_unknown_parameters() {
4256 let mut properties = serde_json::Map::new();
4258 properties.insert("page_size".to_string(), json!({"type": "integer"}));
4259 properties.insert("user_id".to_string(), json!({"type": "string"}));
4260
4261 let mut args = serde_json::Map::new();
4262 args.insert("page_sixe".to_string(), json!(10)); let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4265 assert!(!result.is_empty());
4266 assert_eq!(result.len(), 1);
4267
4268 match &result[0] {
4269 ValidationError::InvalidParameter {
4270 parameter,
4271 suggestions,
4272 valid_parameters,
4273 } => {
4274 assert_eq!(parameter, "page_sixe");
4275 assert_eq!(suggestions, &vec!["page_size".to_string()]);
4276 assert_eq!(
4277 valid_parameters,
4278 &vec!["page_size".to_string(), "user_id".to_string()]
4279 );
4280 }
4281 _ => panic!("Expected InvalidParameter variant"),
4282 }
4283 }
4284
4285 #[test]
4286 fn test_check_unknown_parameters_no_suggestions() {
4287 let mut properties = serde_json::Map::new();
4289 properties.insert("limit".to_string(), json!({"type": "integer"}));
4290 properties.insert("offset".to_string(), json!({"type": "integer"}));
4291
4292 let mut args = serde_json::Map::new();
4293 args.insert("xyz123".to_string(), json!("value"));
4294
4295 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4296 assert!(!result.is_empty());
4297 assert_eq!(result.len(), 1);
4298
4299 match &result[0] {
4300 ValidationError::InvalidParameter {
4301 parameter,
4302 suggestions,
4303 valid_parameters,
4304 } => {
4305 assert_eq!(parameter, "xyz123");
4306 assert!(suggestions.is_empty());
4307 assert!(valid_parameters.contains(&"limit".to_string()));
4308 assert!(valid_parameters.contains(&"offset".to_string()));
4309 }
4310 _ => panic!("Expected InvalidParameter variant"),
4311 }
4312 }
4313
4314 #[test]
4315 fn test_check_unknown_parameters_multiple_suggestions() {
4316 let mut properties = serde_json::Map::new();
4318 properties.insert("user_id".to_string(), json!({"type": "string"}));
4319 properties.insert("user_iid".to_string(), json!({"type": "string"}));
4320 properties.insert("user_name".to_string(), json!({"type": "string"}));
4321
4322 let mut args = serde_json::Map::new();
4323 args.insert("usr_id".to_string(), json!("123"));
4324
4325 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4326 assert!(!result.is_empty());
4327 assert_eq!(result.len(), 1);
4328
4329 match &result[0] {
4330 ValidationError::InvalidParameter {
4331 parameter,
4332 suggestions,
4333 valid_parameters,
4334 } => {
4335 assert_eq!(parameter, "usr_id");
4336 assert!(!suggestions.is_empty());
4337 assert!(suggestions.contains(&"user_id".to_string()));
4338 assert_eq!(valid_parameters.len(), 3);
4339 }
4340 _ => panic!("Expected InvalidParameter variant"),
4341 }
4342 }
4343
4344 #[test]
4345 fn test_check_unknown_parameters_valid() {
4346 let mut properties = serde_json::Map::new();
4348 properties.insert("name".to_string(), json!({"type": "string"}));
4349 properties.insert("email".to_string(), json!({"type": "string"}));
4350
4351 let mut args = serde_json::Map::new();
4352 args.insert("name".to_string(), json!("John"));
4353 args.insert("email".to_string(), json!("john@example.com"));
4354
4355 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4356 assert!(result.is_empty());
4357 }
4358
4359 #[test]
4360 fn test_check_unknown_parameters_empty() {
4361 let properties = serde_json::Map::new();
4363
4364 let mut args = serde_json::Map::new();
4365 args.insert("any_param".to_string(), json!("value"));
4366
4367 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4368 assert!(!result.is_empty());
4369 assert_eq!(result.len(), 1);
4370
4371 match &result[0] {
4372 ValidationError::InvalidParameter {
4373 parameter,
4374 suggestions,
4375 valid_parameters,
4376 } => {
4377 assert_eq!(parameter, "any_param");
4378 assert!(suggestions.is_empty());
4379 assert!(valid_parameters.is_empty());
4380 }
4381 _ => panic!("Expected InvalidParameter variant"),
4382 }
4383 }
4384
4385 #[test]
4386 fn test_check_unknown_parameters_gltf_pagination() {
4387 let mut properties = serde_json::Map::new();
4389 properties.insert(
4390 "page_number".to_string(),
4391 json!({
4392 "type": "integer",
4393 "x-original-name": "page[number]"
4394 }),
4395 );
4396 properties.insert(
4397 "page_size".to_string(),
4398 json!({
4399 "type": "integer",
4400 "x-original-name": "page[size]"
4401 }),
4402 );
4403
4404 let mut args = serde_json::Map::new();
4406 args.insert("page".to_string(), json!(1));
4407 args.insert("per_page".to_string(), json!(10));
4408
4409 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4410 assert_eq!(result.len(), 2, "Should have 2 unknown parameters");
4411
4412 let page_error = result
4414 .iter()
4415 .find(|e| {
4416 if let ValidationError::InvalidParameter { parameter, .. } = e {
4417 parameter == "page"
4418 } else {
4419 false
4420 }
4421 })
4422 .expect("Should have error for 'page'");
4423
4424 let per_page_error = result
4425 .iter()
4426 .find(|e| {
4427 if let ValidationError::InvalidParameter { parameter, .. } = e {
4428 parameter == "per_page"
4429 } else {
4430 false
4431 }
4432 })
4433 .expect("Should have error for 'per_page'");
4434
4435 match page_error {
4437 ValidationError::InvalidParameter {
4438 suggestions,
4439 valid_parameters,
4440 ..
4441 } => {
4442 assert!(
4443 suggestions.contains(&"page_number".to_string()),
4444 "Should suggest 'page_number' for 'page'"
4445 );
4446 assert_eq!(valid_parameters.len(), 2);
4447 assert!(valid_parameters.contains(&"page_number".to_string()));
4448 assert!(valid_parameters.contains(&"page_size".to_string()));
4449 }
4450 _ => panic!("Expected InvalidParameter"),
4451 }
4452
4453 match per_page_error {
4455 ValidationError::InvalidParameter {
4456 parameter,
4457 suggestions,
4458 valid_parameters,
4459 ..
4460 } => {
4461 assert_eq!(parameter, "per_page");
4462 assert_eq!(valid_parameters.len(), 2);
4463 if !suggestions.is_empty() {
4466 assert!(suggestions.contains(&"page_size".to_string()));
4467 }
4468 }
4469 _ => panic!("Expected InvalidParameter"),
4470 }
4471 }
4472
4473 #[test]
4474 fn test_validate_parameters_with_invalid_params() {
4475 let tool_metadata = ToolMetadata {
4477 name: "listItems".to_string(),
4478 title: None,
4479 description: Some("List items".to_string()),
4480 parameters: json!({
4481 "type": "object",
4482 "properties": {
4483 "page_number": {
4484 "type": "integer",
4485 "x-original-name": "page[number]"
4486 },
4487 "page_size": {
4488 "type": "integer",
4489 "x-original-name": "page[size]"
4490 }
4491 },
4492 "required": []
4493 }),
4494 output_schema: None,
4495 method: "GET".to_string(),
4496 path: "/items".to_string(),
4497 security: None,
4498 };
4499
4500 let arguments = json!({
4502 "page": 1,
4503 "per_page": 10
4504 });
4505
4506 let result = ToolGenerator::validate_parameters(&tool_metadata, &arguments);
4507 assert!(
4508 result.is_err(),
4509 "Should fail validation with unknown parameters"
4510 );
4511
4512 let error = result.unwrap_err();
4513 match error {
4514 ToolCallValidationError::InvalidParameters { violations } => {
4515 assert_eq!(violations.len(), 2, "Should have 2 validation errors");
4516
4517 let has_page_error = violations.iter().any(|v| {
4519 if let ValidationError::InvalidParameter { parameter, .. } = v {
4520 parameter == "page"
4521 } else {
4522 false
4523 }
4524 });
4525
4526 let has_per_page_error = violations.iter().any(|v| {
4527 if let ValidationError::InvalidParameter { parameter, .. } = v {
4528 parameter == "per_page"
4529 } else {
4530 false
4531 }
4532 });
4533
4534 assert!(has_page_error, "Should have error for 'page' parameter");
4535 assert!(
4536 has_per_page_error,
4537 "Should have error for 'per_page' parameter"
4538 );
4539 }
4540 _ => panic!("Expected InvalidParameters"),
4541 }
4542 }
4543
4544 #[test]
4545 fn test_cookie_parameter_sanitization() {
4546 let spec = create_test_spec();
4547
4548 let operation = Operation {
4549 operation_id: Some("testCookie".to_string()),
4550 parameters: vec![ObjectOrReference::Object(Parameter {
4551 name: "session[id]".to_string(),
4552 location: ParameterIn::Cookie,
4553 description: Some("Session ID".to_string()),
4554 required: Some(false),
4555 deprecated: Some(false),
4556 allow_empty_value: Some(false),
4557 style: None,
4558 explode: None,
4559 allow_reserved: Some(false),
4560 schema: Some(ObjectOrReference::Object(ObjectSchema {
4561 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4562 ..Default::default()
4563 })),
4564 example: None,
4565 examples: Default::default(),
4566 content: None,
4567 extensions: Default::default(),
4568 })],
4569 ..Default::default()
4570 };
4571
4572 let tool_metadata = ToolGenerator::generate_tool_metadata(
4573 &operation,
4574 "get".to_string(),
4575 "/data".to_string(),
4576 &spec,
4577 false,
4578 false,
4579 )
4580 .unwrap();
4581
4582 let properties = tool_metadata
4583 .parameters
4584 .get("properties")
4585 .unwrap()
4586 .as_object()
4587 .unwrap();
4588
4589 assert!(properties.contains_key("cookie_session_id"));
4591
4592 let arguments = json!({
4594 "cookie_session_id": "abc123"
4595 });
4596
4597 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
4598
4599 assert_eq!(extracted.cookies.get("session[id]"), Some(&json!("abc123")));
4601 }
4602
4603 #[test]
4604 fn test_parameter_description_with_examples() {
4605 let spec = create_test_spec();
4606
4607 let param_with_example = Parameter {
4609 name: "status".to_string(),
4610 location: ParameterIn::Query,
4611 description: Some("Filter by status".to_string()),
4612 required: Some(false),
4613 deprecated: Some(false),
4614 allow_empty_value: Some(false),
4615 style: None,
4616 explode: None,
4617 allow_reserved: Some(false),
4618 schema: Some(ObjectOrReference::Object(ObjectSchema {
4619 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4620 ..Default::default()
4621 })),
4622 example: Some(json!("active")),
4623 examples: Default::default(),
4624 content: None,
4625 extensions: Default::default(),
4626 };
4627
4628 let (schema, _) = ToolGenerator::convert_parameter_schema(
4629 ¶m_with_example,
4630 ParameterIn::Query,
4631 &spec,
4632 false,
4633 )
4634 .unwrap();
4635 let description = schema.get("description").unwrap().as_str().unwrap();
4636 assert_eq!(description, "Filter by status. Example: `\"active\"`");
4637
4638 let mut examples_map = std::collections::BTreeMap::new();
4640 examples_map.insert(
4641 "example1".to_string(),
4642 ObjectOrReference::Object(oas3::spec::Example {
4643 value: Some(json!("pending")),
4644 ..Default::default()
4645 }),
4646 );
4647 examples_map.insert(
4648 "example2".to_string(),
4649 ObjectOrReference::Object(oas3::spec::Example {
4650 value: Some(json!("completed")),
4651 ..Default::default()
4652 }),
4653 );
4654
4655 let param_with_examples = Parameter {
4656 name: "status".to_string(),
4657 location: ParameterIn::Query,
4658 description: Some("Filter by status".to_string()),
4659 required: Some(false),
4660 deprecated: Some(false),
4661 allow_empty_value: Some(false),
4662 style: None,
4663 explode: None,
4664 allow_reserved: Some(false),
4665 schema: Some(ObjectOrReference::Object(ObjectSchema {
4666 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4667 ..Default::default()
4668 })),
4669 example: None,
4670 examples: examples_map,
4671 content: None,
4672 extensions: Default::default(),
4673 };
4674
4675 let (schema, _) = ToolGenerator::convert_parameter_schema(
4676 ¶m_with_examples,
4677 ParameterIn::Query,
4678 &spec,
4679 false,
4680 )
4681 .unwrap();
4682 let description = schema.get("description").unwrap().as_str().unwrap();
4683 assert!(description.starts_with("Filter by status. Examples:\n"));
4684 assert!(description.contains("`\"pending\"`"));
4685 assert!(description.contains("`\"completed\"`"));
4686
4687 let param_no_desc = Parameter {
4689 name: "limit".to_string(),
4690 location: ParameterIn::Query,
4691 description: None,
4692 required: Some(false),
4693 deprecated: Some(false),
4694 allow_empty_value: Some(false),
4695 style: None,
4696 explode: None,
4697 allow_reserved: Some(false),
4698 schema: Some(ObjectOrReference::Object(ObjectSchema {
4699 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4700 ..Default::default()
4701 })),
4702 example: Some(json!(100)),
4703 examples: Default::default(),
4704 content: None,
4705 extensions: Default::default(),
4706 };
4707
4708 let (schema, _) = ToolGenerator::convert_parameter_schema(
4709 ¶m_no_desc,
4710 ParameterIn::Query,
4711 &spec,
4712 false,
4713 )
4714 .unwrap();
4715 let description = schema.get("description").unwrap().as_str().unwrap();
4716 assert_eq!(description, "limit parameter. Example: `100`");
4717 }
4718
4719 #[test]
4720 fn test_format_examples_for_description() {
4721 let examples = vec![json!("active")];
4723 let result = ToolGenerator::format_examples_for_description(&examples);
4724 assert_eq!(result, Some("Example: `\"active\"`".to_string()));
4725
4726 let examples = vec![json!(42)];
4728 let result = ToolGenerator::format_examples_for_description(&examples);
4729 assert_eq!(result, Some("Example: `42`".to_string()));
4730
4731 let examples = vec![json!(true)];
4733 let result = ToolGenerator::format_examples_for_description(&examples);
4734 assert_eq!(result, Some("Example: `true`".to_string()));
4735
4736 let examples = vec![json!("active"), json!("pending"), json!("completed")];
4738 let result = ToolGenerator::format_examples_for_description(&examples);
4739 assert_eq!(
4740 result,
4741 Some("Examples:\n- `\"active\"`\n- `\"pending\"`\n- `\"completed\"`".to_string())
4742 );
4743
4744 let examples = vec![json!(["a", "b", "c"])];
4746 let result = ToolGenerator::format_examples_for_description(&examples);
4747 assert_eq!(result, Some("Example: `[\"a\",\"b\",\"c\"]`".to_string()));
4748
4749 let examples = vec![json!({"key": "value"})];
4751 let result = ToolGenerator::format_examples_for_description(&examples);
4752 assert_eq!(result, Some("Example: `{\"key\":\"value\"}`".to_string()));
4753
4754 let examples = vec![];
4756 let result = ToolGenerator::format_examples_for_description(&examples);
4757 assert_eq!(result, None);
4758
4759 let examples = vec![json!(null)];
4761 let result = ToolGenerator::format_examples_for_description(&examples);
4762 assert_eq!(result, Some("Example: `null`".to_string()));
4763
4764 let examples = vec![json!("text"), json!(123), json!(true)];
4766 let result = ToolGenerator::format_examples_for_description(&examples);
4767 assert_eq!(
4768 result,
4769 Some("Examples:\n- `\"text\"`\n- `123`\n- `true`".to_string())
4770 );
4771
4772 let examples = vec![json!(["a", "b", "c", "d", "e", "f"])];
4774 let result = ToolGenerator::format_examples_for_description(&examples);
4775 assert_eq!(
4776 result,
4777 Some("Example: `[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\"]`".to_string())
4778 );
4779
4780 let examples = vec![json!([1, 2])];
4782 let result = ToolGenerator::format_examples_for_description(&examples);
4783 assert_eq!(result, Some("Example: `[1,2]`".to_string()));
4784
4785 let examples = vec![json!({"user": {"name": "John", "age": 30}})];
4787 let result = ToolGenerator::format_examples_for_description(&examples);
4788 assert_eq!(
4789 result,
4790 Some("Example: `{\"user\":{\"name\":\"John\",\"age\":30}}`".to_string())
4791 );
4792
4793 let examples = vec![json!("a"), json!("b"), json!("c"), json!("d"), json!("e")];
4795 let result = ToolGenerator::format_examples_for_description(&examples);
4796 assert_eq!(
4797 result,
4798 Some("Examples:\n- `\"a\"`\n- `\"b\"`\n- `\"c\"`\n- `\"d\"`\n- `\"e\"`".to_string())
4799 );
4800
4801 let examples = vec![json!(3.5)];
4803 let result = ToolGenerator::format_examples_for_description(&examples);
4804 assert_eq!(result, Some("Example: `3.5`".to_string()));
4805
4806 let examples = vec![json!(-42)];
4808 let result = ToolGenerator::format_examples_for_description(&examples);
4809 assert_eq!(result, Some("Example: `-42`".to_string()));
4810
4811 let examples = vec![json!(false)];
4813 let result = ToolGenerator::format_examples_for_description(&examples);
4814 assert_eq!(result, Some("Example: `false`".to_string()));
4815
4816 let examples = vec![json!("hello \"world\"")];
4818 let result = ToolGenerator::format_examples_for_description(&examples);
4819 assert_eq!(result, Some(r#"Example: `"hello \"world\""`"#.to_string()));
4821
4822 let examples = vec![json!("")];
4824 let result = ToolGenerator::format_examples_for_description(&examples);
4825 assert_eq!(result, Some("Example: `\"\"`".to_string()));
4826
4827 let examples = vec![json!([])];
4829 let result = ToolGenerator::format_examples_for_description(&examples);
4830 assert_eq!(result, Some("Example: `[]`".to_string()));
4831
4832 let examples = vec![json!({})];
4834 let result = ToolGenerator::format_examples_for_description(&examples);
4835 assert_eq!(result, Some("Example: `{}`".to_string()));
4836 }
4837
4838 #[test]
4839 fn test_reference_metadata_functionality() {
4840 let metadata = ReferenceMetadata::new(
4842 Some("User Reference".to_string()),
4843 Some("A reference to user data with additional context".to_string()),
4844 );
4845
4846 assert!(!metadata.is_empty());
4847 assert_eq!(metadata.summary(), Some("User Reference"));
4848 assert_eq!(
4849 metadata.best_description(),
4850 Some("A reference to user data with additional context")
4851 );
4852
4853 let summary_only = ReferenceMetadata::new(Some("Pet Summary".to_string()), None);
4855 assert_eq!(summary_only.best_description(), Some("Pet Summary"));
4856
4857 let empty_metadata = ReferenceMetadata::new(None, None);
4859 assert!(empty_metadata.is_empty());
4860 assert_eq!(empty_metadata.best_description(), None);
4861
4862 let metadata = ReferenceMetadata::new(
4864 Some("Reference Summary".to_string()),
4865 Some("Reference Description".to_string()),
4866 );
4867
4868 let result = metadata.merge_with_description(None, false);
4870 assert_eq!(result, Some("Reference Description".to_string()));
4871
4872 let result = metadata.merge_with_description(Some("Existing desc"), false);
4874 assert_eq!(result, Some("Reference Description".to_string()));
4875
4876 let result = metadata.merge_with_description(Some("Existing desc"), true);
4878 assert_eq!(result, Some("Reference Description".to_string()));
4879
4880 let result = metadata.enhance_parameter_description("userId", Some("User ID parameter"));
4882 assert_eq!(result, Some("userId: Reference Description".to_string()));
4883
4884 let result = metadata.enhance_parameter_description("userId", None);
4885 assert_eq!(result, Some("userId: Reference Description".to_string()));
4886
4887 let summary_only = ReferenceMetadata::new(Some("API Token".to_string()), None);
4889
4890 let result = summary_only.merge_with_description(Some("Generic token"), false);
4891 assert_eq!(result, Some("API Token".to_string()));
4892
4893 let result = summary_only.merge_with_description(Some("Different desc"), true);
4894 assert_eq!(result, Some("API Token".to_string())); let result = summary_only.enhance_parameter_description("token", Some("Token field"));
4897 assert_eq!(result, Some("token: API Token".to_string()));
4898
4899 let empty_meta = ReferenceMetadata::new(None, None);
4901
4902 let result = empty_meta.merge_with_description(Some("Schema description"), false);
4903 assert_eq!(result, Some("Schema description".to_string()));
4904
4905 let result = empty_meta.enhance_parameter_description("param", Some("Schema param"));
4906 assert_eq!(result, Some("Schema param".to_string()));
4907
4908 let result = empty_meta.enhance_parameter_description("param", None);
4909 assert_eq!(result, Some("param parameter".to_string()));
4910 }
4911
4912 #[test]
4913 fn test_parameter_schema_with_reference_metadata() {
4914 let mut spec = create_test_spec();
4915
4916 spec.components.as_mut().unwrap().schemas.insert(
4918 "Pet".to_string(),
4919 ObjectOrReference::Object(ObjectSchema {
4920 description: None, schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4922 ..Default::default()
4923 }),
4924 );
4925
4926 let param_with_ref = Parameter {
4928 name: "user".to_string(),
4929 location: ParameterIn::Query,
4930 description: None,
4931 required: Some(true),
4932 deprecated: Some(false),
4933 allow_empty_value: Some(false),
4934 style: None,
4935 explode: None,
4936 allow_reserved: Some(false),
4937 schema: Some(ObjectOrReference::Ref {
4938 ref_path: "#/components/schemas/Pet".to_string(),
4939 summary: Some("Pet Reference".to_string()),
4940 description: Some("A reference to pet schema with additional context".to_string()),
4941 }),
4942 example: None,
4943 examples: BTreeMap::new(),
4944 content: None,
4945 extensions: Default::default(),
4946 };
4947
4948 let result = ToolGenerator::convert_parameter_schema(
4950 ¶m_with_ref,
4951 ParameterIn::Query,
4952 &spec,
4953 false,
4954 );
4955
4956 assert!(result.is_ok());
4957 let (schema, _annotations) = result.unwrap();
4958
4959 let description = schema.get("description").and_then(|v| v.as_str());
4961 assert!(description.is_some());
4962 assert!(
4964 description.unwrap().contains("Pet Reference")
4965 || description
4966 .unwrap()
4967 .contains("A reference to pet schema with additional context")
4968 );
4969 }
4970
4971 #[test]
4972 fn test_request_body_with_reference_metadata() {
4973 let spec = create_test_spec();
4974
4975 let request_body_ref = ObjectOrReference::Ref {
4977 ref_path: "#/components/requestBodies/PetBody".to_string(),
4978 summary: Some("Pet Request Body".to_string()),
4979 description: Some(
4980 "Request body containing pet information for API operations".to_string(),
4981 ),
4982 };
4983
4984 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body_ref, &spec);
4985
4986 assert!(result.is_ok());
4987 let schema_result = result.unwrap();
4988 assert!(schema_result.is_some());
4989
4990 let (schema, _annotations, _required) = schema_result.unwrap();
4991 let description = schema.get("description").and_then(|v| v.as_str());
4992
4993 assert!(description.is_some());
4994 assert_eq!(
4996 description.unwrap(),
4997 "Request body containing pet information for API operations"
4998 );
4999 }
5000
5001 #[test]
5002 fn test_response_schema_with_reference_metadata() {
5003 let spec = create_test_spec();
5004
5005 let mut responses = BTreeMap::new();
5007 responses.insert(
5008 "200".to_string(),
5009 ObjectOrReference::Ref {
5010 ref_path: "#/components/responses/PetResponse".to_string(),
5011 summary: Some("Successful Pet Response".to_string()),
5012 description: Some(
5013 "Response containing pet data on successful operation".to_string(),
5014 ),
5015 },
5016 );
5017 let responses_option = Some(responses);
5018
5019 let result = ToolGenerator::extract_output_schema(&responses_option, &spec);
5020
5021 assert!(result.is_ok());
5022 let schema = result.unwrap();
5023 assert!(schema.is_some());
5024
5025 let schema_value = schema.unwrap();
5026 let body_desc = schema_value
5027 .get("properties")
5028 .and_then(|props| props.get("body"))
5029 .and_then(|body| body.get("description"))
5030 .and_then(|desc| desc.as_str());
5031
5032 assert!(body_desc.is_some());
5033 assert_eq!(
5035 body_desc.unwrap(),
5036 "Response containing pet data on successful operation"
5037 );
5038 }
5039}