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