Skip to main content

rmcp_openapi/
spec.rs

1use crate::error::Error;
2use crate::normalize_tag;
3use crate::tool::ToolMetadata;
4use crate::tool_generator::ToolGenerator;
5use bon::Builder;
6use oas3::Spec as Oas3Spec;
7use reqwest::Method;
8use serde::de::IntoDeserializer;
9use serde_json::Value;
10
11/// OpenAPI specification wrapper that provides convenience methods
12/// for working with oas3::Spec
13#[derive(Debug, Clone)]
14pub struct Spec {
15    pub spec: Oas3Spec,
16}
17
18impl Spec {
19    /// Parse an OpenAPI specification from a JSON value.
20    ///
21    /// Uses `serde_path_to_error` to provide the exact JSON path on
22    /// deserialization failures, turning opaque messages like
23    /// "data did not match any variant of untagged enum" into actionable
24    /// diagnostics that pinpoint the offending location in the spec.
25    pub fn from_value(json_value: Value) -> Result<Self, Error> {
26        let spec: Oas3Spec = serde_path_to_error::deserialize(json_value.into_deserializer())
27            .map_err(|err| Error::JsonAtPath {
28                path: err.path().to_string(),
29                source: err.into_inner(),
30            })?;
31        Ok(Spec { spec })
32    }
33
34    /// Convert all operations to MCP tool metadata
35    pub fn to_tool_metadata(
36        &self,
37        filters: Option<&Filters>,
38        skip_tool_descriptions: bool,
39        skip_parameter_descriptions: bool,
40    ) -> Result<Vec<ToolMetadata>, Error> {
41        let mut tools = Vec::new();
42
43        if let Some(paths) = &self.spec.paths {
44            for (path, path_item) in paths {
45                // Handle operations in the path item
46                let operations = [
47                    (Method::GET, &path_item.get),
48                    (Method::POST, &path_item.post),
49                    (Method::PUT, &path_item.put),
50                    (Method::DELETE, &path_item.delete),
51                    (Method::PATCH, &path_item.patch),
52                    (Method::HEAD, &path_item.head),
53                    (Method::OPTIONS, &path_item.options),
54                    (Method::TRACE, &path_item.trace),
55                ];
56
57                for (method, operation_ref) in operations {
58                    if let Some(operation) = operation_ref {
59                        if let Some(filters) = filters {
60                            // Filter by methods if specified
61                            match &filters.methods {
62                                Some(Filter::Include(m)) if !m.contains(&method) => continue,
63                                Some(Filter::Exclude(m)) if m.contains(&method) => continue,
64                                _ => {}
65                            }
66
67                            // Filter by tags if specified (with kebab-case normalization)
68                            match (&filters.tags, operation.tags.is_empty()) {
69                                (Some(Filter::Include(tags)), false) => {
70                                    let normalized_filter_tags: Vec<String> =
71                                        tags.iter().map(|tag| normalize_tag(tag)).collect();
72
73                                    let has_matching_tag =
74                                        operation.tags.iter().any(|operation_tag| {
75                                            let normalized_operation_tag =
76                                                normalize_tag(operation_tag);
77                                            normalized_filter_tags
78                                                .contains(&normalized_operation_tag)
79                                        });
80
81                                    if !has_matching_tag {
82                                        continue; // Skip this operation
83                                    }
84                                }
85                                (Some(Filter::Exclude(tags)), false) => {
86                                    let normalized_filter_tags: Vec<String> =
87                                        tags.iter().map(|tag| normalize_tag(tag)).collect();
88
89                                    let has_matching_tag =
90                                        operation.tags.iter().any(|operation_tag| {
91                                            let normalized_operation_tag =
92                                                normalize_tag(operation_tag);
93                                            normalized_filter_tags
94                                                .contains(&normalized_operation_tag)
95                                        });
96
97                                    if has_matching_tag {
98                                        continue; // Skip this operation
99                                    }
100                                }
101                                (_, true) => continue, // Skip operations without tags when filtering
102                                _ => {}
103                            }
104
105                            // Filter by OperationId
106                            match (operation.operation_id.as_ref(), &filters.operations_id) {
107                                (Some(op), Some(Filter::Include(ops))) if !ops.contains(op) => {
108                                    continue;
109                                }
110                                (Some(op), Some(Filter::Exclude(ops))) if ops.contains(op) => {
111                                    continue;
112                                }
113                                _ => {}
114                            }
115                        }
116
117                        let tool_metadata = ToolGenerator::generate_tool_metadata(
118                            operation,
119                            method.to_string(),
120                            path.clone(),
121                            &self.spec,
122                            skip_tool_descriptions,
123                            skip_parameter_descriptions,
124                        )?;
125                        tools.push(tool_metadata);
126                    }
127                }
128            }
129        }
130
131        Ok(tools)
132    }
133
134    /// Convert all operations to OpenApiTool instances with HTTP configuration
135    ///
136    /// # Errors
137    ///
138    /// Returns an error if any operations cannot be converted or OpenApiTool instances cannot be created
139    pub fn to_openapi_tools(
140        &self,
141        filters: Option<&Filters>,
142        base_url: Option<url::Url>,
143        default_headers: Option<reqwest::header::HeaderMap>,
144        skip_tool_descriptions: bool,
145        skip_parameter_descriptions: bool,
146        insecure: bool,
147    ) -> Result<Vec<crate::tool::Tool>, Error> {
148        // First generate the tool metadata using existing method
149        let tools_metadata =
150            self.to_tool_metadata(filters, skip_tool_descriptions, skip_parameter_descriptions)?;
151
152        // Then convert to Tool instances
153        crate::tool_generator::ToolGenerator::generate_openapi_tools(
154            tools_metadata,
155            base_url,
156            default_headers,
157            insecure,
158        )
159    }
160
161    /// Get operation by operation ID
162    pub fn get_operation(
163        &self,
164        operation_id: &str,
165    ) -> Option<(&oas3::spec::Operation, String, String)> {
166        if let Some(paths) = &self.spec.paths {
167            for (path, path_item) in paths {
168                let operations = [
169                    (Method::GET, &path_item.get),
170                    (Method::POST, &path_item.post),
171                    (Method::PUT, &path_item.put),
172                    (Method::DELETE, &path_item.delete),
173                    (Method::PATCH, &path_item.patch),
174                    (Method::HEAD, &path_item.head),
175                    (Method::OPTIONS, &path_item.options),
176                    (Method::TRACE, &path_item.trace),
177                ];
178
179                for (method, operation_ref) in operations {
180                    if let Some(operation) = operation_ref {
181                        let default_id = format!(
182                            "{}_{}",
183                            method,
184                            path.replace('/', "_").replace(['{', '}'], "")
185                        );
186                        let op_id = operation.operation_id.as_deref().unwrap_or(&default_id);
187
188                        if op_id == operation_id {
189                            return Some((operation, method.to_string(), path.clone()));
190                        }
191                    }
192                }
193            }
194        }
195        None
196    }
197
198    /// Get all operation IDs
199    pub fn get_operation_ids(&self) -> Vec<String> {
200        let mut operation_ids = Vec::new();
201
202        if let Some(paths) = &self.spec.paths {
203            for (path, path_item) in paths {
204                let operations = [
205                    (Method::GET, &path_item.get),
206                    (Method::POST, &path_item.post),
207                    (Method::PUT, &path_item.put),
208                    (Method::DELETE, &path_item.delete),
209                    (Method::PATCH, &path_item.patch),
210                    (Method::HEAD, &path_item.head),
211                    (Method::OPTIONS, &path_item.options),
212                    (Method::TRACE, &path_item.trace),
213                ];
214
215                for (method, operation_ref) in operations {
216                    if let Some(operation) = operation_ref {
217                        let default_id = format!(
218                            "{}_{}",
219                            method,
220                            path.replace('/', "_").replace(['{', '}'], "")
221                        );
222                        let op_id = operation.operation_id.as_deref().unwrap_or(&default_id);
223                        operation_ids.push(op_id.to_string());
224                    }
225                }
226            }
227        }
228
229        operation_ids
230    }
231}
232
233#[derive(Builder, Debug, Clone)]
234pub struct Filters {
235    pub tags: Option<Filter<String>>,
236    pub methods: Option<Filter<reqwest::Method>>,
237    pub operations_id: Option<Filter<String>>,
238}
239
240#[derive(Debug, Clone)]
241pub enum Filter<T> {
242    Include(Vec<T>),
243    Exclude(Vec<T>),
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use serde_json::json;
250
251    fn create_test_spec_with_tags() -> Spec {
252        let spec_json = json!({
253            "openapi": "3.0.3",
254            "info": {
255                "title": "Test API",
256                "version": "1.0.0"
257            },
258            "paths": {
259                "/pets": {
260                    "get": {
261                        "operationId": "listPets",
262                        "tags": ["pet", "list"],
263                        "responses": {
264                            "200": {
265                                "description": "List of pets"
266                            }
267                        }
268                    },
269                    "post": {
270                        "operationId": "createPet",
271                        "tags": ["pet"],
272                        "responses": {
273                            "201": {
274                                "description": "Pet created"
275                            }
276                        }
277                    }
278                },
279                "/users": {
280                    "get": {
281                        "operationId": "listUsers",
282                        "tags": ["user"],
283                        "responses": {
284                            "200": {
285                                "description": "List of users"
286                            }
287                        }
288                    }
289                },
290                "/admin": {
291                    "get": {
292                        "operationId": "adminPanel",
293                        "tags": ["admin", "management"],
294                        "responses": {
295                            "200": {
296                                "description": "Admin panel"
297                            }
298                        }
299                    }
300                },
301                "/public": {
302                    "get": {
303                        "operationId": "publicEndpoint",
304                        "responses": {
305                            "200": {
306                                "description": "Public endpoint with no tags"
307                            }
308                        }
309                    }
310                }
311            }
312        });
313
314        Spec::from_value(spec_json).expect("Failed to create test spec")
315    }
316
317    fn create_test_spec_with_mixed_case_tags() -> Spec {
318        let spec_json = json!({
319            "openapi": "3.0.3",
320            "info": {
321                "title": "Test API with Mixed Case Tags",
322                "version": "1.0.0"
323            },
324            "paths": {
325                "/camel": {
326                    "get": {
327                        "operationId": "camelCaseOperation",
328                        "tags": ["userManagement"],
329                        "responses": {
330                            "200": {
331                                "description": "camelCase tag"
332                            }
333                        }
334                    }
335                },
336                "/pascal": {
337                    "get": {
338                        "operationId": "pascalCaseOperation",
339                        "tags": ["UserManagement"],
340                        "responses": {
341                            "200": {
342                                "description": "PascalCase tag"
343                            }
344                        }
345                    }
346                },
347                "/snake": {
348                    "get": {
349                        "operationId": "snakeCaseOperation",
350                        "tags": ["user_management"],
351                        "responses": {
352                            "200": {
353                                "description": "snake_case tag"
354                            }
355                        }
356                    }
357                },
358                "/screaming": {
359                    "get": {
360                        "operationId": "screamingCaseOperation",
361                        "tags": ["USER_MANAGEMENT"],
362                        "responses": {
363                            "200": {
364                                "description": "SCREAMING_SNAKE_CASE tag"
365                            }
366                        }
367                    }
368                },
369                "/kebab": {
370                    "get": {
371                        "operationId": "kebabCaseOperation",
372                        "tags": ["user-management"],
373                        "responses": {
374                            "200": {
375                                "description": "kebab-case tag"
376                            }
377                        }
378                    }
379                },
380                "/mixed": {
381                    "get": {
382                        "operationId": "mixedCaseOperation",
383                        "tags": ["XMLHttpRequest", "HTTPSConnection", "APIKey"],
384                        "responses": {
385                            "200": {
386                                "description": "Mixed case with acronyms"
387                            }
388                        }
389                    }
390                }
391            }
392        });
393
394        Spec::from_value(spec_json).expect("Failed to create test spec")
395    }
396
397    fn create_test_spec_with_methods() -> Spec {
398        let spec_json = json!({
399            "openapi": "3.0.3",
400            "info": {
401                "title": "Test API with Multiple Methods",
402                "version": "1.0.0"
403            },
404            "paths": {
405                "/users": {
406                    "get": {
407                        "operationId": "listUsers",
408                        "tags": ["user"],
409                        "responses": {
410                            "200": {
411                                "description": "List of users"
412                            }
413                        }
414                    },
415                    "post": {
416                        "operationId": "createUser",
417                        "tags": ["user"],
418                        "responses": {
419                            "201": {
420                                "description": "User created"
421                            }
422                        }
423                    },
424                    "put": {
425                        "operationId": "updateUser",
426                        "tags": ["user"],
427                        "responses": {
428                            "200": {
429                                "description": "User updated"
430                            }
431                        }
432                    },
433                    "delete": {
434                        "operationId": "deleteUser",
435                        "tags": ["user"],
436                        "responses": {
437                            "204": {
438                                "description": "User deleted"
439                            }
440                        }
441                    }
442                },
443                "/pets": {
444                    "get": {
445                        "operationId": "listPets",
446                        "tags": ["pet"],
447                        "responses": {
448                            "200": {
449                                "description": "List of pets"
450                            }
451                        }
452                    },
453                    "post": {
454                        "operationId": "createPet",
455                        "tags": ["pet"],
456                        "responses": {
457                            "201": {
458                                "description": "Pet created"
459                            }
460                        }
461                    },
462                    "patch": {
463                        "operationId": "patchPet",
464                        "tags": ["pet"],
465                        "responses": {
466                            "200": {
467                                "description": "Pet patched"
468                            }
469                        }
470                    }
471                },
472                "/health": {
473                    "head": {
474                        "operationId": "healthCheck",
475                        "tags": ["health"],
476                        "responses": {
477                            "200": {
478                                "description": "Health check"
479                            }
480                        }
481                    },
482                    "options": {
483                        "operationId": "healthOptions",
484                        "tags": ["health"],
485                        "responses": {
486                            "200": {
487                                "description": "Health options"
488                            }
489                        }
490                    }
491                }
492            }
493        });
494
495        Spec::from_value(spec_json).expect("Failed to create test spec")
496    }
497
498    #[test]
499    fn test_tag_filtering_no_filter() {
500        let spec = create_test_spec_with_tags();
501        let tools = spec
502            .to_tool_metadata(None, false, false)
503            .expect("Failed to generate tools");
504
505        // All operations should be included
506        assert_eq!(tools.len(), 5);
507
508        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
509        assert!(tool_names.contains(&"listPets"));
510        assert!(tool_names.contains(&"createPet"));
511        assert!(tool_names.contains(&"listUsers"));
512        assert!(tool_names.contains(&"adminPanel"));
513        assert!(tool_names.contains(&"publicEndpoint"));
514    }
515
516    #[test]
517    fn test_tag_filtering_single_tag() {
518        let spec = create_test_spec_with_tags();
519        let filters = Some(
520            Filters::builder()
521                .tags(Filter::Include(vec!["pet".to_string()]))
522                .build(),
523        );
524        let tools = spec
525            .to_tool_metadata(filters.as_ref(), false, false)
526            .expect("Failed to generate tools");
527
528        // Only pet operations should be included
529        assert_eq!(tools.len(), 2);
530
531        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
532        assert!(tool_names.contains(&"listPets"));
533        assert!(tool_names.contains(&"createPet"));
534        assert!(!tool_names.contains(&"listUsers"));
535        assert!(!tool_names.contains(&"adminPanel"));
536        assert!(!tool_names.contains(&"publicEndpoint"));
537    }
538
539    #[test]
540    fn test_tag_filtering_multiple_tags() {
541        let spec = create_test_spec_with_tags();
542        let filters = Some(
543            Filters::builder()
544                .tags(Filter::Include(vec!["pet".to_string(), "user".to_string()]))
545                .build(),
546        );
547        let tools = spec
548            .to_tool_metadata(filters.as_ref(), false, false)
549            .expect("Failed to generate tools");
550
551        // Pet and user operations should be included
552        assert_eq!(tools.len(), 3);
553
554        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
555        assert!(tool_names.contains(&"listPets"));
556        assert!(tool_names.contains(&"createPet"));
557        assert!(tool_names.contains(&"listUsers"));
558        assert!(!tool_names.contains(&"adminPanel"));
559        assert!(!tool_names.contains(&"publicEndpoint"));
560    }
561
562    #[test]
563    fn test_tag_filtering_or_logic() {
564        let spec = create_test_spec_with_tags();
565        let filters = Some(
566            Filters::builder()
567                .tags(Filter::Include(vec!["list".to_string()]))
568                .build(),
569        );
570        let tools = spec
571            .to_tool_metadata(filters.as_ref(), false, false)
572            .expect("Failed to generate tools");
573
574        // Only operations with "list" tag should be included
575        assert_eq!(tools.len(), 1);
576
577        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
578        assert!(tool_names.contains(&"listPets")); // Has both "pet" and "list" tags
579        assert!(!tool_names.contains(&"createPet")); // Only has "pet" tag
580    }
581
582    #[test]
583    fn test_tag_filtering_no_matching_tags() {
584        let spec = create_test_spec_with_tags();
585        let filters = Some(
586            Filters::builder()
587                .tags(Filter::Include(vec!["nonexistent".to_string()]))
588                .build(),
589        );
590        let tools = spec
591            .to_tool_metadata(filters.as_ref(), false, false)
592            .expect("Failed to generate tools");
593
594        // No operations should be included
595        assert_eq!(tools.len(), 0);
596    }
597
598    #[test]
599    fn test_tag_filtering_excludes_operations_without_tags() {
600        let spec = create_test_spec_with_tags();
601        let filters = Some(
602            Filters::builder()
603                .tags(Filter::Include(vec!["admin".to_string()]))
604                .build(),
605        );
606        let tools = spec
607            .to_tool_metadata(filters.as_ref(), false, false)
608            .expect("Failed to generate tools");
609
610        // Only admin operations should be included, public endpoint (no tags) should be excluded
611        assert_eq!(tools.len(), 1);
612
613        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
614        assert!(tool_names.contains(&"adminPanel"));
615        assert!(!tool_names.contains(&"publicEndpoint")); // No tags, should be excluded
616    }
617
618    #[test]
619    fn test_tag_normalization_all_cases_match() {
620        let spec = create_test_spec_with_mixed_case_tags();
621        let filters = Some(
622            Filters::builder()
623                .tags(Filter::Include(vec!["user-management".to_string()]))
624                .build(),
625        );
626        let tools = spec
627            .to_tool_metadata(filters.as_ref(), false, false)
628            .expect("Failed to generate tools");
629
630        // All userManagement variants should match user-management filter
631        assert_eq!(tools.len(), 5);
632
633        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
634        assert!(tool_names.contains(&"camelCaseOperation")); // userManagement
635        assert!(tool_names.contains(&"pascalCaseOperation")); // UserManagement
636        assert!(tool_names.contains(&"snakeCaseOperation")); // user_management
637        assert!(tool_names.contains(&"screamingCaseOperation")); // USER_MANAGEMENT
638        assert!(tool_names.contains(&"kebabCaseOperation")); // user-management
639        assert!(!tool_names.contains(&"mixedCaseOperation")); // Different tags
640    }
641
642    #[test]
643    fn test_tag_normalization_camel_case_filter() {
644        let spec = create_test_spec_with_mixed_case_tags();
645        let filters = Some(
646            Filters::builder()
647                .tags(Filter::Include(vec!["userManagement".to_string()]))
648                .build(),
649        );
650        let tools = spec
651            .to_tool_metadata(filters.as_ref(), false, false)
652            .expect("Failed to generate tools");
653
654        // All userManagement variants should match camelCase filter
655        assert_eq!(tools.len(), 5);
656
657        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
658        assert!(tool_names.contains(&"camelCaseOperation"));
659        assert!(tool_names.contains(&"pascalCaseOperation"));
660        assert!(tool_names.contains(&"snakeCaseOperation"));
661        assert!(tool_names.contains(&"screamingCaseOperation"));
662        assert!(tool_names.contains(&"kebabCaseOperation"));
663    }
664
665    #[test]
666    fn test_tag_normalization_snake_case_filter() {
667        let spec = create_test_spec_with_mixed_case_tags();
668        let filters = Some(
669            Filters::builder()
670                .tags(Filter::Include(vec!["user_management".to_string()]))
671                .build(),
672        );
673        let tools = spec
674            .to_tool_metadata(filters.as_ref(), false, false)
675            .expect("Failed to generate tools");
676
677        // All userManagement variants should match snake_case filter
678        assert_eq!(tools.len(), 5);
679    }
680
681    #[test]
682    fn test_tag_normalization_acronyms() {
683        let spec = create_test_spec_with_mixed_case_tags();
684        let filters = Some(
685            Filters::builder()
686                .tags(Filter::Include(vec!["xml-http-request".to_string()]))
687                .build(),
688        );
689        let tools = spec
690            .to_tool_metadata(filters.as_ref(), false, false)
691            .expect("Failed to generate tools");
692
693        // Should match XMLHttpRequest tag
694        assert_eq!(tools.len(), 1);
695
696        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
697        assert!(tool_names.contains(&"mixedCaseOperation"));
698    }
699
700    #[test]
701    fn test_tag_normalization_multiple_mixed_filters() {
702        let spec = create_test_spec_with_mixed_case_tags();
703        let filters = Some(
704            Filters::builder()
705                .tags(Filter::Include(vec![
706                    "user-management".to_string(),
707                    "HTTPSConnection".to_string(),
708                ]))
709                .build(),
710        );
711        let tools = spec
712            .to_tool_metadata(filters.as_ref(), false, false)
713            .expect("Failed to generate tools");
714
715        // Should match all userManagement variants + mixedCaseOperation (for HTTPSConnection)
716        assert_eq!(tools.len(), 6);
717
718        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
719        assert!(tool_names.contains(&"camelCaseOperation"));
720        assert!(tool_names.contains(&"pascalCaseOperation"));
721        assert!(tool_names.contains(&"snakeCaseOperation"));
722        assert!(tool_names.contains(&"screamingCaseOperation"));
723        assert!(tool_names.contains(&"kebabCaseOperation"));
724        assert!(tool_names.contains(&"mixedCaseOperation"));
725    }
726
727    #[test]
728    fn test_tag_filtering_empty_filter_list() {
729        let spec = create_test_spec_with_tags();
730        let filters = Some(Filters::builder().tags(Filter::Include(vec![])).build());
731        let tools = spec
732            .to_tool_metadata(filters.as_ref(), false, false)
733            .expect("Failed to generate tools");
734
735        // Empty filter should exclude all operations
736        dbg!(&tools);
737        assert_eq!(tools.len(), 0);
738    }
739
740    #[test]
741    fn test_tag_filtering_complex_scenario() {
742        let spec = create_test_spec_with_tags();
743        let filters = Some(
744            Filters::builder()
745                .tags(Filter::Include(vec![
746                    "management".to_string(),
747                    "list".to_string(),
748                ]))
749                .build(),
750        );
751        let tools = spec
752            .to_tool_metadata(filters.as_ref(), false, false)
753            .expect("Failed to generate tools");
754
755        // Should include adminPanel (has "management") and listPets (has "list")
756        assert_eq!(tools.len(), 2);
757
758        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
759        assert!(tool_names.contains(&"adminPanel"));
760        assert!(tool_names.contains(&"listPets"));
761        assert!(!tool_names.contains(&"createPet"));
762        assert!(!tool_names.contains(&"listUsers"));
763        assert!(!tool_names.contains(&"publicEndpoint"));
764    }
765
766    #[test]
767    fn test_method_filtering_no_filter() {
768        let spec = create_test_spec_with_methods();
769        let tools = spec
770            .to_tool_metadata(None, false, false)
771            .expect("Failed to generate tools");
772
773        // All operations should be included (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)
774        assert_eq!(tools.len(), 9);
775
776        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
777        assert!(tool_names.contains(&"listUsers")); // GET /users
778        assert!(tool_names.contains(&"createUser")); // POST /users
779        assert!(tool_names.contains(&"updateUser")); // PUT /users
780        assert!(tool_names.contains(&"deleteUser")); // DELETE /users
781        assert!(tool_names.contains(&"listPets")); // GET /pets
782        assert!(tool_names.contains(&"createPet")); // POST /pets
783        assert!(tool_names.contains(&"patchPet")); // PATCH /pets
784        assert!(tool_names.contains(&"healthCheck")); // HEAD /health
785        assert!(tool_names.contains(&"healthOptions")); // OPTIONS /health
786    }
787
788    #[test]
789    fn test_method_filtering_single_method() {
790        use reqwest::Method;
791
792        let spec = create_test_spec_with_methods();
793        let filters = Some(
794            Filters::builder()
795                .methods(Filter::Include(vec![Method::GET]))
796                .build(),
797        );
798        let tools = spec
799            .to_tool_metadata(filters.as_ref(), false, false)
800            .expect("Failed to generate tools");
801
802        // Only GET operations should be included
803        assert_eq!(tools.len(), 2);
804
805        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
806        assert!(tool_names.contains(&"listUsers")); // GET /users
807        assert!(tool_names.contains(&"listPets")); // GET /pets
808        assert!(!tool_names.contains(&"createUser")); // POST /users
809        assert!(!tool_names.contains(&"updateUser")); // PUT /users
810        assert!(!tool_names.contains(&"deleteUser")); // DELETE /users
811        assert!(!tool_names.contains(&"createPet")); // POST /pets
812        assert!(!tool_names.contains(&"patchPet")); // PATCH /pets
813        assert!(!tool_names.contains(&"healthCheck")); // HEAD /health
814        assert!(!tool_names.contains(&"healthOptions")); // OPTIONS /health
815    }
816
817    #[test]
818    fn test_method_filtering_multiple_methods() {
819        use reqwest::Method;
820
821        let spec = create_test_spec_with_methods();
822        let filters = Some(
823            Filters::builder()
824                .methods(Filter::Include(vec![Method::GET, Method::POST]))
825                .build(),
826        );
827        let tools = spec
828            .to_tool_metadata(filters.as_ref(), false, false)
829            .expect("Failed to generate tools");
830
831        // Only GET and POST operations should be included
832        assert_eq!(tools.len(), 4);
833
834        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
835        assert!(tool_names.contains(&"listUsers")); // GET /users
836        assert!(tool_names.contains(&"createUser")); // POST /users
837        assert!(tool_names.contains(&"listPets")); // GET /pets
838        assert!(tool_names.contains(&"createPet")); // POST /pets
839        assert!(!tool_names.contains(&"updateUser")); // PUT /users
840        assert!(!tool_names.contains(&"deleteUser")); // DELETE /users
841        assert!(!tool_names.contains(&"patchPet")); // PATCH /pets
842        assert!(!tool_names.contains(&"healthCheck")); // HEAD /health
843        assert!(!tool_names.contains(&"healthOptions")); // OPTIONS /health
844    }
845
846    #[test]
847    fn test_method_filtering_uncommon_methods() {
848        use reqwest::Method;
849
850        let spec = create_test_spec_with_methods();
851        let filters = Some(
852            Filters::builder()
853                .methods(Filter::Include(vec![
854                    Method::HEAD,
855                    Method::OPTIONS,
856                    Method::PATCH,
857                ]))
858                .build(),
859        );
860        let tools = spec
861            .to_tool_metadata(filters.as_ref(), false, false)
862            .expect("Failed to generate tools");
863
864        // Only HEAD, OPTIONS, and PATCH operations should be included
865        assert_eq!(tools.len(), 3);
866
867        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
868        assert!(tool_names.contains(&"patchPet")); // PATCH /pets
869        assert!(tool_names.contains(&"healthCheck")); // HEAD /health
870        assert!(tool_names.contains(&"healthOptions")); // OPTIONS /health
871        assert!(!tool_names.contains(&"listUsers")); // GET /users
872        assert!(!tool_names.contains(&"createUser")); // POST /users
873        assert!(!tool_names.contains(&"updateUser")); // PUT /users
874        assert!(!tool_names.contains(&"deleteUser")); // DELETE /users
875        assert!(!tool_names.contains(&"listPets")); // GET /pets
876        assert!(!tool_names.contains(&"createPet")); // POST /pets
877    }
878
879    #[test]
880    fn test_method_and_tag_filtering_combined() {
881        use reqwest::Method;
882
883        let spec = create_test_spec_with_methods();
884        let filters = Some(
885            Filters::builder()
886                .tags(Filter::Include(vec!["user".to_string()]))
887                .methods(Filter::Include(vec![Method::GET, Method::POST]))
888                .build(),
889        );
890        let tools = spec
891            .to_tool_metadata(filters.as_ref(), false, false)
892            .expect("Failed to generate tools");
893
894        // Only user operations with GET and POST methods should be included
895        assert_eq!(tools.len(), 2);
896
897        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
898        assert!(tool_names.contains(&"listUsers")); // GET /users (has user tag)
899        assert!(tool_names.contains(&"createUser")); // POST /users (has user tag)
900        assert!(!tool_names.contains(&"updateUser")); // PUT /users (user tag but not GET/POST)
901        assert!(!tool_names.contains(&"deleteUser")); // DELETE /users (user tag but not GET/POST)
902        assert!(!tool_names.contains(&"listPets")); // GET /pets (GET method but not user tag)
903        assert!(!tool_names.contains(&"createPet")); // POST /pets (POST method but not user tag)
904        assert!(!tool_names.contains(&"patchPet")); // PATCH /pets (neither user tag nor GET/POST)
905        assert!(!tool_names.contains(&"healthCheck")); // HEAD /health (neither user tag nor GET/POST)
906        assert!(!tool_names.contains(&"healthOptions")); // OPTIONS /health (neither user tag nor GET/POST)
907    }
908
909    #[test]
910    fn test_method_filtering_no_matching_methods() {
911        use reqwest::Method;
912
913        let spec = create_test_spec_with_methods();
914        let filters = Some(
915            Filters::builder()
916                .methods(Filter::Include(vec![Method::TRACE]))
917                .build(),
918        );
919        let tools = spec
920            .to_tool_metadata(filters.as_ref(), false, false)
921            .expect("Failed to generate tools");
922
923        // No operations should be included
924        assert_eq!(tools.len(), 0);
925    }
926
927    #[test]
928    fn test_method_filtering_empty_filter_list() {
929        let spec = create_test_spec_with_methods();
930        let filters = Some(Filters::builder().methods(Filter::Include(vec![])).build());
931        let tools = spec
932            .to_tool_metadata(filters.as_ref(), false, false)
933            .expect("Failed to generate tools");
934
935        // Empty filter should exclude all operations
936        assert_eq!(tools.len(), 0);
937    }
938
939    #[test]
940    fn test_operations_include_filter_empty_filter_list() {
941        let spec = create_test_spec_with_methods();
942        let filters = Some(Filters::builder().methods(Filter::Include(vec![])).build());
943        let tools = spec
944            .to_tool_metadata(filters.as_ref(), false, false)
945            .expect("Failed to generate tools");
946
947        // Empty include filter should exclude all operations
948        assert_eq!(tools.len(), 0);
949    }
950
951    #[test]
952    fn test_operations_include_filter_two_operations_filter_list() {
953        let spec = create_test_spec_with_methods();
954        let filters = Some(
955            Filters::builder()
956                .operations_id(Filter::Include(vec![
957                    "listUsers".to_owned(),
958                    "patchPet".to_owned(),
959                ]))
960                .build(),
961        );
962        let tools = spec
963            .to_tool_metadata(filters.as_ref(), false, false)
964            .expect("Failed to generate tools");
965
966        assert_eq!(tools.len(), 2);
967
968        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
969        assert!(tool_names.contains(&"listUsers")); // GET /users (has user tag)
970        assert!(tool_names.contains(&"patchPet")); // POST /users (has user tag)
971    }
972
973    #[test]
974    fn test_operations_exclude_filter_empty_filter_list() {
975        let spec = create_test_spec_with_methods();
976        let filters = Some(
977            Filters::builder()
978                .operations_id(Filter::Exclude(vec![]))
979                .build(),
980        );
981        let tools = spec
982            .to_tool_metadata(filters.as_ref(), false, false)
983            .expect("Failed to generate tools");
984
985        // Empty include filter should exclude all operations
986        assert_eq!(tools.len(), 9);
987    }
988
989    #[test]
990    fn test_operations_exclude_filter_three_operations_filter_list() {
991        let spec = create_test_spec_with_methods();
992        let filters = Some(
993            Filters::builder()
994                .operations_id(Filter::Exclude(vec![
995                    "createUser".to_owned(),
996                    "deleteUser".to_owned(),
997                    "healthCheck".to_owned(),
998                ]))
999                .build(),
1000        );
1001        let tools = spec
1002            .to_tool_metadata(filters.as_ref(), false, false)
1003            .expect("Failed to generate tools");
1004
1005        assert_eq!(tools.len(), 6);
1006
1007        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
1008        assert!(tool_names.contains(&"listUsers"));
1009        assert!(tool_names.contains(&"updateUser"));
1010        assert!(tool_names.contains(&"listPets"));
1011        assert!(tool_names.contains(&"createPet"));
1012        assert!(tool_names.contains(&"patchPet"));
1013        assert!(tool_names.contains(&"healthOptions"))
1014    }
1015
1016    #[test]
1017    fn test_all_filters_combined_1() {
1018        let spec = create_test_spec_with_tags();
1019        let filters = Some(
1020            Filters::builder()
1021                .tags(Filter::Include(vec![
1022                    "pet".to_owned(),
1023                    "user".to_owned(),
1024                    "admin".to_owned(),
1025                ]))
1026                .methods(Filter::Include(vec![Method::GET, Method::POST]))
1027                .operations_id(Filter::Exclude(vec![
1028                    "listPets".to_owned(),
1029                    "createPet".to_owned(),
1030                    "publicEndpoint".to_owned(),
1031                ]))
1032                .build(),
1033        );
1034        let tools = spec
1035            .to_tool_metadata(filters.as_ref(), false, false)
1036            .expect("Failed to generate tools");
1037
1038        assert_eq!(tools.len(), 2);
1039
1040        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
1041
1042        assert!(tool_names.contains(&"listUsers"));
1043        assert!(tool_names.contains(&"adminPanel"));
1044    }
1045
1046    #[test]
1047    fn test_all_filters_combined_2() {
1048        let spec = create_test_spec_with_methods();
1049        let filters = Some(
1050            Filters::builder()
1051                .tags(Filter::Exclude(vec!["health".to_owned()]))
1052                .methods(Filter::Exclude(vec![Method::GET, Method::POST]))
1053                .operations_id(Filter::Include(vec![
1054                    "listUsers".to_owned(),
1055                    "updateUser".to_owned(),
1056                    "deleteUser".to_owned(),
1057                    "listPets".to_owned(),
1058                    "patchPet".to_owned(),
1059                    "healthCheck".to_owned(),
1060                    "healthOptions".to_owned(),
1061                ]))
1062                .build(),
1063        );
1064        let tools = spec
1065            .to_tool_metadata(filters.as_ref(), false, false)
1066            .expect("Failed to generate tools");
1067
1068        assert_eq!(tools.len(), 3);
1069
1070        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
1071
1072        assert!(tool_names.contains(&"updateUser"));
1073        assert!(tool_names.contains(&"deleteUser"));
1074        assert!(tool_names.contains(&"patchPet"));
1075    }
1076}