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