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