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 ) -> Result<ToolMetadata, Error> {
742 let name = operation.operation_id.clone().unwrap_or_else(|| {
743 format!(
744 "{}_{}",
745 method,
746 path.replace('/', "_").replace(['{', '}'], "")
747 )
748 });
749
750 let parameters = Self::generate_parameter_schema(
752 &operation.parameters,
753 &method,
754 &operation.request_body,
755 spec,
756 )?;
757
758 let description = Self::build_description(operation, &method, &path);
760
761 let output_schema = Self::extract_output_schema(&operation.responses, spec)?;
763
764 Ok(ToolMetadata {
765 name,
766 title: operation.summary.clone(),
767 description,
768 parameters,
769 output_schema,
770 method,
771 path,
772 security: None, })
774 }
775
776 pub fn generate_openapi_tools(
782 tools_metadata: Vec<ToolMetadata>,
783 base_url: Option<url::Url>,
784 default_headers: Option<reqwest::header::HeaderMap>,
785 ) -> Result<Vec<crate::tool::Tool>, Error> {
786 let mut openapi_tools = Vec::with_capacity(tools_metadata.len());
787
788 for metadata in tools_metadata {
789 let tool = crate::tool::Tool::new(metadata, base_url.clone(), default_headers.clone())?;
790 openapi_tools.push(tool);
791 }
792
793 Ok(openapi_tools)
794 }
795
796 fn build_description(operation: &Operation, method: &str, path: &str) -> String {
798 match (&operation.summary, &operation.description) {
799 (Some(summary), Some(desc)) => {
800 format!(
801 "{}\n\n{}\n\nEndpoint: {} {}",
802 summary,
803 desc,
804 method.to_uppercase(),
805 path
806 )
807 }
808 (Some(summary), None) => {
809 format!(
810 "{}\n\nEndpoint: {} {}",
811 summary,
812 method.to_uppercase(),
813 path
814 )
815 }
816 (None, Some(desc)) => {
817 format!("{}\n\nEndpoint: {} {}", desc, method.to_uppercase(), path)
818 }
819 (None, None) => {
820 format!("API endpoint: {} {}", method.to_uppercase(), path)
821 }
822 }
823 }
824
825 fn extract_output_schema(
829 responses: &Option<BTreeMap<String, ObjectOrReference<Response>>>,
830 spec: &Spec,
831 ) -> Result<Option<Value>, Error> {
832 let responses = match responses {
833 Some(r) => r,
834 None => return Ok(None),
835 };
836 let priority_codes = vec![
838 "200", "201", "202", "203", "204", "2XX", "default", ];
846
847 for status_code in priority_codes {
848 if let Some(response_or_ref) = responses.get(status_code) {
849 let response = match response_or_ref {
851 ObjectOrReference::Object(response) => response,
852 ObjectOrReference::Ref {
853 ref_path,
854 summary,
855 description,
856 } => {
857 let ref_metadata =
860 ReferenceMetadata::new(summary.clone(), description.clone());
861
862 if let Some(ref_desc) = ref_metadata.best_description() {
863 let response_schema = json!({
865 "type": "object",
866 "description": "Unified response structure with success and error variants",
867 "properties": {
868 "status_code": {
869 "type": "integer",
870 "description": "HTTP status code"
871 },
872 "body": {
873 "type": "object",
874 "description": ref_desc,
875 "additionalProperties": true
876 }
877 },
878 "required": ["status_code", "body"]
879 });
880
881 trace!(
882 reference_path = %ref_path,
883 reference_description = %ref_desc,
884 "Created response schema using reference metadata"
885 );
886
887 return Ok(Some(response_schema));
888 }
889
890 continue;
892 }
893 };
894
895 if status_code == "204" {
897 continue;
898 }
899
900 if !response.content.is_empty() {
902 let content = &response.content;
903 let json_media_types = vec![
905 "application/json",
906 "application/ld+json",
907 "application/vnd.api+json",
908 ];
909
910 for media_type_str in json_media_types {
911 if let Some(media_type) = content.get(media_type_str)
912 && let Some(schema_or_ref) = &media_type.schema
913 {
914 let wrapped_schema = Self::wrap_output_schema(schema_or_ref, spec)?;
916 return Ok(Some(wrapped_schema));
917 }
918 }
919
920 for media_type in content.values() {
922 if let Some(schema_or_ref) = &media_type.schema {
923 let wrapped_schema = Self::wrap_output_schema(schema_or_ref, spec)?;
925 return Ok(Some(wrapped_schema));
926 }
927 }
928 }
929 }
930 }
931
932 Ok(None)
934 }
935
936 fn convert_schema_to_json_schema(
946 schema: &Schema,
947 spec: &Spec,
948 visited: &mut HashSet<String>,
949 ) -> Result<Value, Error> {
950 match schema {
951 Schema::Object(obj_schema_or_ref) => match obj_schema_or_ref.as_ref() {
952 ObjectOrReference::Object(obj_schema) => {
953 Self::convert_object_schema_to_json_schema(obj_schema, spec, visited)
954 }
955 ObjectOrReference::Ref { ref_path, .. } => {
956 let resolved = Self::resolve_reference(ref_path, spec, visited)?;
957 Self::convert_object_schema_to_json_schema(&resolved, spec, visited)
958 }
959 },
960 Schema::Boolean(bool_schema) => {
961 if bool_schema.0 {
963 Ok(json!({})) } else {
965 Ok(json!({"not": {}})) }
967 }
968 }
969 }
970
971 fn convert_object_schema_to_json_schema(
981 obj_schema: &ObjectSchema,
982 spec: &Spec,
983 visited: &mut HashSet<String>,
984 ) -> Result<Value, Error> {
985 let mut schema_obj = serde_json::Map::new();
986
987 if let Some(schema_type) = &obj_schema.schema_type {
989 match schema_type {
990 SchemaTypeSet::Single(single_type) => {
991 schema_obj.insert(
992 "type".to_string(),
993 json!(Self::schema_type_to_string(single_type)),
994 );
995 }
996 SchemaTypeSet::Multiple(type_set) => {
997 let types: Vec<String> =
998 type_set.iter().map(Self::schema_type_to_string).collect();
999 schema_obj.insert("type".to_string(), json!(types));
1000 }
1001 }
1002 }
1003
1004 if let Some(desc) = &obj_schema.description {
1006 schema_obj.insert("description".to_string(), json!(desc));
1007 }
1008
1009 if !obj_schema.one_of.is_empty() {
1011 let mut one_of_schemas = Vec::new();
1012 for schema_ref in &obj_schema.one_of {
1013 let schema_json = match schema_ref {
1014 ObjectOrReference::Object(schema) => {
1015 Self::convert_object_schema_to_json_schema(schema, spec, visited)?
1016 }
1017 ObjectOrReference::Ref { ref_path, .. } => {
1018 let resolved = Self::resolve_reference(ref_path, spec, visited)?;
1019 Self::convert_object_schema_to_json_schema(&resolved, spec, visited)?
1020 }
1021 };
1022 one_of_schemas.push(schema_json);
1023 }
1024 schema_obj.insert("oneOf".to_string(), json!(one_of_schemas));
1025 return Ok(Value::Object(schema_obj));
1028 }
1029
1030 if !obj_schema.properties.is_empty() {
1032 let properties = &obj_schema.properties;
1033 let mut props_map = serde_json::Map::new();
1034 for (prop_name, prop_schema_or_ref) in properties {
1035 let prop_schema = match prop_schema_or_ref {
1036 ObjectOrReference::Object(schema) => {
1037 Self::convert_schema_to_json_schema(
1039 &Schema::Object(Box::new(ObjectOrReference::Object(schema.clone()))),
1040 spec,
1041 visited,
1042 )?
1043 }
1044 ObjectOrReference::Ref { ref_path, .. } => {
1045 let resolved = Self::resolve_reference(ref_path, spec, visited)?;
1046 Self::convert_object_schema_to_json_schema(&resolved, spec, visited)?
1047 }
1048 };
1049
1050 let sanitized_name = sanitize_property_name(prop_name);
1052 if sanitized_name != *prop_name {
1053 let annotations = Annotations::new().with_original_name(prop_name.clone());
1055 let prop_with_annotation =
1056 Self::apply_annotations_to_schema(prop_schema, annotations);
1057 props_map.insert(sanitized_name, prop_with_annotation);
1058 } else {
1059 props_map.insert(prop_name.clone(), prop_schema);
1060 }
1061 }
1062 schema_obj.insert("properties".to_string(), Value::Object(props_map));
1063 }
1064
1065 if !obj_schema.required.is_empty() {
1067 schema_obj.insert("required".to_string(), json!(&obj_schema.required));
1068 }
1069
1070 if let Some(schema_type) = &obj_schema.schema_type
1072 && matches!(schema_type, SchemaTypeSet::Single(SchemaType::Object))
1073 {
1074 match &obj_schema.additional_properties {
1076 None => {
1077 schema_obj.insert("additionalProperties".to_string(), json!(true));
1079 }
1080 Some(Schema::Boolean(BooleanSchema(value))) => {
1081 schema_obj.insert("additionalProperties".to_string(), json!(value));
1083 }
1084 Some(Schema::Object(schema_ref)) => {
1085 let mut visited = HashSet::new();
1087 let additional_props_schema = Self::convert_schema_to_json_schema(
1088 &Schema::Object(schema_ref.clone()),
1089 spec,
1090 &mut visited,
1091 )?;
1092 schema_obj.insert("additionalProperties".to_string(), additional_props_schema);
1093 }
1094 }
1095 }
1096
1097 if let Some(schema_type) = &obj_schema.schema_type {
1099 if matches!(schema_type, SchemaTypeSet::Single(SchemaType::Array)) {
1100 if !obj_schema.prefix_items.is_empty() {
1102 Self::convert_prefix_items_to_draft07(
1104 &obj_schema.prefix_items,
1105 &obj_schema.items,
1106 &mut schema_obj,
1107 spec,
1108 )?;
1109 } else if let Some(items_schema) = &obj_schema.items {
1110 let items_json =
1112 Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
1113 schema_obj.insert("items".to_string(), items_json);
1114 }
1115
1116 if let Some(min_items) = obj_schema.min_items {
1118 schema_obj.insert("minItems".to_string(), json!(min_items));
1119 }
1120 if let Some(max_items) = obj_schema.max_items {
1121 schema_obj.insert("maxItems".to_string(), json!(max_items));
1122 }
1123 } else if let Some(items_schema) = &obj_schema.items {
1124 let items_json = Self::convert_schema_to_json_schema(items_schema, spec, visited)?;
1126 schema_obj.insert("items".to_string(), items_json);
1127 }
1128 }
1129
1130 if let Some(format) = &obj_schema.format {
1132 schema_obj.insert("format".to_string(), json!(format));
1133 }
1134
1135 if let Some(example) = &obj_schema.example {
1136 schema_obj.insert("example".to_string(), example.clone());
1137 }
1138
1139 if let Some(default) = &obj_schema.default {
1140 schema_obj.insert("default".to_string(), default.clone());
1141 }
1142
1143 if !obj_schema.enum_values.is_empty() {
1144 schema_obj.insert("enum".to_string(), json!(&obj_schema.enum_values));
1145 }
1146
1147 if let Some(min) = &obj_schema.minimum {
1148 schema_obj.insert("minimum".to_string(), json!(min));
1149 }
1150
1151 if let Some(max) = &obj_schema.maximum {
1152 schema_obj.insert("maximum".to_string(), json!(max));
1153 }
1154
1155 if let Some(min_length) = &obj_schema.min_length {
1156 schema_obj.insert("minLength".to_string(), json!(min_length));
1157 }
1158
1159 if let Some(max_length) = &obj_schema.max_length {
1160 schema_obj.insert("maxLength".to_string(), json!(max_length));
1161 }
1162
1163 if let Some(pattern) = &obj_schema.pattern {
1164 schema_obj.insert("pattern".to_string(), json!(pattern));
1165 }
1166
1167 Ok(Value::Object(schema_obj))
1168 }
1169
1170 fn schema_type_to_string(schema_type: &SchemaType) -> String {
1172 match schema_type {
1173 SchemaType::Boolean => "boolean",
1174 SchemaType::Integer => "integer",
1175 SchemaType::Number => "number",
1176 SchemaType::String => "string",
1177 SchemaType::Array => "array",
1178 SchemaType::Object => "object",
1179 SchemaType::Null => "null",
1180 }
1181 .to_string()
1182 }
1183
1184 fn resolve_reference(
1194 ref_path: &str,
1195 spec: &Spec,
1196 visited: &mut HashSet<String>,
1197 ) -> Result<ObjectSchema, Error> {
1198 if visited.contains(ref_path) {
1200 return Err(Error::ToolGeneration(format!(
1201 "Circular reference detected: {ref_path}"
1202 )));
1203 }
1204
1205 visited.insert(ref_path.to_string());
1207
1208 if !ref_path.starts_with("#/components/schemas/") {
1211 return Err(Error::ToolGeneration(format!(
1212 "Unsupported reference format: {ref_path}. Only #/components/schemas/ references are supported"
1213 )));
1214 }
1215
1216 let schema_name = ref_path.strip_prefix("#/components/schemas/").unwrap();
1217
1218 let components = spec.components.as_ref().ok_or_else(|| {
1220 Error::ToolGeneration(format!(
1221 "Reference {ref_path} points to components, but spec has no components section"
1222 ))
1223 })?;
1224
1225 let schema_ref = components.schemas.get(schema_name).ok_or_else(|| {
1226 Error::ToolGeneration(format!(
1227 "Schema '{schema_name}' not found in components/schemas"
1228 ))
1229 })?;
1230
1231 let resolved_schema = match schema_ref {
1233 ObjectOrReference::Object(obj_schema) => obj_schema.clone(),
1234 ObjectOrReference::Ref {
1235 ref_path: nested_ref,
1236 ..
1237 } => {
1238 Self::resolve_reference(nested_ref, spec, visited)?
1240 }
1241 };
1242
1243 visited.remove(ref_path);
1245
1246 Ok(resolved_schema)
1247 }
1248
1249 fn resolve_reference_with_metadata(
1254 ref_path: &str,
1255 summary: Option<String>,
1256 description: Option<String>,
1257 spec: &Spec,
1258 visited: &mut HashSet<String>,
1259 ) -> Result<(ObjectSchema, ReferenceMetadata), Error> {
1260 let resolved_schema = Self::resolve_reference(ref_path, spec, visited)?;
1261 let metadata = ReferenceMetadata::new(summary, description);
1262 Ok((resolved_schema, metadata))
1263 }
1264
1265 fn generate_parameter_schema(
1267 parameters: &[ObjectOrReference<Parameter>],
1268 _method: &str,
1269 request_body: &Option<ObjectOrReference<RequestBody>>,
1270 spec: &Spec,
1271 ) -> Result<Value, Error> {
1272 let mut properties = serde_json::Map::new();
1273 let mut required = Vec::new();
1274
1275 let mut path_params = Vec::new();
1277 let mut query_params = Vec::new();
1278 let mut header_params = Vec::new();
1279 let mut cookie_params = Vec::new();
1280
1281 for param_ref in parameters {
1282 let param = match param_ref {
1283 ObjectOrReference::Object(param) => param,
1284 ObjectOrReference::Ref { ref_path, .. } => {
1285 warn!(
1289 reference_path = %ref_path,
1290 "Parameter reference not resolved"
1291 );
1292 continue;
1293 }
1294 };
1295
1296 match ¶m.location {
1297 ParameterIn::Query => query_params.push(param),
1298 ParameterIn::Header => header_params.push(param),
1299 ParameterIn::Path => path_params.push(param),
1300 ParameterIn::Cookie => cookie_params.push(param),
1301 }
1302 }
1303
1304 for param in path_params {
1306 let (param_schema, mut annotations) =
1307 Self::convert_parameter_schema(param, ParameterIn::Path, spec)?;
1308
1309 let sanitized_name = sanitize_property_name(¶m.name);
1311 if sanitized_name != param.name {
1312 annotations = annotations.with_original_name(param.name.clone());
1313 }
1314
1315 let param_schema_with_annotations =
1316 Self::apply_annotations_to_schema(param_schema, annotations);
1317 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
1318 required.push(sanitized_name);
1319 }
1320
1321 for param in &query_params {
1323 let (param_schema, mut annotations) =
1324 Self::convert_parameter_schema(param, ParameterIn::Query, spec)?;
1325
1326 let sanitized_name = sanitize_property_name(¶m.name);
1328 if sanitized_name != param.name {
1329 annotations = annotations.with_original_name(param.name.clone());
1330 }
1331
1332 let param_schema_with_annotations =
1333 Self::apply_annotations_to_schema(param_schema, annotations);
1334 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
1335 if param.required.unwrap_or(false) {
1336 required.push(sanitized_name);
1337 }
1338 }
1339
1340 for param in &header_params {
1342 let (param_schema, mut annotations) =
1343 Self::convert_parameter_schema(param, ParameterIn::Header, spec)?;
1344
1345 let prefixed_name = format!("header_{}", param.name);
1347 let sanitized_name = sanitize_property_name(&prefixed_name);
1348 if sanitized_name != prefixed_name {
1349 annotations = annotations.with_original_name(param.name.clone());
1350 }
1351
1352 let param_schema_with_annotations =
1353 Self::apply_annotations_to_schema(param_schema, annotations);
1354
1355 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
1356 if param.required.unwrap_or(false) {
1357 required.push(sanitized_name);
1358 }
1359 }
1360
1361 for param in &cookie_params {
1363 let (param_schema, mut annotations) =
1364 Self::convert_parameter_schema(param, ParameterIn::Cookie, spec)?;
1365
1366 let prefixed_name = format!("cookie_{}", param.name);
1368 let sanitized_name = sanitize_property_name(&prefixed_name);
1369 if sanitized_name != prefixed_name {
1370 annotations = annotations.with_original_name(param.name.clone());
1371 }
1372
1373 let param_schema_with_annotations =
1374 Self::apply_annotations_to_schema(param_schema, annotations);
1375
1376 properties.insert(sanitized_name.clone(), param_schema_with_annotations);
1377 if param.required.unwrap_or(false) {
1378 required.push(sanitized_name);
1379 }
1380 }
1381
1382 if let Some(request_body) = request_body
1384 && let Some((body_schema, annotations, is_required)) =
1385 Self::convert_request_body_to_json_schema(request_body, spec)?
1386 {
1387 let body_schema_with_annotations =
1388 Self::apply_annotations_to_schema(body_schema, annotations);
1389 properties.insert("request_body".to_string(), body_schema_with_annotations);
1390 if is_required {
1391 required.push("request_body".to_string());
1392 }
1393 }
1394
1395 if !query_params.is_empty() || !header_params.is_empty() || !cookie_params.is_empty() {
1397 properties.insert(
1399 "timeout_seconds".to_string(),
1400 json!({
1401 "type": "integer",
1402 "description": "Request timeout in seconds",
1403 "minimum": 1,
1404 "maximum": 300,
1405 "default": 30
1406 }),
1407 );
1408 }
1409
1410 Ok(json!({
1411 "type": "object",
1412 "properties": properties,
1413 "required": required,
1414 "additionalProperties": false
1415 }))
1416 }
1417
1418 fn convert_parameter_schema(
1420 param: &Parameter,
1421 location: ParameterIn,
1422 spec: &Spec,
1423 ) -> Result<(Value, Annotations), Error> {
1424 let base_schema = if let Some(schema_ref) = ¶m.schema {
1426 match schema_ref {
1427 ObjectOrReference::Object(obj_schema) => {
1428 let mut visited = HashSet::new();
1429 Self::convert_schema_to_json_schema(
1430 &Schema::Object(Box::new(ObjectOrReference::Object(obj_schema.clone()))),
1431 spec,
1432 &mut visited,
1433 )?
1434 }
1435 ObjectOrReference::Ref {
1436 ref_path,
1437 summary,
1438 description,
1439 } => {
1440 let mut visited = HashSet::new();
1442 match Self::resolve_reference_with_metadata(
1443 ref_path,
1444 summary.clone(),
1445 description.clone(),
1446 spec,
1447 &mut visited,
1448 ) {
1449 Ok((resolved_schema, ref_metadata)) => {
1450 let mut schema_json = Self::convert_schema_to_json_schema(
1451 &Schema::Object(Box::new(ObjectOrReference::Object(
1452 resolved_schema,
1453 ))),
1454 spec,
1455 &mut visited,
1456 )?;
1457
1458 if let Value::Object(ref mut schema_obj) = schema_json {
1460 if let Some(ref_desc) = ref_metadata.best_description() {
1462 schema_obj.insert("description".to_string(), json!(ref_desc));
1463 }
1464 }
1467
1468 schema_json
1469 }
1470 Err(_) => {
1471 json!({"type": "string"})
1473 }
1474 }
1475 }
1476 }
1477 } else {
1478 json!({"type": "string"})
1480 };
1481
1482 let mut result = match base_schema {
1484 Value::Object(obj) => obj,
1485 _ => {
1486 return Err(Error::ToolGeneration(format!(
1488 "Internal error: schema converter returned non-object for parameter '{}'",
1489 param.name
1490 )));
1491 }
1492 };
1493
1494 let mut collected_examples = Vec::new();
1496
1497 if let Some(example) = ¶m.example {
1499 collected_examples.push(example.clone());
1500 } else if !param.examples.is_empty() {
1501 for example_ref in param.examples.values() {
1503 match example_ref {
1504 ObjectOrReference::Object(example_obj) => {
1505 if let Some(value) = &example_obj.value {
1506 collected_examples.push(value.clone());
1507 }
1508 }
1509 ObjectOrReference::Ref { .. } => {
1510 }
1512 }
1513 }
1514 } else if let Some(Value::String(ex_str)) = result.get("example") {
1515 collected_examples.push(json!(ex_str));
1517 } else if let Some(ex) = result.get("example") {
1518 collected_examples.push(ex.clone());
1519 }
1520
1521 let base_description = param
1523 .description
1524 .as_ref()
1525 .map(|d| d.to_string())
1526 .or_else(|| {
1527 result
1528 .get("description")
1529 .and_then(|d| d.as_str())
1530 .map(|d| d.to_string())
1531 })
1532 .unwrap_or_else(|| format!("{} parameter", param.name));
1533
1534 let description_with_examples = if let Some(examples_str) =
1535 Self::format_examples_for_description(&collected_examples)
1536 {
1537 format!("{base_description}. {examples_str}")
1538 } else {
1539 base_description
1540 };
1541
1542 result.insert("description".to_string(), json!(description_with_examples));
1543
1544 if let Some(example) = ¶m.example {
1549 result.insert("example".to_string(), example.clone());
1550 } else if !param.examples.is_empty() {
1551 let mut examples_array = Vec::new();
1554 for (example_name, example_ref) in ¶m.examples {
1555 match example_ref {
1556 ObjectOrReference::Object(example_obj) => {
1557 if let Some(value) = &example_obj.value {
1558 examples_array.push(json!({
1559 "name": example_name,
1560 "value": value
1561 }));
1562 }
1563 }
1564 ObjectOrReference::Ref { .. } => {
1565 }
1568 }
1569 }
1570
1571 if !examples_array.is_empty() {
1572 if let Some(first_example) = examples_array.first()
1574 && let Some(value) = first_example.get("value")
1575 {
1576 result.insert("example".to_string(), value.clone());
1577 }
1578 result.insert("x-examples".to_string(), json!(examples_array));
1580 }
1581 }
1582
1583 let mut annotations = Annotations::new()
1585 .with_location(Location::Parameter(location))
1586 .with_required(param.required.unwrap_or(false));
1587
1588 if let Some(explode) = param.explode {
1590 annotations = annotations.with_explode(explode);
1591 } else {
1592 let default_explode = match ¶m.style {
1596 Some(ParameterStyle::Form) | None => true, _ => false,
1598 };
1599 annotations = annotations.with_explode(default_explode);
1600 }
1601
1602 Ok((Value::Object(result), annotations))
1603 }
1604
1605 fn apply_annotations_to_schema(schema: Value, annotations: Annotations) -> Value {
1607 match schema {
1608 Value::Object(mut obj) => {
1609 if let Ok(Value::Object(ann_map)) = serde_json::to_value(&annotations) {
1611 for (key, value) in ann_map {
1612 obj.insert(key, value);
1613 }
1614 }
1615 Value::Object(obj)
1616 }
1617 _ => schema,
1618 }
1619 }
1620
1621 fn format_examples_for_description(examples: &[Value]) -> Option<String> {
1623 if examples.is_empty() {
1624 return None;
1625 }
1626
1627 if examples.len() == 1 {
1628 let example_str =
1629 serde_json::to_string(&examples[0]).unwrap_or_else(|_| "null".to_string());
1630 Some(format!("Example: `{example_str}`"))
1631 } else {
1632 let mut result = String::from("Examples:\n");
1633 for ex in examples {
1634 let json_str = serde_json::to_string(ex).unwrap_or_else(|_| "null".to_string());
1635 result.push_str(&format!("- `{json_str}`\n"));
1636 }
1637 result.pop();
1639 Some(result)
1640 }
1641 }
1642
1643 fn convert_prefix_items_to_draft07(
1654 prefix_items: &[ObjectOrReference<ObjectSchema>],
1655 items: &Option<Box<Schema>>,
1656 result: &mut serde_json::Map<String, Value>,
1657 spec: &Spec,
1658 ) -> Result<(), Error> {
1659 let prefix_count = prefix_items.len();
1660
1661 let mut item_types = Vec::new();
1663 for prefix_item in prefix_items {
1664 match prefix_item {
1665 ObjectOrReference::Object(obj_schema) => {
1666 if let Some(schema_type) = &obj_schema.schema_type {
1667 match schema_type {
1668 SchemaTypeSet::Single(SchemaType::String) => item_types.push("string"),
1669 SchemaTypeSet::Single(SchemaType::Integer) => {
1670 item_types.push("integer")
1671 }
1672 SchemaTypeSet::Single(SchemaType::Number) => item_types.push("number"),
1673 SchemaTypeSet::Single(SchemaType::Boolean) => {
1674 item_types.push("boolean")
1675 }
1676 SchemaTypeSet::Single(SchemaType::Array) => item_types.push("array"),
1677 SchemaTypeSet::Single(SchemaType::Object) => item_types.push("object"),
1678 _ => item_types.push("string"), }
1680 } else {
1681 item_types.push("string"); }
1683 }
1684 ObjectOrReference::Ref { ref_path, .. } => {
1685 let mut visited = HashSet::new();
1687 match Self::resolve_reference(ref_path, spec, &mut visited) {
1688 Ok(resolved_schema) => {
1689 if let Some(schema_type_set) = &resolved_schema.schema_type {
1691 match schema_type_set {
1692 SchemaTypeSet::Single(SchemaType::String) => {
1693 item_types.push("string")
1694 }
1695 SchemaTypeSet::Single(SchemaType::Integer) => {
1696 item_types.push("integer")
1697 }
1698 SchemaTypeSet::Single(SchemaType::Number) => {
1699 item_types.push("number")
1700 }
1701 SchemaTypeSet::Single(SchemaType::Boolean) => {
1702 item_types.push("boolean")
1703 }
1704 SchemaTypeSet::Single(SchemaType::Array) => {
1705 item_types.push("array")
1706 }
1707 SchemaTypeSet::Single(SchemaType::Object) => {
1708 item_types.push("object")
1709 }
1710 _ => item_types.push("string"), }
1712 } else {
1713 item_types.push("string"); }
1715 }
1716 Err(_) => {
1717 item_types.push("string");
1719 }
1720 }
1721 }
1722 }
1723 }
1724
1725 let items_is_false =
1727 matches!(items.as_ref().map(|i| i.as_ref()), Some(Schema::Boolean(b)) if !b.0);
1728
1729 if items_is_false {
1730 result.insert("minItems".to_string(), json!(prefix_count));
1732 result.insert("maxItems".to_string(), json!(prefix_count));
1733 }
1734
1735 let unique_types: std::collections::BTreeSet<_> = item_types.into_iter().collect();
1737
1738 if unique_types.len() == 1 {
1739 let item_type = unique_types.into_iter().next().unwrap();
1741 result.insert("items".to_string(), json!({"type": item_type}));
1742 } else if unique_types.len() > 1 {
1743 let one_of: Vec<Value> = unique_types
1745 .into_iter()
1746 .map(|t| json!({"type": t}))
1747 .collect();
1748 result.insert("items".to_string(), json!({"oneOf": one_of}));
1749 }
1750
1751 Ok(())
1752 }
1753
1754 fn convert_request_body_to_json_schema(
1766 request_body_ref: &ObjectOrReference<RequestBody>,
1767 spec: &Spec,
1768 ) -> Result<Option<(Value, Annotations, bool)>, Error> {
1769 match request_body_ref {
1770 ObjectOrReference::Object(request_body) => {
1771 let schema_info = request_body
1774 .content
1775 .get(mime::APPLICATION_JSON.as_ref())
1776 .or_else(|| request_body.content.get("application/json"))
1777 .or_else(|| {
1778 request_body.content.values().next()
1780 });
1781
1782 if let Some(media_type) = schema_info {
1783 if let Some(schema_ref) = &media_type.schema {
1784 let schema = Schema::Object(Box::new(schema_ref.clone()));
1786
1787 let mut visited = HashSet::new();
1789 let converted_schema =
1790 Self::convert_schema_to_json_schema(&schema, spec, &mut visited)?;
1791
1792 let mut schema_obj = match converted_schema {
1794 Value::Object(obj) => obj,
1795 _ => {
1796 let mut obj = serde_json::Map::new();
1798 obj.insert("type".to_string(), json!("object"));
1799 obj.insert("additionalProperties".to_string(), json!(true));
1800 obj
1801 }
1802 };
1803
1804 if !schema_obj.contains_key("description") {
1806 let description = request_body
1807 .description
1808 .clone()
1809 .unwrap_or_else(|| "Request body data".to_string());
1810 schema_obj.insert("description".to_string(), json!(description));
1811 }
1812
1813 let annotations = Annotations::new()
1815 .with_location(Location::Body)
1816 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
1817
1818 let required = request_body.required.unwrap_or(false);
1819 Ok(Some((Value::Object(schema_obj), annotations, required)))
1820 } else {
1821 Ok(None)
1822 }
1823 } else {
1824 Ok(None)
1825 }
1826 }
1827 ObjectOrReference::Ref {
1828 ref_path: _,
1829 summary,
1830 description,
1831 } => {
1832 let ref_metadata = ReferenceMetadata::new(summary.clone(), description.clone());
1834 let enhanced_description = ref_metadata
1835 .best_description()
1836 .map(|desc| desc.to_string())
1837 .unwrap_or_else(|| "Request body data".to_string());
1838
1839 let mut result = serde_json::Map::new();
1840 result.insert("type".to_string(), json!("object"));
1841 result.insert("additionalProperties".to_string(), json!(true));
1842 result.insert("description".to_string(), json!(enhanced_description));
1843
1844 let annotations = Annotations::new()
1846 .with_location(Location::Body)
1847 .with_content_type(mime::APPLICATION_JSON.as_ref().to_string());
1848
1849 Ok(Some((Value::Object(result), annotations, false)))
1850 }
1851 }
1852 }
1853
1854 pub fn extract_parameters(
1860 tool_metadata: &ToolMetadata,
1861 arguments: &Value,
1862 ) -> Result<ExtractedParameters, ToolCallValidationError> {
1863 let args = arguments.as_object().ok_or_else(|| {
1864 ToolCallValidationError::RequestConstructionError {
1865 reason: "Arguments must be an object".to_string(),
1866 }
1867 })?;
1868
1869 trace!(
1870 tool_name = %tool_metadata.name,
1871 raw_arguments = ?arguments,
1872 "Starting parameter extraction"
1873 );
1874
1875 let mut path_params = HashMap::new();
1876 let mut query_params = HashMap::new();
1877 let mut header_params = HashMap::new();
1878 let mut cookie_params = HashMap::new();
1879 let mut body_params = HashMap::new();
1880 let mut config = RequestConfig::default();
1881
1882 if let Some(timeout) = args.get("timeout_seconds").and_then(Value::as_u64) {
1884 config.timeout_seconds = u32::try_from(timeout).unwrap_or(u32::MAX);
1885 }
1886
1887 for (key, value) in args {
1889 if key == "timeout_seconds" {
1890 continue; }
1892
1893 if key == "request_body" {
1895 body_params.insert("request_body".to_string(), value.clone());
1896 continue;
1897 }
1898
1899 let location = Self::get_parameter_location(tool_metadata, key).map_err(|e| {
1901 ToolCallValidationError::RequestConstructionError {
1902 reason: e.to_string(),
1903 }
1904 })?;
1905
1906 let original_name = Self::get_original_parameter_name(tool_metadata, key);
1908
1909 match location.as_str() {
1910 "path" => {
1911 path_params.insert(original_name.unwrap_or_else(|| key.clone()), value.clone());
1912 }
1913 "query" => {
1914 let param_name = original_name.unwrap_or_else(|| key.clone());
1915 let explode = Self::get_parameter_explode(tool_metadata, key);
1916 query_params.insert(param_name, QueryParameter::new(value.clone(), explode));
1917 }
1918 "header" => {
1919 let header_name = if let Some(orig) = original_name {
1921 orig
1922 } else if key.starts_with("header_") {
1923 key.strip_prefix("header_").unwrap_or(key).to_string()
1924 } else {
1925 key.clone()
1926 };
1927 header_params.insert(header_name, value.clone());
1928 }
1929 "cookie" => {
1930 let cookie_name = if let Some(orig) = original_name {
1932 orig
1933 } else if key.starts_with("cookie_") {
1934 key.strip_prefix("cookie_").unwrap_or(key).to_string()
1935 } else {
1936 key.clone()
1937 };
1938 cookie_params.insert(cookie_name, value.clone());
1939 }
1940 "body" => {
1941 let body_name = if key.starts_with("body_") {
1943 key.strip_prefix("body_").unwrap_or(key).to_string()
1944 } else {
1945 key.clone()
1946 };
1947 body_params.insert(body_name, value.clone());
1948 }
1949 _ => {
1950 return Err(ToolCallValidationError::RequestConstructionError {
1951 reason: format!("Unknown parameter location for parameter: {key}"),
1952 });
1953 }
1954 }
1955 }
1956
1957 let extracted = ExtractedParameters {
1958 path: path_params,
1959 query: query_params,
1960 headers: header_params,
1961 cookies: cookie_params,
1962 body: body_params,
1963 config,
1964 };
1965
1966 trace!(
1967 tool_name = %tool_metadata.name,
1968 extracted_parameters = ?extracted,
1969 "Parameter extraction completed"
1970 );
1971
1972 Self::validate_parameters(tool_metadata, arguments)?;
1974
1975 Ok(extracted)
1976 }
1977
1978 fn get_original_parameter_name(
1980 tool_metadata: &ToolMetadata,
1981 param_name: &str,
1982 ) -> Option<String> {
1983 tool_metadata
1984 .parameters
1985 .get("properties")
1986 .and_then(|p| p.as_object())
1987 .and_then(|props| props.get(param_name))
1988 .and_then(|schema| schema.get(X_ORIGINAL_NAME))
1989 .and_then(|v| v.as_str())
1990 .map(|s| s.to_string())
1991 }
1992
1993 fn get_parameter_explode(tool_metadata: &ToolMetadata, param_name: &str) -> bool {
1995 tool_metadata
1996 .parameters
1997 .get("properties")
1998 .and_then(|p| p.as_object())
1999 .and_then(|props| props.get(param_name))
2000 .and_then(|schema| schema.get(X_PARAMETER_EXPLODE))
2001 .and_then(|v| v.as_bool())
2002 .unwrap_or(true) }
2004
2005 fn get_parameter_location(
2007 tool_metadata: &ToolMetadata,
2008 param_name: &str,
2009 ) -> Result<String, Error> {
2010 let properties = tool_metadata
2011 .parameters
2012 .get("properties")
2013 .and_then(|p| p.as_object())
2014 .ok_or_else(|| Error::ToolGeneration("Invalid tool parameters schema".to_string()))?;
2015
2016 if let Some(param_schema) = properties.get(param_name)
2017 && let Some(location) = param_schema
2018 .get(X_PARAMETER_LOCATION)
2019 .and_then(|v| v.as_str())
2020 {
2021 return Ok(location.to_string());
2022 }
2023
2024 if param_name.starts_with("header_") {
2026 Ok("header".to_string())
2027 } else if param_name.starts_with("cookie_") {
2028 Ok("cookie".to_string())
2029 } else if param_name.starts_with("body_") {
2030 Ok("body".to_string())
2031 } else {
2032 Ok("query".to_string())
2034 }
2035 }
2036
2037 fn validate_parameters(
2039 tool_metadata: &ToolMetadata,
2040 arguments: &Value,
2041 ) -> Result<(), ToolCallValidationError> {
2042 let schema = &tool_metadata.parameters;
2043
2044 let required_params = schema
2046 .get("required")
2047 .and_then(|r| r.as_array())
2048 .map(|arr| {
2049 arr.iter()
2050 .filter_map(|v| v.as_str())
2051 .collect::<std::collections::HashSet<_>>()
2052 })
2053 .unwrap_or_default();
2054
2055 let properties = schema
2056 .get("properties")
2057 .and_then(|p| p.as_object())
2058 .ok_or_else(|| ToolCallValidationError::RequestConstructionError {
2059 reason: "Tool schema missing properties".to_string(),
2060 })?;
2061
2062 let args = arguments.as_object().ok_or_else(|| {
2063 ToolCallValidationError::RequestConstructionError {
2064 reason: "Arguments must be an object".to_string(),
2065 }
2066 })?;
2067
2068 let mut all_errors = Vec::new();
2070
2071 all_errors.extend(Self::check_unknown_parameters(args, properties));
2073
2074 all_errors.extend(Self::check_missing_required(
2076 args,
2077 properties,
2078 &required_params,
2079 ));
2080
2081 all_errors.extend(Self::validate_parameter_values(args, properties));
2083
2084 if !all_errors.is_empty() {
2086 return Err(ToolCallValidationError::InvalidParameters {
2087 violations: all_errors,
2088 });
2089 }
2090
2091 Ok(())
2092 }
2093
2094 fn check_unknown_parameters(
2096 args: &serde_json::Map<String, Value>,
2097 properties: &serde_json::Map<String, Value>,
2098 ) -> Vec<ValidationError> {
2099 let mut errors = Vec::new();
2100
2101 let valid_params: Vec<String> = properties.keys().map(|s| s.to_string()).collect();
2103
2104 for (arg_name, _) in args.iter() {
2106 if !properties.contains_key(arg_name) {
2107 errors.push(ValidationError::invalid_parameter(
2109 arg_name.clone(),
2110 &valid_params,
2111 ));
2112 }
2113 }
2114
2115 errors
2116 }
2117
2118 fn check_missing_required(
2120 args: &serde_json::Map<String, Value>,
2121 properties: &serde_json::Map<String, Value>,
2122 required_params: &HashSet<&str>,
2123 ) -> Vec<ValidationError> {
2124 let mut errors = Vec::new();
2125
2126 for required_param in required_params {
2127 if !args.contains_key(*required_param) {
2128 let param_schema = properties.get(*required_param);
2130
2131 let description = param_schema
2132 .and_then(|schema| schema.get("description"))
2133 .and_then(|d| d.as_str())
2134 .map(|s| s.to_string());
2135
2136 let expected_type = param_schema
2137 .and_then(Self::get_expected_type)
2138 .unwrap_or_else(|| "unknown".to_string());
2139
2140 errors.push(ValidationError::MissingRequiredParameter {
2141 parameter: (*required_param).to_string(),
2142 description,
2143 expected_type,
2144 });
2145 }
2146 }
2147
2148 errors
2149 }
2150
2151 fn validate_parameter_values(
2153 args: &serde_json::Map<String, Value>,
2154 properties: &serde_json::Map<String, Value>,
2155 ) -> Vec<ValidationError> {
2156 let mut errors = Vec::new();
2157
2158 for (param_name, param_value) in args {
2159 if let Some(param_schema) = properties.get(param_name) {
2160 let schema = json!({
2162 "type": "object",
2163 "properties": {
2164 param_name: param_schema
2165 }
2166 });
2167
2168 let compiled = match jsonschema::validator_for(&schema) {
2170 Ok(compiled) => compiled,
2171 Err(e) => {
2172 errors.push(ValidationError::ConstraintViolation {
2173 parameter: param_name.clone(),
2174 message: format!(
2175 "Failed to compile schema for parameter '{param_name}': {e}"
2176 ),
2177 field_path: None,
2178 actual_value: None,
2179 expected_type: None,
2180 constraints: vec![],
2181 });
2182 continue;
2183 }
2184 };
2185
2186 let instance = json!({ param_name: param_value });
2188
2189 let validation_errors: Vec<_> =
2191 compiled.validate(&instance).err().into_iter().collect();
2192
2193 for validation_error in validation_errors {
2194 let error_message = validation_error.to_string();
2196 let instance_path_str = validation_error.instance_path.to_string();
2197 let field_path = if instance_path_str.is_empty() || instance_path_str == "/" {
2198 Some(param_name.clone())
2199 } else {
2200 Some(instance_path_str.trim_start_matches('/').to_string())
2201 };
2202
2203 let constraints = Self::extract_constraints_from_schema(param_schema);
2205
2206 let expected_type = Self::get_expected_type(param_schema);
2208
2209 errors.push(ValidationError::ConstraintViolation {
2210 parameter: param_name.clone(),
2211 message: error_message,
2212 field_path,
2213 actual_value: Some(Box::new(param_value.clone())),
2214 expected_type,
2215 constraints,
2216 });
2217 }
2218 }
2219 }
2220
2221 errors
2222 }
2223
2224 fn extract_constraints_from_schema(schema: &Value) -> Vec<ValidationConstraint> {
2226 let mut constraints = Vec::new();
2227
2228 if let Some(min_value) = schema.get("minimum").and_then(|v| v.as_f64()) {
2230 let exclusive = schema
2231 .get("exclusiveMinimum")
2232 .and_then(|v| v.as_bool())
2233 .unwrap_or(false);
2234 constraints.push(ValidationConstraint::Minimum {
2235 value: min_value,
2236 exclusive,
2237 });
2238 }
2239
2240 if let Some(max_value) = schema.get("maximum").and_then(|v| v.as_f64()) {
2242 let exclusive = schema
2243 .get("exclusiveMaximum")
2244 .and_then(|v| v.as_bool())
2245 .unwrap_or(false);
2246 constraints.push(ValidationConstraint::Maximum {
2247 value: max_value,
2248 exclusive,
2249 });
2250 }
2251
2252 if let Some(min_len) = schema
2254 .get("minLength")
2255 .and_then(|v| v.as_u64())
2256 .map(|v| v as usize)
2257 {
2258 constraints.push(ValidationConstraint::MinLength { value: min_len });
2259 }
2260
2261 if let Some(max_len) = schema
2263 .get("maxLength")
2264 .and_then(|v| v.as_u64())
2265 .map(|v| v as usize)
2266 {
2267 constraints.push(ValidationConstraint::MaxLength { value: max_len });
2268 }
2269
2270 if let Some(pattern) = schema
2272 .get("pattern")
2273 .and_then(|v| v.as_str())
2274 .map(|s| s.to_string())
2275 {
2276 constraints.push(ValidationConstraint::Pattern { pattern });
2277 }
2278
2279 if let Some(enum_values) = schema.get("enum").and_then(|v| v.as_array()).cloned() {
2281 constraints.push(ValidationConstraint::EnumValues {
2282 values: enum_values,
2283 });
2284 }
2285
2286 if let Some(format) = schema
2288 .get("format")
2289 .and_then(|v| v.as_str())
2290 .map(|s| s.to_string())
2291 {
2292 constraints.push(ValidationConstraint::Format { format });
2293 }
2294
2295 if let Some(multiple_of) = schema.get("multipleOf").and_then(|v| v.as_f64()) {
2297 constraints.push(ValidationConstraint::MultipleOf { value: multiple_of });
2298 }
2299
2300 if let Some(min_items) = schema
2302 .get("minItems")
2303 .and_then(|v| v.as_u64())
2304 .map(|v| v as usize)
2305 {
2306 constraints.push(ValidationConstraint::MinItems { value: min_items });
2307 }
2308
2309 if let Some(max_items) = schema
2311 .get("maxItems")
2312 .and_then(|v| v.as_u64())
2313 .map(|v| v as usize)
2314 {
2315 constraints.push(ValidationConstraint::MaxItems { value: max_items });
2316 }
2317
2318 if let Some(true) = schema.get("uniqueItems").and_then(|v| v.as_bool()) {
2320 constraints.push(ValidationConstraint::UniqueItems);
2321 }
2322
2323 if let Some(min_props) = schema
2325 .get("minProperties")
2326 .and_then(|v| v.as_u64())
2327 .map(|v| v as usize)
2328 {
2329 constraints.push(ValidationConstraint::MinProperties { value: min_props });
2330 }
2331
2332 if let Some(max_props) = schema
2334 .get("maxProperties")
2335 .and_then(|v| v.as_u64())
2336 .map(|v| v as usize)
2337 {
2338 constraints.push(ValidationConstraint::MaxProperties { value: max_props });
2339 }
2340
2341 if let Some(const_value) = schema.get("const").cloned() {
2343 constraints.push(ValidationConstraint::ConstValue { value: const_value });
2344 }
2345
2346 if let Some(required) = schema.get("required").and_then(|v| v.as_array()) {
2348 let properties: Vec<String> = required
2349 .iter()
2350 .filter_map(|v| v.as_str().map(|s| s.to_string()))
2351 .collect();
2352 if !properties.is_empty() {
2353 constraints.push(ValidationConstraint::Required { properties });
2354 }
2355 }
2356
2357 constraints
2358 }
2359
2360 fn get_expected_type(schema: &Value) -> Option<String> {
2362 if let Some(type_value) = schema.get("type") {
2363 if let Some(type_str) = type_value.as_str() {
2364 return Some(type_str.to_string());
2365 } else if let Some(type_array) = type_value.as_array() {
2366 let types: Vec<String> = type_array
2368 .iter()
2369 .filter_map(|v| v.as_str())
2370 .map(|s| s.to_string())
2371 .collect();
2372 if !types.is_empty() {
2373 return Some(types.join(" | "));
2374 }
2375 }
2376 }
2377 None
2378 }
2379
2380 fn wrap_output_schema(
2404 body_schema: &ObjectOrReference<ObjectSchema>,
2405 spec: &Spec,
2406 ) -> Result<Value, Error> {
2407 let mut visited = HashSet::new();
2409 let body_schema_json = match body_schema {
2410 ObjectOrReference::Object(obj_schema) => {
2411 Self::convert_object_schema_to_json_schema(obj_schema, spec, &mut visited)?
2412 }
2413 ObjectOrReference::Ref { ref_path, .. } => {
2414 let resolved = Self::resolve_reference(ref_path, spec, &mut visited)?;
2415 Self::convert_object_schema_to_json_schema(&resolved, spec, &mut visited)?
2416 }
2417 };
2418
2419 let error_schema = create_error_response_schema();
2420
2421 Ok(json!({
2422 "type": "object",
2423 "description": "Unified response structure with success and error variants",
2424 "required": ["status", "body"],
2425 "additionalProperties": false,
2426 "properties": {
2427 "status": {
2428 "type": "integer",
2429 "description": "HTTP status code",
2430 "minimum": 100,
2431 "maximum": 599
2432 },
2433 "body": {
2434 "description": "Response body - either success data or error information",
2435 "oneOf": [
2436 body_schema_json,
2437 error_schema
2438 ]
2439 }
2440 }
2441 }))
2442 }
2443}
2444
2445fn create_error_response_schema() -> Value {
2447 let root_schema = schema_for!(ErrorResponse);
2448 let schema_json = serde_json::to_value(root_schema).expect("Valid error schema");
2449
2450 let definitions = schema_json
2452 .get("$defs")
2453 .or_else(|| schema_json.get("definitions"))
2454 .cloned()
2455 .unwrap_or_else(|| json!({}));
2456
2457 let mut result = schema_json.clone();
2459 if let Some(obj) = result.as_object_mut() {
2460 obj.remove("$schema");
2461 obj.remove("$defs");
2462 obj.remove("definitions");
2463 obj.remove("title");
2464 }
2465
2466 inline_refs(&mut result, &definitions);
2468
2469 result
2470}
2471
2472fn inline_refs(schema: &mut Value, definitions: &Value) {
2474 match schema {
2475 Value::Object(obj) => {
2476 if let Some(ref_value) = obj.get("$ref").cloned()
2478 && let Some(ref_str) = ref_value.as_str()
2479 {
2480 let def_name = ref_str
2482 .strip_prefix("#/$defs/")
2483 .or_else(|| ref_str.strip_prefix("#/definitions/"));
2484
2485 if let Some(name) = def_name
2486 && let Some(definition) = definitions.get(name)
2487 {
2488 *schema = definition.clone();
2490 inline_refs(schema, definitions);
2492 return;
2493 }
2494 }
2495
2496 for (_, value) in obj.iter_mut() {
2498 inline_refs(value, definitions);
2499 }
2500 }
2501 Value::Array(arr) => {
2502 for item in arr.iter_mut() {
2504 inline_refs(item, definitions);
2505 }
2506 }
2507 _ => {} }
2509}
2510
2511#[derive(Debug, Clone)]
2513pub struct QueryParameter {
2514 pub value: Value,
2515 pub explode: bool,
2516}
2517
2518impl QueryParameter {
2519 pub fn new(value: Value, explode: bool) -> Self {
2520 Self { value, explode }
2521 }
2522}
2523
2524#[derive(Debug, Clone)]
2526pub struct ExtractedParameters {
2527 pub path: HashMap<String, Value>,
2528 pub query: HashMap<String, QueryParameter>,
2529 pub headers: HashMap<String, Value>,
2530 pub cookies: HashMap<String, Value>,
2531 pub body: HashMap<String, Value>,
2532 pub config: RequestConfig,
2533}
2534
2535#[derive(Debug, Clone)]
2537pub struct RequestConfig {
2538 pub timeout_seconds: u32,
2539 pub content_type: String,
2540}
2541
2542impl Default for RequestConfig {
2543 fn default() -> Self {
2544 Self {
2545 timeout_seconds: 30,
2546 content_type: mime::APPLICATION_JSON.to_string(),
2547 }
2548 }
2549}
2550
2551#[cfg(test)]
2552mod tests {
2553 use super::*;
2554
2555 use insta::assert_json_snapshot;
2556 use oas3::spec::{
2557 BooleanSchema, Components, MediaType, ObjectOrReference, ObjectSchema, Operation,
2558 Parameter, ParameterIn, RequestBody, Schema, SchemaType, SchemaTypeSet, Spec,
2559 };
2560 use rmcp::model::Tool;
2561 use serde_json::{Value, json};
2562 use std::collections::BTreeMap;
2563
2564 fn create_test_spec() -> Spec {
2566 Spec {
2567 openapi: "3.0.0".to_string(),
2568 info: oas3::spec::Info {
2569 title: "Test API".to_string(),
2570 version: "1.0.0".to_string(),
2571 summary: None,
2572 description: Some("Test API for unit tests".to_string()),
2573 terms_of_service: None,
2574 contact: None,
2575 license: None,
2576 extensions: Default::default(),
2577 },
2578 components: Some(Components {
2579 schemas: BTreeMap::new(),
2580 responses: BTreeMap::new(),
2581 parameters: BTreeMap::new(),
2582 examples: BTreeMap::new(),
2583 request_bodies: BTreeMap::new(),
2584 headers: BTreeMap::new(),
2585 security_schemes: BTreeMap::new(),
2586 links: BTreeMap::new(),
2587 callbacks: BTreeMap::new(),
2588 path_items: BTreeMap::new(),
2589 extensions: Default::default(),
2590 }),
2591 servers: vec![],
2592 paths: None,
2593 external_docs: None,
2594 tags: vec![],
2595 security: vec![],
2596 webhooks: BTreeMap::new(),
2597 extensions: Default::default(),
2598 }
2599 }
2600
2601 fn validate_tool_against_mcp_schema(metadata: &ToolMetadata) {
2602 let schema_content = std::fs::read_to_string("schema/2025-06-18/schema.json")
2603 .expect("Failed to read MCP schema file");
2604 let full_schema: Value =
2605 serde_json::from_str(&schema_content).expect("Failed to parse MCP schema JSON");
2606
2607 let tool_schema = json!({
2609 "$schema": "http://json-schema.org/draft-07/schema#",
2610 "definitions": full_schema.get("definitions"),
2611 "$ref": "#/definitions/Tool"
2612 });
2613
2614 let validator =
2615 jsonschema::validator_for(&tool_schema).expect("Failed to compile MCP Tool schema");
2616
2617 let tool = Tool::from(metadata);
2619
2620 let mcp_tool_json = serde_json::to_value(&tool).expect("Failed to serialize Tool to JSON");
2622
2623 let errors: Vec<String> = validator
2625 .iter_errors(&mcp_tool_json)
2626 .map(|e| e.to_string())
2627 .collect();
2628
2629 if !errors.is_empty() {
2630 panic!("Generated tool failed MCP schema validation: {errors:?}");
2631 }
2632 }
2633
2634 #[test]
2635 fn test_error_schema_structure() {
2636 let error_schema = create_error_response_schema();
2637
2638 assert!(error_schema.get("$schema").is_none());
2640 assert!(error_schema.get("definitions").is_none());
2641
2642 assert_json_snapshot!(error_schema);
2644 }
2645
2646 #[test]
2647 fn test_petstore_get_pet_by_id() {
2648 use oas3::spec::Response;
2649
2650 let mut operation = Operation {
2651 operation_id: Some("getPetById".to_string()),
2652 summary: Some("Find pet by ID".to_string()),
2653 description: Some("Returns a single pet".to_string()),
2654 tags: vec![],
2655 external_docs: None,
2656 parameters: vec![],
2657 request_body: None,
2658 responses: Default::default(),
2659 callbacks: Default::default(),
2660 deprecated: Some(false),
2661 security: vec![],
2662 servers: vec![],
2663 extensions: Default::default(),
2664 };
2665
2666 let param = Parameter {
2668 name: "petId".to_string(),
2669 location: ParameterIn::Path,
2670 description: Some("ID of pet to return".to_string()),
2671 required: Some(true),
2672 deprecated: Some(false),
2673 allow_empty_value: Some(false),
2674 style: None,
2675 explode: None,
2676 allow_reserved: Some(false),
2677 schema: Some(ObjectOrReference::Object(ObjectSchema {
2678 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2679 minimum: Some(serde_json::Number::from(1_i64)),
2680 format: Some("int64".to_string()),
2681 ..Default::default()
2682 })),
2683 example: None,
2684 examples: Default::default(),
2685 content: None,
2686 extensions: Default::default(),
2687 };
2688
2689 operation.parameters.push(ObjectOrReference::Object(param));
2690
2691 let mut responses = BTreeMap::new();
2693 let mut content = BTreeMap::new();
2694 content.insert(
2695 "application/json".to_string(),
2696 MediaType {
2697 extensions: Default::default(),
2698 schema: Some(ObjectOrReference::Object(ObjectSchema {
2699 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2700 properties: {
2701 let mut props = BTreeMap::new();
2702 props.insert(
2703 "id".to_string(),
2704 ObjectOrReference::Object(ObjectSchema {
2705 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2706 format: Some("int64".to_string()),
2707 ..Default::default()
2708 }),
2709 );
2710 props.insert(
2711 "name".to_string(),
2712 ObjectOrReference::Object(ObjectSchema {
2713 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2714 ..Default::default()
2715 }),
2716 );
2717 props.insert(
2718 "status".to_string(),
2719 ObjectOrReference::Object(ObjectSchema {
2720 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2721 ..Default::default()
2722 }),
2723 );
2724 props
2725 },
2726 required: vec!["id".to_string(), "name".to_string()],
2727 ..Default::default()
2728 })),
2729 examples: None,
2730 encoding: Default::default(),
2731 },
2732 );
2733
2734 responses.insert(
2735 "200".to_string(),
2736 ObjectOrReference::Object(Response {
2737 description: Some("successful operation".to_string()),
2738 headers: Default::default(),
2739 content,
2740 links: Default::default(),
2741 extensions: Default::default(),
2742 }),
2743 );
2744 operation.responses = Some(responses);
2745
2746 let spec = create_test_spec();
2747 let metadata = ToolGenerator::generate_tool_metadata(
2748 &operation,
2749 "get".to_string(),
2750 "/pet/{petId}".to_string(),
2751 &spec,
2752 )
2753 .unwrap();
2754
2755 assert_eq!(metadata.name, "getPetById");
2756 assert_eq!(metadata.method, "get");
2757 assert_eq!(metadata.path, "/pet/{petId}");
2758 assert!(metadata.description.contains("Find pet by ID"));
2759
2760 assert!(metadata.output_schema.is_some());
2762 let output_schema = metadata.output_schema.as_ref().unwrap();
2763
2764 insta::assert_json_snapshot!("test_petstore_get_pet_by_id_output_schema", output_schema);
2766
2767 validate_tool_against_mcp_schema(&metadata);
2769 }
2770
2771 #[test]
2772 fn test_convert_prefix_items_to_draft07_mixed_types() {
2773 let prefix_items = vec![
2776 ObjectOrReference::Object(ObjectSchema {
2777 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
2778 format: Some("int32".to_string()),
2779 ..Default::default()
2780 }),
2781 ObjectOrReference::Object(ObjectSchema {
2782 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2783 ..Default::default()
2784 }),
2785 ];
2786
2787 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
2789
2790 let mut result = serde_json::Map::new();
2791 let spec = create_test_spec();
2792 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
2793 .unwrap();
2794
2795 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_mixed_types", result);
2797 }
2798
2799 #[test]
2800 fn test_convert_prefix_items_to_draft07_uniform_types() {
2801 let prefix_items = vec![
2803 ObjectOrReference::Object(ObjectSchema {
2804 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2805 ..Default::default()
2806 }),
2807 ObjectOrReference::Object(ObjectSchema {
2808 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2809 ..Default::default()
2810 }),
2811 ];
2812
2813 let items = Some(Box::new(Schema::Boolean(BooleanSchema(false))));
2815
2816 let mut result = serde_json::Map::new();
2817 let spec = create_test_spec();
2818 ToolGenerator::convert_prefix_items_to_draft07(&prefix_items, &items, &mut result, &spec)
2819 .unwrap();
2820
2821 insta::assert_json_snapshot!("test_convert_prefix_items_to_draft07_uniform_types", result);
2823 }
2824
2825 #[test]
2826 fn test_array_with_prefix_items_integration() {
2827 let param = Parameter {
2829 name: "coordinates".to_string(),
2830 location: ParameterIn::Query,
2831 description: Some("X,Y coordinates as tuple".to_string()),
2832 required: Some(true),
2833 deprecated: Some(false),
2834 allow_empty_value: Some(false),
2835 style: None,
2836 explode: None,
2837 allow_reserved: Some(false),
2838 schema: Some(ObjectOrReference::Object(ObjectSchema {
2839 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
2840 prefix_items: vec![
2841 ObjectOrReference::Object(ObjectSchema {
2842 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
2843 format: Some("double".to_string()),
2844 ..Default::default()
2845 }),
2846 ObjectOrReference::Object(ObjectSchema {
2847 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
2848 format: Some("double".to_string()),
2849 ..Default::default()
2850 }),
2851 ],
2852 items: Some(Box::new(Schema::Boolean(BooleanSchema(false)))),
2853 ..Default::default()
2854 })),
2855 example: None,
2856 examples: Default::default(),
2857 content: None,
2858 extensions: Default::default(),
2859 };
2860
2861 let spec = create_test_spec();
2862 let (result, _annotations) =
2863 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec).unwrap();
2864
2865 insta::assert_json_snapshot!("test_array_with_prefix_items_integration", result);
2867 }
2868
2869 #[test]
2870 fn test_array_with_regular_items_schema() {
2871 let param = Parameter {
2873 name: "tags".to_string(),
2874 location: ParameterIn::Query,
2875 description: Some("List of tags".to_string()),
2876 required: Some(false),
2877 deprecated: Some(false),
2878 allow_empty_value: Some(false),
2879 style: None,
2880 explode: None,
2881 allow_reserved: Some(false),
2882 schema: Some(ObjectOrReference::Object(ObjectSchema {
2883 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
2884 items: Some(Box::new(Schema::Object(Box::new(
2885 ObjectOrReference::Object(ObjectSchema {
2886 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
2887 min_length: Some(1),
2888 max_length: Some(50),
2889 ..Default::default()
2890 }),
2891 )))),
2892 ..Default::default()
2893 })),
2894 example: None,
2895 examples: Default::default(),
2896 content: None,
2897 extensions: Default::default(),
2898 };
2899
2900 let spec = create_test_spec();
2901 let (result, _annotations) =
2902 ToolGenerator::convert_parameter_schema(¶m, ParameterIn::Query, &spec).unwrap();
2903
2904 insta::assert_json_snapshot!("test_array_with_regular_items_schema", result);
2906 }
2907
2908 #[test]
2909 fn test_request_body_object_schema() {
2910 let operation = Operation {
2912 operation_id: Some("createPet".to_string()),
2913 summary: Some("Create a new pet".to_string()),
2914 description: Some("Creates a new pet in the store".to_string()),
2915 tags: vec![],
2916 external_docs: None,
2917 parameters: vec![],
2918 request_body: Some(ObjectOrReference::Object(RequestBody {
2919 description: Some("Pet object that needs to be added to the store".to_string()),
2920 content: {
2921 let mut content = BTreeMap::new();
2922 content.insert(
2923 "application/json".to_string(),
2924 MediaType {
2925 extensions: Default::default(),
2926 schema: Some(ObjectOrReference::Object(ObjectSchema {
2927 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
2928 ..Default::default()
2929 })),
2930 examples: None,
2931 encoding: Default::default(),
2932 },
2933 );
2934 content
2935 },
2936 required: Some(true),
2937 })),
2938 responses: Default::default(),
2939 callbacks: Default::default(),
2940 deprecated: Some(false),
2941 security: vec![],
2942 servers: vec![],
2943 extensions: Default::default(),
2944 };
2945
2946 let spec = create_test_spec();
2947 let metadata = ToolGenerator::generate_tool_metadata(
2948 &operation,
2949 "post".to_string(),
2950 "/pets".to_string(),
2951 &spec,
2952 )
2953 .unwrap();
2954
2955 let properties = metadata
2957 .parameters
2958 .get("properties")
2959 .unwrap()
2960 .as_object()
2961 .unwrap();
2962 assert!(properties.contains_key("request_body"));
2963
2964 let required = metadata
2966 .parameters
2967 .get("required")
2968 .unwrap()
2969 .as_array()
2970 .unwrap();
2971 assert!(required.contains(&json!("request_body")));
2972
2973 let request_body_schema = properties.get("request_body").unwrap();
2975 insta::assert_json_snapshot!("test_request_body_object_schema", request_body_schema);
2976
2977 validate_tool_against_mcp_schema(&metadata);
2979 }
2980
2981 #[test]
2982 fn test_request_body_array_schema() {
2983 let operation = Operation {
2985 operation_id: Some("createPets".to_string()),
2986 summary: Some("Create multiple pets".to_string()),
2987 description: None,
2988 tags: vec![],
2989 external_docs: None,
2990 parameters: vec![],
2991 request_body: Some(ObjectOrReference::Object(RequestBody {
2992 description: Some("Array of pet objects".to_string()),
2993 content: {
2994 let mut content = BTreeMap::new();
2995 content.insert(
2996 "application/json".to_string(),
2997 MediaType {
2998 extensions: Default::default(),
2999 schema: Some(ObjectOrReference::Object(ObjectSchema {
3000 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3001 items: Some(Box::new(Schema::Object(Box::new(
3002 ObjectOrReference::Object(ObjectSchema {
3003 schema_type: Some(SchemaTypeSet::Single(
3004 SchemaType::Object,
3005 )),
3006 ..Default::default()
3007 }),
3008 )))),
3009 ..Default::default()
3010 })),
3011 examples: None,
3012 encoding: Default::default(),
3013 },
3014 );
3015 content
3016 },
3017 required: Some(false),
3018 })),
3019 responses: Default::default(),
3020 callbacks: Default::default(),
3021 deprecated: Some(false),
3022 security: vec![],
3023 servers: vec![],
3024 extensions: Default::default(),
3025 };
3026
3027 let spec = create_test_spec();
3028 let metadata = ToolGenerator::generate_tool_metadata(
3029 &operation,
3030 "post".to_string(),
3031 "/pets/batch".to_string(),
3032 &spec,
3033 )
3034 .unwrap();
3035
3036 let properties = metadata
3038 .parameters
3039 .get("properties")
3040 .unwrap()
3041 .as_object()
3042 .unwrap();
3043 assert!(properties.contains_key("request_body"));
3044
3045 let required = metadata
3047 .parameters
3048 .get("required")
3049 .unwrap()
3050 .as_array()
3051 .unwrap();
3052 assert!(!required.contains(&json!("request_body")));
3053
3054 let request_body_schema = properties.get("request_body").unwrap();
3056 insta::assert_json_snapshot!("test_request_body_array_schema", request_body_schema);
3057
3058 validate_tool_against_mcp_schema(&metadata);
3060 }
3061
3062 #[test]
3063 fn test_request_body_string_schema() {
3064 let operation = Operation {
3066 operation_id: Some("updatePetName".to_string()),
3067 summary: Some("Update pet name".to_string()),
3068 description: None,
3069 tags: vec![],
3070 external_docs: None,
3071 parameters: vec![],
3072 request_body: Some(ObjectOrReference::Object(RequestBody {
3073 description: None,
3074 content: {
3075 let mut content = BTreeMap::new();
3076 content.insert(
3077 "text/plain".to_string(),
3078 MediaType {
3079 extensions: Default::default(),
3080 schema: Some(ObjectOrReference::Object(ObjectSchema {
3081 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3082 min_length: Some(1),
3083 max_length: Some(100),
3084 ..Default::default()
3085 })),
3086 examples: None,
3087 encoding: Default::default(),
3088 },
3089 );
3090 content
3091 },
3092 required: Some(true),
3093 })),
3094 responses: Default::default(),
3095 callbacks: Default::default(),
3096 deprecated: Some(false),
3097 security: vec![],
3098 servers: vec![],
3099 extensions: Default::default(),
3100 };
3101
3102 let spec = create_test_spec();
3103 let metadata = ToolGenerator::generate_tool_metadata(
3104 &operation,
3105 "put".to_string(),
3106 "/pets/{petId}/name".to_string(),
3107 &spec,
3108 )
3109 .unwrap();
3110
3111 let properties = metadata
3113 .parameters
3114 .get("properties")
3115 .unwrap()
3116 .as_object()
3117 .unwrap();
3118 let request_body_schema = properties.get("request_body").unwrap();
3119 insta::assert_json_snapshot!("test_request_body_string_schema", request_body_schema);
3120
3121 validate_tool_against_mcp_schema(&metadata);
3123 }
3124
3125 #[test]
3126 fn test_request_body_ref_schema() {
3127 let operation = Operation {
3129 operation_id: Some("updatePet".to_string()),
3130 summary: Some("Update existing pet".to_string()),
3131 description: None,
3132 tags: vec![],
3133 external_docs: None,
3134 parameters: vec![],
3135 request_body: Some(ObjectOrReference::Ref {
3136 ref_path: "#/components/requestBodies/PetBody".to_string(),
3137 summary: None,
3138 description: None,
3139 }),
3140 responses: Default::default(),
3141 callbacks: Default::default(),
3142 deprecated: Some(false),
3143 security: vec![],
3144 servers: vec![],
3145 extensions: Default::default(),
3146 };
3147
3148 let spec = create_test_spec();
3149 let metadata = ToolGenerator::generate_tool_metadata(
3150 &operation,
3151 "put".to_string(),
3152 "/pets/{petId}".to_string(),
3153 &spec,
3154 )
3155 .unwrap();
3156
3157 let properties = metadata
3159 .parameters
3160 .get("properties")
3161 .unwrap()
3162 .as_object()
3163 .unwrap();
3164 let request_body_schema = properties.get("request_body").unwrap();
3165 insta::assert_json_snapshot!("test_request_body_ref_schema", request_body_schema);
3166
3167 validate_tool_against_mcp_schema(&metadata);
3169 }
3170
3171 #[test]
3172 fn test_no_request_body_for_get() {
3173 let operation = Operation {
3175 operation_id: Some("listPets".to_string()),
3176 summary: Some("List all pets".to_string()),
3177 description: None,
3178 tags: vec![],
3179 external_docs: None,
3180 parameters: vec![],
3181 request_body: None,
3182 responses: Default::default(),
3183 callbacks: Default::default(),
3184 deprecated: Some(false),
3185 security: vec![],
3186 servers: vec![],
3187 extensions: Default::default(),
3188 };
3189
3190 let spec = create_test_spec();
3191 let metadata = ToolGenerator::generate_tool_metadata(
3192 &operation,
3193 "get".to_string(),
3194 "/pets".to_string(),
3195 &spec,
3196 )
3197 .unwrap();
3198
3199 let properties = metadata
3201 .parameters
3202 .get("properties")
3203 .unwrap()
3204 .as_object()
3205 .unwrap();
3206 assert!(!properties.contains_key("request_body"));
3207
3208 validate_tool_against_mcp_schema(&metadata);
3210 }
3211
3212 #[test]
3213 fn test_request_body_simple_object_with_properties() {
3214 let operation = Operation {
3216 operation_id: Some("updatePetStatus".to_string()),
3217 summary: Some("Update pet status".to_string()),
3218 description: None,
3219 tags: vec![],
3220 external_docs: None,
3221 parameters: vec![],
3222 request_body: Some(ObjectOrReference::Object(RequestBody {
3223 description: Some("Pet status update".to_string()),
3224 content: {
3225 let mut content = BTreeMap::new();
3226 content.insert(
3227 "application/json".to_string(),
3228 MediaType {
3229 extensions: Default::default(),
3230 schema: Some(ObjectOrReference::Object(ObjectSchema {
3231 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3232 properties: {
3233 let mut props = BTreeMap::new();
3234 props.insert(
3235 "status".to_string(),
3236 ObjectOrReference::Object(ObjectSchema {
3237 schema_type: Some(SchemaTypeSet::Single(
3238 SchemaType::String,
3239 )),
3240 ..Default::default()
3241 }),
3242 );
3243 props.insert(
3244 "reason".to_string(),
3245 ObjectOrReference::Object(ObjectSchema {
3246 schema_type: Some(SchemaTypeSet::Single(
3247 SchemaType::String,
3248 )),
3249 ..Default::default()
3250 }),
3251 );
3252 props
3253 },
3254 required: vec!["status".to_string()],
3255 ..Default::default()
3256 })),
3257 examples: None,
3258 encoding: Default::default(),
3259 },
3260 );
3261 content
3262 },
3263 required: Some(false),
3264 })),
3265 responses: Default::default(),
3266 callbacks: Default::default(),
3267 deprecated: Some(false),
3268 security: vec![],
3269 servers: vec![],
3270 extensions: Default::default(),
3271 };
3272
3273 let spec = create_test_spec();
3274 let metadata = ToolGenerator::generate_tool_metadata(
3275 &operation,
3276 "patch".to_string(),
3277 "/pets/{petId}/status".to_string(),
3278 &spec,
3279 )
3280 .unwrap();
3281
3282 let properties = metadata
3284 .parameters
3285 .get("properties")
3286 .unwrap()
3287 .as_object()
3288 .unwrap();
3289 let request_body_schema = properties.get("request_body").unwrap();
3290 insta::assert_json_snapshot!(
3291 "test_request_body_simple_object_with_properties",
3292 request_body_schema
3293 );
3294
3295 let required = metadata
3297 .parameters
3298 .get("required")
3299 .unwrap()
3300 .as_array()
3301 .unwrap();
3302 assert!(!required.contains(&json!("request_body")));
3303
3304 validate_tool_against_mcp_schema(&metadata);
3306 }
3307
3308 #[test]
3309 fn test_request_body_with_nested_properties() {
3310 let operation = Operation {
3312 operation_id: Some("createUser".to_string()),
3313 summary: Some("Create a new user".to_string()),
3314 description: None,
3315 tags: vec![],
3316 external_docs: None,
3317 parameters: vec![],
3318 request_body: Some(ObjectOrReference::Object(RequestBody {
3319 description: Some("User creation data".to_string()),
3320 content: {
3321 let mut content = BTreeMap::new();
3322 content.insert(
3323 "application/json".to_string(),
3324 MediaType {
3325 extensions: Default::default(),
3326 schema: Some(ObjectOrReference::Object(ObjectSchema {
3327 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3328 properties: {
3329 let mut props = BTreeMap::new();
3330 props.insert(
3331 "name".to_string(),
3332 ObjectOrReference::Object(ObjectSchema {
3333 schema_type: Some(SchemaTypeSet::Single(
3334 SchemaType::String,
3335 )),
3336 ..Default::default()
3337 }),
3338 );
3339 props.insert(
3340 "age".to_string(),
3341 ObjectOrReference::Object(ObjectSchema {
3342 schema_type: Some(SchemaTypeSet::Single(
3343 SchemaType::Integer,
3344 )),
3345 minimum: Some(serde_json::Number::from(0)),
3346 maximum: Some(serde_json::Number::from(150)),
3347 ..Default::default()
3348 }),
3349 );
3350 props
3351 },
3352 required: vec!["name".to_string()],
3353 ..Default::default()
3354 })),
3355 examples: None,
3356 encoding: Default::default(),
3357 },
3358 );
3359 content
3360 },
3361 required: Some(true),
3362 })),
3363 responses: Default::default(),
3364 callbacks: Default::default(),
3365 deprecated: Some(false),
3366 security: vec![],
3367 servers: vec![],
3368 extensions: Default::default(),
3369 };
3370
3371 let spec = create_test_spec();
3372 let metadata = ToolGenerator::generate_tool_metadata(
3373 &operation,
3374 "post".to_string(),
3375 "/users".to_string(),
3376 &spec,
3377 )
3378 .unwrap();
3379
3380 let properties = metadata
3382 .parameters
3383 .get("properties")
3384 .unwrap()
3385 .as_object()
3386 .unwrap();
3387 let request_body_schema = properties.get("request_body").unwrap();
3388 insta::assert_json_snapshot!(
3389 "test_request_body_with_nested_properties",
3390 request_body_schema
3391 );
3392
3393 validate_tool_against_mcp_schema(&metadata);
3395 }
3396
3397 #[test]
3398 fn test_operation_without_responses_has_no_output_schema() {
3399 let operation = Operation {
3400 operation_id: Some("testOperation".to_string()),
3401 summary: Some("Test operation".to_string()),
3402 description: None,
3403 tags: vec![],
3404 external_docs: None,
3405 parameters: vec![],
3406 request_body: None,
3407 responses: None,
3408 callbacks: Default::default(),
3409 deprecated: Some(false),
3410 security: vec![],
3411 servers: vec![],
3412 extensions: Default::default(),
3413 };
3414
3415 let spec = create_test_spec();
3416 let metadata = ToolGenerator::generate_tool_metadata(
3417 &operation,
3418 "get".to_string(),
3419 "/test".to_string(),
3420 &spec,
3421 )
3422 .unwrap();
3423
3424 assert!(metadata.output_schema.is_none());
3426
3427 validate_tool_against_mcp_schema(&metadata);
3429 }
3430
3431 #[test]
3432 fn test_extract_output_schema_with_200_response() {
3433 use oas3::spec::Response;
3434
3435 let mut responses = BTreeMap::new();
3437 let mut content = BTreeMap::new();
3438 content.insert(
3439 "application/json".to_string(),
3440 MediaType {
3441 extensions: Default::default(),
3442 schema: Some(ObjectOrReference::Object(ObjectSchema {
3443 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3444 properties: {
3445 let mut props = BTreeMap::new();
3446 props.insert(
3447 "id".to_string(),
3448 ObjectOrReference::Object(ObjectSchema {
3449 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3450 ..Default::default()
3451 }),
3452 );
3453 props.insert(
3454 "name".to_string(),
3455 ObjectOrReference::Object(ObjectSchema {
3456 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3457 ..Default::default()
3458 }),
3459 );
3460 props
3461 },
3462 required: vec!["id".to_string(), "name".to_string()],
3463 ..Default::default()
3464 })),
3465 examples: None,
3466 encoding: Default::default(),
3467 },
3468 );
3469
3470 responses.insert(
3471 "200".to_string(),
3472 ObjectOrReference::Object(Response {
3473 description: Some("Successful response".to_string()),
3474 headers: Default::default(),
3475 content,
3476 links: Default::default(),
3477 extensions: Default::default(),
3478 }),
3479 );
3480
3481 let spec = create_test_spec();
3482 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
3483
3484 insta::assert_json_snapshot!(result);
3486 }
3487
3488 #[test]
3489 fn test_extract_output_schema_with_201_response() {
3490 use oas3::spec::Response;
3491
3492 let mut responses = BTreeMap::new();
3494 let mut content = BTreeMap::new();
3495 content.insert(
3496 "application/json".to_string(),
3497 MediaType {
3498 extensions: Default::default(),
3499 schema: Some(ObjectOrReference::Object(ObjectSchema {
3500 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3501 properties: {
3502 let mut props = BTreeMap::new();
3503 props.insert(
3504 "created".to_string(),
3505 ObjectOrReference::Object(ObjectSchema {
3506 schema_type: Some(SchemaTypeSet::Single(SchemaType::Boolean)),
3507 ..Default::default()
3508 }),
3509 );
3510 props
3511 },
3512 ..Default::default()
3513 })),
3514 examples: None,
3515 encoding: Default::default(),
3516 },
3517 );
3518
3519 responses.insert(
3520 "201".to_string(),
3521 ObjectOrReference::Object(Response {
3522 description: Some("Created".to_string()),
3523 headers: Default::default(),
3524 content,
3525 links: Default::default(),
3526 extensions: Default::default(),
3527 }),
3528 );
3529
3530 let spec = create_test_spec();
3531 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
3532
3533 insta::assert_json_snapshot!(result);
3535 }
3536
3537 #[test]
3538 fn test_extract_output_schema_with_2xx_response() {
3539 use oas3::spec::Response;
3540
3541 let mut responses = BTreeMap::new();
3543 let mut content = BTreeMap::new();
3544 content.insert(
3545 "application/json".to_string(),
3546 MediaType {
3547 extensions: Default::default(),
3548 schema: Some(ObjectOrReference::Object(ObjectSchema {
3549 schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
3550 items: Some(Box::new(Schema::Object(Box::new(
3551 ObjectOrReference::Object(ObjectSchema {
3552 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3553 ..Default::default()
3554 }),
3555 )))),
3556 ..Default::default()
3557 })),
3558 examples: None,
3559 encoding: Default::default(),
3560 },
3561 );
3562
3563 responses.insert(
3564 "2XX".to_string(),
3565 ObjectOrReference::Object(Response {
3566 description: Some("Success".to_string()),
3567 headers: Default::default(),
3568 content,
3569 links: Default::default(),
3570 extensions: Default::default(),
3571 }),
3572 );
3573
3574 let spec = create_test_spec();
3575 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
3576
3577 insta::assert_json_snapshot!(result);
3579 }
3580
3581 #[test]
3582 fn test_extract_output_schema_no_responses() {
3583 let spec = create_test_spec();
3584 let result = ToolGenerator::extract_output_schema(&None, &spec).unwrap();
3585
3586 insta::assert_json_snapshot!(result);
3588 }
3589
3590 #[test]
3591 fn test_extract_output_schema_only_error_responses() {
3592 use oas3::spec::Response;
3593
3594 let mut responses = BTreeMap::new();
3596 responses.insert(
3597 "404".to_string(),
3598 ObjectOrReference::Object(Response {
3599 description: Some("Not found".to_string()),
3600 headers: Default::default(),
3601 content: Default::default(),
3602 links: Default::default(),
3603 extensions: Default::default(),
3604 }),
3605 );
3606 responses.insert(
3607 "500".to_string(),
3608 ObjectOrReference::Object(Response {
3609 description: Some("Server error".to_string()),
3610 headers: Default::default(),
3611 content: Default::default(),
3612 links: Default::default(),
3613 extensions: Default::default(),
3614 }),
3615 );
3616
3617 let spec = create_test_spec();
3618 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
3619
3620 insta::assert_json_snapshot!(result);
3622 }
3623
3624 #[test]
3625 fn test_extract_output_schema_with_ref() {
3626 use oas3::spec::Response;
3627
3628 let mut spec = create_test_spec();
3630 let mut schemas = BTreeMap::new();
3631 schemas.insert(
3632 "Pet".to_string(),
3633 ObjectOrReference::Object(ObjectSchema {
3634 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3635 properties: {
3636 let mut props = BTreeMap::new();
3637 props.insert(
3638 "name".to_string(),
3639 ObjectOrReference::Object(ObjectSchema {
3640 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3641 ..Default::default()
3642 }),
3643 );
3644 props
3645 },
3646 ..Default::default()
3647 }),
3648 );
3649 spec.components.as_mut().unwrap().schemas = schemas;
3650
3651 let mut responses = BTreeMap::new();
3653 let mut content = BTreeMap::new();
3654 content.insert(
3655 "application/json".to_string(),
3656 MediaType {
3657 extensions: Default::default(),
3658 schema: Some(ObjectOrReference::Ref {
3659 ref_path: "#/components/schemas/Pet".to_string(),
3660 summary: None,
3661 description: None,
3662 }),
3663 examples: None,
3664 encoding: Default::default(),
3665 },
3666 );
3667
3668 responses.insert(
3669 "200".to_string(),
3670 ObjectOrReference::Object(Response {
3671 description: Some("Success".to_string()),
3672 headers: Default::default(),
3673 content,
3674 links: Default::default(),
3675 extensions: Default::default(),
3676 }),
3677 );
3678
3679 let result = ToolGenerator::extract_output_schema(&Some(responses), &spec).unwrap();
3680
3681 insta::assert_json_snapshot!(result);
3683 }
3684
3685 #[test]
3686 fn test_generate_tool_metadata_includes_output_schema() {
3687 use oas3::spec::Response;
3688
3689 let mut operation = Operation {
3690 operation_id: Some("getPet".to_string()),
3691 summary: Some("Get a pet".to_string()),
3692 description: None,
3693 tags: vec![],
3694 external_docs: None,
3695 parameters: vec![],
3696 request_body: None,
3697 responses: Default::default(),
3698 callbacks: Default::default(),
3699 deprecated: Some(false),
3700 security: vec![],
3701 servers: vec![],
3702 extensions: Default::default(),
3703 };
3704
3705 let mut responses = BTreeMap::new();
3707 let mut content = BTreeMap::new();
3708 content.insert(
3709 "application/json".to_string(),
3710 MediaType {
3711 extensions: Default::default(),
3712 schema: Some(ObjectOrReference::Object(ObjectSchema {
3713 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3714 properties: {
3715 let mut props = BTreeMap::new();
3716 props.insert(
3717 "id".to_string(),
3718 ObjectOrReference::Object(ObjectSchema {
3719 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3720 ..Default::default()
3721 }),
3722 );
3723 props
3724 },
3725 ..Default::default()
3726 })),
3727 examples: None,
3728 encoding: Default::default(),
3729 },
3730 );
3731
3732 responses.insert(
3733 "200".to_string(),
3734 ObjectOrReference::Object(Response {
3735 description: Some("Success".to_string()),
3736 headers: Default::default(),
3737 content,
3738 links: Default::default(),
3739 extensions: Default::default(),
3740 }),
3741 );
3742 operation.responses = Some(responses);
3743
3744 let spec = create_test_spec();
3745 let metadata = ToolGenerator::generate_tool_metadata(
3746 &operation,
3747 "get".to_string(),
3748 "/pets/{id}".to_string(),
3749 &spec,
3750 )
3751 .unwrap();
3752
3753 assert!(metadata.output_schema.is_some());
3755 let output_schema = metadata.output_schema.as_ref().unwrap();
3756
3757 insta::assert_json_snapshot!(
3759 "test_generate_tool_metadata_includes_output_schema",
3760 output_schema
3761 );
3762
3763 validate_tool_against_mcp_schema(&metadata);
3765 }
3766
3767 #[test]
3768 fn test_sanitize_property_name() {
3769 assert_eq!(sanitize_property_name("user name"), "user_name");
3771 assert_eq!(
3772 sanitize_property_name("first name last name"),
3773 "first_name_last_name"
3774 );
3775
3776 assert_eq!(sanitize_property_name("user(admin)"), "user_admin");
3778 assert_eq!(sanitize_property_name("user[admin]"), "user_admin");
3779 assert_eq!(sanitize_property_name("price($)"), "price");
3780 assert_eq!(sanitize_property_name("email@address"), "email_address");
3781 assert_eq!(sanitize_property_name("item#1"), "item_1");
3782 assert_eq!(sanitize_property_name("a/b/c"), "a_b_c");
3783
3784 assert_eq!(sanitize_property_name("user_name"), "user_name");
3786 assert_eq!(sanitize_property_name("userName123"), "userName123");
3787 assert_eq!(sanitize_property_name("user.name"), "user.name");
3788 assert_eq!(sanitize_property_name("user-name"), "user-name");
3789
3790 assert_eq!(sanitize_property_name("123name"), "param_123name");
3792 assert_eq!(sanitize_property_name("1st_place"), "param_1st_place");
3793
3794 assert_eq!(sanitize_property_name(""), "param_");
3796
3797 let long_name = "a".repeat(100);
3799 assert_eq!(sanitize_property_name(&long_name).len(), 64);
3800
3801 assert_eq!(sanitize_property_name("!@#$%^&*()"), "param_");
3804 }
3805
3806 #[test]
3807 fn test_sanitize_property_name_trailing_underscores() {
3808 assert_eq!(sanitize_property_name("page[size]"), "page_size");
3810 assert_eq!(sanitize_property_name("user[id]"), "user_id");
3811 assert_eq!(sanitize_property_name("field[]"), "field");
3812
3813 assert_eq!(sanitize_property_name("field___"), "field");
3815 assert_eq!(sanitize_property_name("test[[["), "test");
3816 }
3817
3818 #[test]
3819 fn test_sanitize_property_name_consecutive_underscores() {
3820 assert_eq!(sanitize_property_name("user__name"), "user_name");
3822 assert_eq!(sanitize_property_name("first___last"), "first_last");
3823 assert_eq!(sanitize_property_name("a____b____c"), "a_b_c");
3824
3825 assert_eq!(sanitize_property_name("user[[name]]"), "user_name");
3827 assert_eq!(sanitize_property_name("field@#$value"), "field_value");
3828 }
3829
3830 #[test]
3831 fn test_sanitize_property_name_edge_cases() {
3832 assert_eq!(sanitize_property_name("_private"), "_private");
3834 assert_eq!(sanitize_property_name("__dunder"), "_dunder");
3835
3836 assert_eq!(sanitize_property_name("[[["), "param_");
3838 assert_eq!(sanitize_property_name("@@@"), "param_");
3839
3840 assert_eq!(sanitize_property_name(""), "param_");
3842
3843 assert_eq!(sanitize_property_name("_field[size]"), "_field_size");
3845 assert_eq!(sanitize_property_name("__test__"), "_test");
3846 }
3847
3848 #[test]
3849 fn test_sanitize_property_name_complex_cases() {
3850 assert_eq!(sanitize_property_name("page[size]"), "page_size");
3852 assert_eq!(sanitize_property_name("filter[status]"), "filter_status");
3853 assert_eq!(
3854 sanitize_property_name("sort[-created_at]"),
3855 "sort_-created_at"
3856 );
3857 assert_eq!(
3858 sanitize_property_name("include[author.posts]"),
3859 "include_author.posts"
3860 );
3861
3862 let long_name = "very_long_field_name_with_special[characters]_that_needs_truncation_____";
3864 let expected = "very_long_field_name_with_special_characters_that_needs_truncat";
3865 assert_eq!(sanitize_property_name(long_name), expected);
3866 }
3867
3868 #[test]
3869 fn test_property_sanitization_with_annotations() {
3870 let spec = create_test_spec();
3871 let mut visited = HashSet::new();
3872
3873 let obj_schema = ObjectSchema {
3875 schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
3876 properties: {
3877 let mut props = BTreeMap::new();
3878 props.insert(
3880 "user name".to_string(),
3881 ObjectOrReference::Object(ObjectSchema {
3882 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3883 ..Default::default()
3884 }),
3885 );
3886 props.insert(
3888 "price($)".to_string(),
3889 ObjectOrReference::Object(ObjectSchema {
3890 schema_type: Some(SchemaTypeSet::Single(SchemaType::Number)),
3891 ..Default::default()
3892 }),
3893 );
3894 props.insert(
3896 "validName".to_string(),
3897 ObjectOrReference::Object(ObjectSchema {
3898 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3899 ..Default::default()
3900 }),
3901 );
3902 props
3903 },
3904 ..Default::default()
3905 };
3906
3907 let result =
3908 ToolGenerator::convert_object_schema_to_json_schema(&obj_schema, &spec, &mut visited)
3909 .unwrap();
3910
3911 insta::assert_json_snapshot!("test_property_sanitization_with_annotations", result);
3913 }
3914
3915 #[test]
3916 fn test_parameter_sanitization_and_extraction() {
3917 let spec = create_test_spec();
3918
3919 let operation = Operation {
3921 operation_id: Some("testOp".to_string()),
3922 parameters: vec![
3923 ObjectOrReference::Object(Parameter {
3925 name: "user(id)".to_string(),
3926 location: ParameterIn::Path,
3927 description: Some("User ID".to_string()),
3928 required: Some(true),
3929 deprecated: Some(false),
3930 allow_empty_value: Some(false),
3931 style: None,
3932 explode: None,
3933 allow_reserved: Some(false),
3934 schema: Some(ObjectOrReference::Object(ObjectSchema {
3935 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3936 ..Default::default()
3937 })),
3938 example: None,
3939 examples: Default::default(),
3940 content: None,
3941 extensions: Default::default(),
3942 }),
3943 ObjectOrReference::Object(Parameter {
3945 name: "page size".to_string(),
3946 location: ParameterIn::Query,
3947 description: Some("Page size".to_string()),
3948 required: Some(false),
3949 deprecated: Some(false),
3950 allow_empty_value: Some(false),
3951 style: None,
3952 explode: None,
3953 allow_reserved: Some(false),
3954 schema: Some(ObjectOrReference::Object(ObjectSchema {
3955 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
3956 ..Default::default()
3957 })),
3958 example: None,
3959 examples: Default::default(),
3960 content: None,
3961 extensions: Default::default(),
3962 }),
3963 ObjectOrReference::Object(Parameter {
3965 name: "auth-token!".to_string(),
3966 location: ParameterIn::Header,
3967 description: Some("Auth token".to_string()),
3968 required: Some(false),
3969 deprecated: Some(false),
3970 allow_empty_value: Some(false),
3971 style: None,
3972 explode: None,
3973 allow_reserved: Some(false),
3974 schema: Some(ObjectOrReference::Object(ObjectSchema {
3975 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
3976 ..Default::default()
3977 })),
3978 example: None,
3979 examples: Default::default(),
3980 content: None,
3981 extensions: Default::default(),
3982 }),
3983 ],
3984 ..Default::default()
3985 };
3986
3987 let tool_metadata = ToolGenerator::generate_tool_metadata(
3988 &operation,
3989 "get".to_string(),
3990 "/users/{user(id)}".to_string(),
3991 &spec,
3992 )
3993 .unwrap();
3994
3995 let properties = tool_metadata
3997 .parameters
3998 .get("properties")
3999 .unwrap()
4000 .as_object()
4001 .unwrap();
4002
4003 assert!(properties.contains_key("user_id"));
4004 assert!(properties.contains_key("page_size"));
4005 assert!(properties.contains_key("header_auth-token"));
4006
4007 let required = tool_metadata
4009 .parameters
4010 .get("required")
4011 .unwrap()
4012 .as_array()
4013 .unwrap();
4014 assert!(required.contains(&json!("user_id")));
4015
4016 let arguments = json!({
4018 "user_id": "123",
4019 "page_size": 10,
4020 "header_auth-token": "secret"
4021 });
4022
4023 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
4024
4025 assert_eq!(extracted.path.get("user(id)"), Some(&json!("123")));
4027
4028 assert_eq!(
4030 extracted.query.get("page size").map(|q| &q.value),
4031 Some(&json!(10))
4032 );
4033
4034 assert_eq!(extracted.headers.get("auth-token!"), Some(&json!("secret")));
4036 }
4037
4038 #[test]
4039 fn test_check_unknown_parameters() {
4040 let mut properties = serde_json::Map::new();
4042 properties.insert("page_size".to_string(), json!({"type": "integer"}));
4043 properties.insert("user_id".to_string(), json!({"type": "string"}));
4044
4045 let mut args = serde_json::Map::new();
4046 args.insert("page_sixe".to_string(), json!(10)); let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4049 assert!(!result.is_empty());
4050 assert_eq!(result.len(), 1);
4051
4052 match &result[0] {
4053 ValidationError::InvalidParameter {
4054 parameter,
4055 suggestions,
4056 valid_parameters,
4057 } => {
4058 assert_eq!(parameter, "page_sixe");
4059 assert_eq!(suggestions, &vec!["page_size".to_string()]);
4060 assert_eq!(
4061 valid_parameters,
4062 &vec!["page_size".to_string(), "user_id".to_string()]
4063 );
4064 }
4065 _ => panic!("Expected InvalidParameter variant"),
4066 }
4067 }
4068
4069 #[test]
4070 fn test_check_unknown_parameters_no_suggestions() {
4071 let mut properties = serde_json::Map::new();
4073 properties.insert("limit".to_string(), json!({"type": "integer"}));
4074 properties.insert("offset".to_string(), json!({"type": "integer"}));
4075
4076 let mut args = serde_json::Map::new();
4077 args.insert("xyz123".to_string(), json!("value"));
4078
4079 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4080 assert!(!result.is_empty());
4081 assert_eq!(result.len(), 1);
4082
4083 match &result[0] {
4084 ValidationError::InvalidParameter {
4085 parameter,
4086 suggestions,
4087 valid_parameters,
4088 } => {
4089 assert_eq!(parameter, "xyz123");
4090 assert!(suggestions.is_empty());
4091 assert!(valid_parameters.contains(&"limit".to_string()));
4092 assert!(valid_parameters.contains(&"offset".to_string()));
4093 }
4094 _ => panic!("Expected InvalidParameter variant"),
4095 }
4096 }
4097
4098 #[test]
4099 fn test_check_unknown_parameters_multiple_suggestions() {
4100 let mut properties = serde_json::Map::new();
4102 properties.insert("user_id".to_string(), json!({"type": "string"}));
4103 properties.insert("user_iid".to_string(), json!({"type": "string"}));
4104 properties.insert("user_name".to_string(), json!({"type": "string"}));
4105
4106 let mut args = serde_json::Map::new();
4107 args.insert("usr_id".to_string(), json!("123"));
4108
4109 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4110 assert!(!result.is_empty());
4111 assert_eq!(result.len(), 1);
4112
4113 match &result[0] {
4114 ValidationError::InvalidParameter {
4115 parameter,
4116 suggestions,
4117 valid_parameters,
4118 } => {
4119 assert_eq!(parameter, "usr_id");
4120 assert!(!suggestions.is_empty());
4121 assert!(suggestions.contains(&"user_id".to_string()));
4122 assert_eq!(valid_parameters.len(), 3);
4123 }
4124 _ => panic!("Expected InvalidParameter variant"),
4125 }
4126 }
4127
4128 #[test]
4129 fn test_check_unknown_parameters_valid() {
4130 let mut properties = serde_json::Map::new();
4132 properties.insert("name".to_string(), json!({"type": "string"}));
4133 properties.insert("email".to_string(), json!({"type": "string"}));
4134
4135 let mut args = serde_json::Map::new();
4136 args.insert("name".to_string(), json!("John"));
4137 args.insert("email".to_string(), json!("john@example.com"));
4138
4139 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4140 assert!(result.is_empty());
4141 }
4142
4143 #[test]
4144 fn test_check_unknown_parameters_empty() {
4145 let properties = serde_json::Map::new();
4147
4148 let mut args = serde_json::Map::new();
4149 args.insert("any_param".to_string(), json!("value"));
4150
4151 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4152 assert!(!result.is_empty());
4153 assert_eq!(result.len(), 1);
4154
4155 match &result[0] {
4156 ValidationError::InvalidParameter {
4157 parameter,
4158 suggestions,
4159 valid_parameters,
4160 } => {
4161 assert_eq!(parameter, "any_param");
4162 assert!(suggestions.is_empty());
4163 assert!(valid_parameters.is_empty());
4164 }
4165 _ => panic!("Expected InvalidParameter variant"),
4166 }
4167 }
4168
4169 #[test]
4170 fn test_check_unknown_parameters_gltf_pagination() {
4171 let mut properties = serde_json::Map::new();
4173 properties.insert(
4174 "page_number".to_string(),
4175 json!({
4176 "type": "integer",
4177 "x-original-name": "page[number]"
4178 }),
4179 );
4180 properties.insert(
4181 "page_size".to_string(),
4182 json!({
4183 "type": "integer",
4184 "x-original-name": "page[size]"
4185 }),
4186 );
4187
4188 let mut args = serde_json::Map::new();
4190 args.insert("page".to_string(), json!(1));
4191 args.insert("per_page".to_string(), json!(10));
4192
4193 let result = ToolGenerator::check_unknown_parameters(&args, &properties);
4194 assert_eq!(result.len(), 2, "Should have 2 unknown parameters");
4195
4196 let page_error = result
4198 .iter()
4199 .find(|e| {
4200 if let ValidationError::InvalidParameter { parameter, .. } = e {
4201 parameter == "page"
4202 } else {
4203 false
4204 }
4205 })
4206 .expect("Should have error for 'page'");
4207
4208 let per_page_error = result
4209 .iter()
4210 .find(|e| {
4211 if let ValidationError::InvalidParameter { parameter, .. } = e {
4212 parameter == "per_page"
4213 } else {
4214 false
4215 }
4216 })
4217 .expect("Should have error for 'per_page'");
4218
4219 match page_error {
4221 ValidationError::InvalidParameter {
4222 suggestions,
4223 valid_parameters,
4224 ..
4225 } => {
4226 assert!(
4227 suggestions.contains(&"page_number".to_string()),
4228 "Should suggest 'page_number' for 'page'"
4229 );
4230 assert_eq!(valid_parameters.len(), 2);
4231 assert!(valid_parameters.contains(&"page_number".to_string()));
4232 assert!(valid_parameters.contains(&"page_size".to_string()));
4233 }
4234 _ => panic!("Expected InvalidParameter"),
4235 }
4236
4237 match per_page_error {
4239 ValidationError::InvalidParameter {
4240 parameter,
4241 suggestions,
4242 valid_parameters,
4243 ..
4244 } => {
4245 assert_eq!(parameter, "per_page");
4246 assert_eq!(valid_parameters.len(), 2);
4247 if !suggestions.is_empty() {
4250 assert!(suggestions.contains(&"page_size".to_string()));
4251 }
4252 }
4253 _ => panic!("Expected InvalidParameter"),
4254 }
4255 }
4256
4257 #[test]
4258 fn test_validate_parameters_with_invalid_params() {
4259 let tool_metadata = ToolMetadata {
4261 name: "listItems".to_string(),
4262 title: None,
4263 description: "List items".to_string(),
4264 parameters: json!({
4265 "type": "object",
4266 "properties": {
4267 "page_number": {
4268 "type": "integer",
4269 "x-original-name": "page[number]"
4270 },
4271 "page_size": {
4272 "type": "integer",
4273 "x-original-name": "page[size]"
4274 }
4275 },
4276 "required": []
4277 }),
4278 output_schema: None,
4279 method: "GET".to_string(),
4280 path: "/items".to_string(),
4281 security: None,
4282 };
4283
4284 let arguments = json!({
4286 "page": 1,
4287 "per_page": 10
4288 });
4289
4290 let result = ToolGenerator::validate_parameters(&tool_metadata, &arguments);
4291 assert!(
4292 result.is_err(),
4293 "Should fail validation with unknown parameters"
4294 );
4295
4296 let error = result.unwrap_err();
4297 match error {
4298 ToolCallValidationError::InvalidParameters { violations } => {
4299 assert_eq!(violations.len(), 2, "Should have 2 validation errors");
4300
4301 let has_page_error = violations.iter().any(|v| {
4303 if let ValidationError::InvalidParameter { parameter, .. } = v {
4304 parameter == "page"
4305 } else {
4306 false
4307 }
4308 });
4309
4310 let has_per_page_error = violations.iter().any(|v| {
4311 if let ValidationError::InvalidParameter { parameter, .. } = v {
4312 parameter == "per_page"
4313 } else {
4314 false
4315 }
4316 });
4317
4318 assert!(has_page_error, "Should have error for 'page' parameter");
4319 assert!(
4320 has_per_page_error,
4321 "Should have error for 'per_page' parameter"
4322 );
4323 }
4324 _ => panic!("Expected InvalidParameters"),
4325 }
4326 }
4327
4328 #[test]
4329 fn test_cookie_parameter_sanitization() {
4330 let spec = create_test_spec();
4331
4332 let operation = Operation {
4333 operation_id: Some("testCookie".to_string()),
4334 parameters: vec![ObjectOrReference::Object(Parameter {
4335 name: "session[id]".to_string(),
4336 location: ParameterIn::Cookie,
4337 description: Some("Session ID".to_string()),
4338 required: Some(false),
4339 deprecated: Some(false),
4340 allow_empty_value: Some(false),
4341 style: None,
4342 explode: None,
4343 allow_reserved: Some(false),
4344 schema: Some(ObjectOrReference::Object(ObjectSchema {
4345 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4346 ..Default::default()
4347 })),
4348 example: None,
4349 examples: Default::default(),
4350 content: None,
4351 extensions: Default::default(),
4352 })],
4353 ..Default::default()
4354 };
4355
4356 let tool_metadata = ToolGenerator::generate_tool_metadata(
4357 &operation,
4358 "get".to_string(),
4359 "/data".to_string(),
4360 &spec,
4361 )
4362 .unwrap();
4363
4364 let properties = tool_metadata
4365 .parameters
4366 .get("properties")
4367 .unwrap()
4368 .as_object()
4369 .unwrap();
4370
4371 assert!(properties.contains_key("cookie_session_id"));
4373
4374 let arguments = json!({
4376 "cookie_session_id": "abc123"
4377 });
4378
4379 let extracted = ToolGenerator::extract_parameters(&tool_metadata, &arguments).unwrap();
4380
4381 assert_eq!(extracted.cookies.get("session[id]"), Some(&json!("abc123")));
4383 }
4384
4385 #[test]
4386 fn test_parameter_description_with_examples() {
4387 let spec = create_test_spec();
4388
4389 let param_with_example = Parameter {
4391 name: "status".to_string(),
4392 location: ParameterIn::Query,
4393 description: Some("Filter by status".to_string()),
4394 required: Some(false),
4395 deprecated: Some(false),
4396 allow_empty_value: Some(false),
4397 style: None,
4398 explode: None,
4399 allow_reserved: Some(false),
4400 schema: Some(ObjectOrReference::Object(ObjectSchema {
4401 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4402 ..Default::default()
4403 })),
4404 example: Some(json!("active")),
4405 examples: Default::default(),
4406 content: None,
4407 extensions: Default::default(),
4408 };
4409
4410 let (schema, _) =
4411 ToolGenerator::convert_parameter_schema(¶m_with_example, ParameterIn::Query, &spec)
4412 .unwrap();
4413 let description = schema.get("description").unwrap().as_str().unwrap();
4414 assert_eq!(description, "Filter by status. Example: `\"active\"`");
4415
4416 let mut examples_map = std::collections::BTreeMap::new();
4418 examples_map.insert(
4419 "example1".to_string(),
4420 ObjectOrReference::Object(oas3::spec::Example {
4421 value: Some(json!("pending")),
4422 ..Default::default()
4423 }),
4424 );
4425 examples_map.insert(
4426 "example2".to_string(),
4427 ObjectOrReference::Object(oas3::spec::Example {
4428 value: Some(json!("completed")),
4429 ..Default::default()
4430 }),
4431 );
4432
4433 let param_with_examples = Parameter {
4434 name: "status".to_string(),
4435 location: ParameterIn::Query,
4436 description: Some("Filter by status".to_string()),
4437 required: Some(false),
4438 deprecated: Some(false),
4439 allow_empty_value: Some(false),
4440 style: None,
4441 explode: None,
4442 allow_reserved: Some(false),
4443 schema: Some(ObjectOrReference::Object(ObjectSchema {
4444 schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4445 ..Default::default()
4446 })),
4447 example: None,
4448 examples: examples_map,
4449 content: None,
4450 extensions: Default::default(),
4451 };
4452
4453 let (schema, _) = ToolGenerator::convert_parameter_schema(
4454 ¶m_with_examples,
4455 ParameterIn::Query,
4456 &spec,
4457 )
4458 .unwrap();
4459 let description = schema.get("description").unwrap().as_str().unwrap();
4460 assert!(description.starts_with("Filter by status. Examples:\n"));
4461 assert!(description.contains("`\"pending\"`"));
4462 assert!(description.contains("`\"completed\"`"));
4463
4464 let param_no_desc = Parameter {
4466 name: "limit".to_string(),
4467 location: ParameterIn::Query,
4468 description: None,
4469 required: Some(false),
4470 deprecated: Some(false),
4471 allow_empty_value: Some(false),
4472 style: None,
4473 explode: None,
4474 allow_reserved: Some(false),
4475 schema: Some(ObjectOrReference::Object(ObjectSchema {
4476 schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
4477 ..Default::default()
4478 })),
4479 example: Some(json!(100)),
4480 examples: Default::default(),
4481 content: None,
4482 extensions: Default::default(),
4483 };
4484
4485 let (schema, _) =
4486 ToolGenerator::convert_parameter_schema(¶m_no_desc, ParameterIn::Query, &spec)
4487 .unwrap();
4488 let description = schema.get("description").unwrap().as_str().unwrap();
4489 assert_eq!(description, "limit parameter. Example: `100`");
4490 }
4491
4492 #[test]
4493 fn test_format_examples_for_description() {
4494 let examples = vec![json!("active")];
4496 let result = ToolGenerator::format_examples_for_description(&examples);
4497 assert_eq!(result, Some("Example: `\"active\"`".to_string()));
4498
4499 let examples = vec![json!(42)];
4501 let result = ToolGenerator::format_examples_for_description(&examples);
4502 assert_eq!(result, Some("Example: `42`".to_string()));
4503
4504 let examples = vec![json!(true)];
4506 let result = ToolGenerator::format_examples_for_description(&examples);
4507 assert_eq!(result, Some("Example: `true`".to_string()));
4508
4509 let examples = vec![json!("active"), json!("pending"), json!("completed")];
4511 let result = ToolGenerator::format_examples_for_description(&examples);
4512 assert_eq!(
4513 result,
4514 Some("Examples:\n- `\"active\"`\n- `\"pending\"`\n- `\"completed\"`".to_string())
4515 );
4516
4517 let examples = vec![json!(["a", "b", "c"])];
4519 let result = ToolGenerator::format_examples_for_description(&examples);
4520 assert_eq!(result, Some("Example: `[\"a\",\"b\",\"c\"]`".to_string()));
4521
4522 let examples = vec![json!({"key": "value"})];
4524 let result = ToolGenerator::format_examples_for_description(&examples);
4525 assert_eq!(result, Some("Example: `{\"key\":\"value\"}`".to_string()));
4526
4527 let examples = vec![];
4529 let result = ToolGenerator::format_examples_for_description(&examples);
4530 assert_eq!(result, None);
4531
4532 let examples = vec![json!(null)];
4534 let result = ToolGenerator::format_examples_for_description(&examples);
4535 assert_eq!(result, Some("Example: `null`".to_string()));
4536
4537 let examples = vec![json!("text"), json!(123), json!(true)];
4539 let result = ToolGenerator::format_examples_for_description(&examples);
4540 assert_eq!(
4541 result,
4542 Some("Examples:\n- `\"text\"`\n- `123`\n- `true`".to_string())
4543 );
4544
4545 let examples = vec![json!(["a", "b", "c", "d", "e", "f"])];
4547 let result = ToolGenerator::format_examples_for_description(&examples);
4548 assert_eq!(
4549 result,
4550 Some("Example: `[\"a\",\"b\",\"c\",\"d\",\"e\",\"f\"]`".to_string())
4551 );
4552
4553 let examples = vec![json!([1, 2])];
4555 let result = ToolGenerator::format_examples_for_description(&examples);
4556 assert_eq!(result, Some("Example: `[1,2]`".to_string()));
4557
4558 let examples = vec![json!({"user": {"name": "John", "age": 30}})];
4560 let result = ToolGenerator::format_examples_for_description(&examples);
4561 assert_eq!(
4562 result,
4563 Some("Example: `{\"user\":{\"name\":\"John\",\"age\":30}}`".to_string())
4564 );
4565
4566 let examples = vec![json!("a"), json!("b"), json!("c"), json!("d"), json!("e")];
4568 let result = ToolGenerator::format_examples_for_description(&examples);
4569 assert_eq!(
4570 result,
4571 Some("Examples:\n- `\"a\"`\n- `\"b\"`\n- `\"c\"`\n- `\"d\"`\n- `\"e\"`".to_string())
4572 );
4573
4574 let examples = vec![json!(3.5)];
4576 let result = ToolGenerator::format_examples_for_description(&examples);
4577 assert_eq!(result, Some("Example: `3.5`".to_string()));
4578
4579 let examples = vec![json!(-42)];
4581 let result = ToolGenerator::format_examples_for_description(&examples);
4582 assert_eq!(result, Some("Example: `-42`".to_string()));
4583
4584 let examples = vec![json!(false)];
4586 let result = ToolGenerator::format_examples_for_description(&examples);
4587 assert_eq!(result, Some("Example: `false`".to_string()));
4588
4589 let examples = vec![json!("hello \"world\"")];
4591 let result = ToolGenerator::format_examples_for_description(&examples);
4592 assert_eq!(result, Some(r#"Example: `"hello \"world\""`"#.to_string()));
4594
4595 let examples = vec![json!("")];
4597 let result = ToolGenerator::format_examples_for_description(&examples);
4598 assert_eq!(result, Some("Example: `\"\"`".to_string()));
4599
4600 let examples = vec![json!([])];
4602 let result = ToolGenerator::format_examples_for_description(&examples);
4603 assert_eq!(result, Some("Example: `[]`".to_string()));
4604
4605 let examples = vec![json!({})];
4607 let result = ToolGenerator::format_examples_for_description(&examples);
4608 assert_eq!(result, Some("Example: `{}`".to_string()));
4609 }
4610
4611 #[test]
4612 fn test_reference_metadata_functionality() {
4613 let metadata = ReferenceMetadata::new(
4615 Some("User Reference".to_string()),
4616 Some("A reference to user data with additional context".to_string()),
4617 );
4618
4619 assert!(!metadata.is_empty());
4620 assert_eq!(metadata.summary(), Some("User Reference"));
4621 assert_eq!(
4622 metadata.best_description(),
4623 Some("A reference to user data with additional context")
4624 );
4625
4626 let summary_only = ReferenceMetadata::new(Some("Pet Summary".to_string()), None);
4628 assert_eq!(summary_only.best_description(), Some("Pet Summary"));
4629
4630 let empty_metadata = ReferenceMetadata::new(None, None);
4632 assert!(empty_metadata.is_empty());
4633 assert_eq!(empty_metadata.best_description(), None);
4634
4635 let metadata = ReferenceMetadata::new(
4637 Some("Reference Summary".to_string()),
4638 Some("Reference Description".to_string()),
4639 );
4640
4641 let result = metadata.merge_with_description(None, false);
4643 assert_eq!(result, Some("Reference Description".to_string()));
4644
4645 let result = metadata.merge_with_description(Some("Existing desc"), false);
4647 assert_eq!(result, Some("Reference Description".to_string()));
4648
4649 let result = metadata.merge_with_description(Some("Existing desc"), true);
4651 assert_eq!(result, Some("Reference Description".to_string()));
4652
4653 let result = metadata.enhance_parameter_description("userId", Some("User ID parameter"));
4655 assert_eq!(result, Some("userId: Reference Description".to_string()));
4656
4657 let result = metadata.enhance_parameter_description("userId", None);
4658 assert_eq!(result, Some("userId: Reference Description".to_string()));
4659
4660 let summary_only = ReferenceMetadata::new(Some("API Token".to_string()), None);
4662
4663 let result = summary_only.merge_with_description(Some("Generic token"), false);
4664 assert_eq!(result, Some("API Token".to_string()));
4665
4666 let result = summary_only.merge_with_description(Some("Different desc"), true);
4667 assert_eq!(result, Some("API Token".to_string())); let result = summary_only.enhance_parameter_description("token", Some("Token field"));
4670 assert_eq!(result, Some("token: API Token".to_string()));
4671
4672 let empty_meta = ReferenceMetadata::new(None, None);
4674
4675 let result = empty_meta.merge_with_description(Some("Schema description"), false);
4676 assert_eq!(result, Some("Schema description".to_string()));
4677
4678 let result = empty_meta.enhance_parameter_description("param", Some("Schema param"));
4679 assert_eq!(result, Some("Schema param".to_string()));
4680
4681 let result = empty_meta.enhance_parameter_description("param", None);
4682 assert_eq!(result, Some("param parameter".to_string()));
4683 }
4684
4685 #[test]
4686 fn test_parameter_schema_with_reference_metadata() {
4687 let mut spec = create_test_spec();
4688
4689 spec.components.as_mut().unwrap().schemas.insert(
4691 "Pet".to_string(),
4692 ObjectOrReference::Object(ObjectSchema {
4693 description: None, schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
4695 ..Default::default()
4696 }),
4697 );
4698
4699 let param_with_ref = Parameter {
4701 name: "user".to_string(),
4702 location: ParameterIn::Query,
4703 description: None,
4704 required: Some(true),
4705 deprecated: Some(false),
4706 allow_empty_value: Some(false),
4707 style: None,
4708 explode: None,
4709 allow_reserved: Some(false),
4710 schema: Some(ObjectOrReference::Ref {
4711 ref_path: "#/components/schemas/Pet".to_string(),
4712 summary: Some("Pet Reference".to_string()),
4713 description: Some("A reference to pet schema with additional context".to_string()),
4714 }),
4715 example: None,
4716 examples: BTreeMap::new(),
4717 content: None,
4718 extensions: Default::default(),
4719 };
4720
4721 let result =
4723 ToolGenerator::convert_parameter_schema(¶m_with_ref, ParameterIn::Query, &spec);
4724
4725 assert!(result.is_ok());
4726 let (schema, _annotations) = result.unwrap();
4727
4728 let description = schema.get("description").and_then(|v| v.as_str());
4730 assert!(description.is_some());
4731 assert!(
4733 description.unwrap().contains("Pet Reference")
4734 || description
4735 .unwrap()
4736 .contains("A reference to pet schema with additional context")
4737 );
4738 }
4739
4740 #[test]
4741 fn test_request_body_with_reference_metadata() {
4742 let spec = create_test_spec();
4743
4744 let request_body_ref = ObjectOrReference::Ref {
4746 ref_path: "#/components/requestBodies/PetBody".to_string(),
4747 summary: Some("Pet Request Body".to_string()),
4748 description: Some(
4749 "Request body containing pet information for API operations".to_string(),
4750 ),
4751 };
4752
4753 let result = ToolGenerator::convert_request_body_to_json_schema(&request_body_ref, &spec);
4754
4755 assert!(result.is_ok());
4756 let schema_result = result.unwrap();
4757 assert!(schema_result.is_some());
4758
4759 let (schema, _annotations, _required) = schema_result.unwrap();
4760 let description = schema.get("description").and_then(|v| v.as_str());
4761
4762 assert!(description.is_some());
4763 assert_eq!(
4765 description.unwrap(),
4766 "Request body containing pet information for API operations"
4767 );
4768 }
4769
4770 #[test]
4771 fn test_response_schema_with_reference_metadata() {
4772 let spec = create_test_spec();
4773
4774 let mut responses = BTreeMap::new();
4776 responses.insert(
4777 "200".to_string(),
4778 ObjectOrReference::Ref {
4779 ref_path: "#/components/responses/PetResponse".to_string(),
4780 summary: Some("Successful Pet Response".to_string()),
4781 description: Some(
4782 "Response containing pet data on successful operation".to_string(),
4783 ),
4784 },
4785 );
4786 let responses_option = Some(responses);
4787
4788 let result = ToolGenerator::extract_output_schema(&responses_option, &spec);
4789
4790 assert!(result.is_ok());
4791 let schema = result.unwrap();
4792 assert!(schema.is_some());
4793
4794 let schema_value = schema.unwrap();
4795 let body_desc = schema_value
4796 .get("properties")
4797 .and_then(|props| props.get("body"))
4798 .and_then(|body| body.get("description"))
4799 .and_then(|desc| desc.as_str());
4800
4801 assert!(body_desc.is_some());
4802 assert_eq!(
4804 body_desc.unwrap(),
4805 "Response containing pet data on successful operation"
4806 );
4807 }
4808}