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