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