rmcp_openapi/
spec.rs

1use crate::error::Error;
2use crate::normalize_tag;
3use crate::tool::ToolMetadata;
4use crate::tool_generator::ToolGenerator;
5use oas3::Spec as Oas3Spec;
6use reqwest::Method;
7use serde_json::Value;
8
9/// OpenAPI specification wrapper that provides convenience methods
10/// for working with oas3::Spec
11#[derive(Debug, Clone)]
12pub struct Spec {
13    pub spec: Oas3Spec,
14}
15
16impl Spec {
17    /// Parse an OpenAPI specification from a JSON value
18    pub fn from_value(json_value: Value) -> Result<Self, Error> {
19        let spec: Oas3Spec = serde_json::from_value(json_value)?;
20        Ok(Spec { spec })
21    }
22
23    /// Convert all operations to MCP tool metadata
24    pub fn to_tool_metadata(
25        &self,
26        tag_filter: Option<&[String]>,
27        method_filter: Option<&[reqwest::Method]>,
28        skip_tool_descriptions: bool,
29        skip_parameter_descriptions: bool,
30    ) -> Result<Vec<ToolMetadata>, Error> {
31        let mut tools = Vec::new();
32
33        if let Some(paths) = &self.spec.paths {
34            for (path, path_item) in paths {
35                // Handle operations in the path item
36                let operations = [
37                    (Method::GET, &path_item.get),
38                    (Method::POST, &path_item.post),
39                    (Method::PUT, &path_item.put),
40                    (Method::DELETE, &path_item.delete),
41                    (Method::PATCH, &path_item.patch),
42                    (Method::HEAD, &path_item.head),
43                    (Method::OPTIONS, &path_item.options),
44                    (Method::TRACE, &path_item.trace),
45                ];
46
47                for (method, operation_ref) in operations {
48                    if let Some(operation) = operation_ref {
49                        // Filter by methods if specified
50                        if let Some(filter_methods) = method_filter
51                            && !filter_methods.contains(&method)
52                        {
53                            continue; // Skip this operation
54                        }
55
56                        // Filter by tags if specified (with kebab-case normalization)
57                        if let Some(filter_tags) = tag_filter {
58                            if !operation.tags.is_empty() {
59                                // Normalize both filter tags and operation tags before comparison
60                                let normalized_filter_tags: Vec<String> =
61                                    filter_tags.iter().map(|tag| normalize_tag(tag)).collect();
62
63                                let has_matching_tag = operation.tags.iter().any(|operation_tag| {
64                                    let normalized_operation_tag = normalize_tag(operation_tag);
65                                    normalized_filter_tags.contains(&normalized_operation_tag)
66                                });
67
68                                if !has_matching_tag {
69                                    continue; // Skip this operation
70                                }
71                            } else {
72                                continue; // Skip operations without tags when filtering
73                            }
74                        }
75
76                        let tool_metadata = ToolGenerator::generate_tool_metadata(
77                            operation,
78                            method.to_string(),
79                            path.clone(),
80                            &self.spec,
81                            skip_tool_descriptions,
82                            skip_parameter_descriptions,
83                        )?;
84                        tools.push(tool_metadata);
85                    }
86                }
87            }
88        }
89
90        Ok(tools)
91    }
92
93    /// Convert all operations to OpenApiTool instances with HTTP configuration
94    ///
95    /// # Errors
96    ///
97    /// Returns an error if any operations cannot be converted or OpenApiTool instances cannot be created
98    pub fn to_openapi_tools(
99        &self,
100        tag_filter: Option<&[String]>,
101        method_filter: Option<&[reqwest::Method]>,
102        base_url: Option<url::Url>,
103        default_headers: Option<reqwest::header::HeaderMap>,
104        skip_tool_descriptions: bool,
105        skip_parameter_descriptions: bool,
106    ) -> Result<Vec<crate::tool::Tool>, Error> {
107        // First generate the tool metadata using existing method
108        let tools_metadata = self.to_tool_metadata(
109            tag_filter,
110            method_filter,
111            skip_tool_descriptions,
112            skip_parameter_descriptions,
113        )?;
114
115        // Then convert to Tool instances
116        crate::tool_generator::ToolGenerator::generate_openapi_tools(
117            tools_metadata,
118            base_url,
119            default_headers,
120        )
121    }
122
123    /// Get operation by operation ID
124    pub fn get_operation(
125        &self,
126        operation_id: &str,
127    ) -> Option<(&oas3::spec::Operation, String, String)> {
128        if let Some(paths) = &self.spec.paths {
129            for (path, path_item) in paths {
130                let operations = [
131                    (Method::GET, &path_item.get),
132                    (Method::POST, &path_item.post),
133                    (Method::PUT, &path_item.put),
134                    (Method::DELETE, &path_item.delete),
135                    (Method::PATCH, &path_item.patch),
136                    (Method::HEAD, &path_item.head),
137                    (Method::OPTIONS, &path_item.options),
138                    (Method::TRACE, &path_item.trace),
139                ];
140
141                for (method, operation_ref) in operations {
142                    if let Some(operation) = operation_ref {
143                        let default_id = format!(
144                            "{}_{}",
145                            method,
146                            path.replace('/', "_").replace(['{', '}'], "")
147                        );
148                        let op_id = operation.operation_id.as_deref().unwrap_or(&default_id);
149
150                        if op_id == operation_id {
151                            return Some((operation, method.to_string(), path.clone()));
152                        }
153                    }
154                }
155            }
156        }
157        None
158    }
159
160    /// Get all operation IDs
161    pub fn get_operation_ids(&self) -> Vec<String> {
162        let mut operation_ids = Vec::new();
163
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                        operation_ids.push(op_id.to_string());
186                    }
187                }
188            }
189        }
190
191        operation_ids
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use serde_json::json;
199
200    fn create_test_spec_with_tags() -> Spec {
201        let spec_json = json!({
202            "openapi": "3.0.3",
203            "info": {
204                "title": "Test API",
205                "version": "1.0.0"
206            },
207            "paths": {
208                "/pets": {
209                    "get": {
210                        "operationId": "listPets",
211                        "tags": ["pet", "list"],
212                        "responses": {
213                            "200": {
214                                "description": "List of pets"
215                            }
216                        }
217                    },
218                    "post": {
219                        "operationId": "createPet",
220                        "tags": ["pet"],
221                        "responses": {
222                            "201": {
223                                "description": "Pet created"
224                            }
225                        }
226                    }
227                },
228                "/users": {
229                    "get": {
230                        "operationId": "listUsers",
231                        "tags": ["user"],
232                        "responses": {
233                            "200": {
234                                "description": "List of users"
235                            }
236                        }
237                    }
238                },
239                "/admin": {
240                    "get": {
241                        "operationId": "adminPanel",
242                        "tags": ["admin", "management"],
243                        "responses": {
244                            "200": {
245                                "description": "Admin panel"
246                            }
247                        }
248                    }
249                },
250                "/public": {
251                    "get": {
252                        "operationId": "publicEndpoint",
253                        "responses": {
254                            "200": {
255                                "description": "Public endpoint with no tags"
256                            }
257                        }
258                    }
259                }
260            }
261        });
262
263        Spec::from_value(spec_json).expect("Failed to create test spec")
264    }
265
266    fn create_test_spec_with_mixed_case_tags() -> Spec {
267        let spec_json = json!({
268            "openapi": "3.0.3",
269            "info": {
270                "title": "Test API with Mixed Case Tags",
271                "version": "1.0.0"
272            },
273            "paths": {
274                "/camel": {
275                    "get": {
276                        "operationId": "camelCaseOperation",
277                        "tags": ["userManagement"],
278                        "responses": {
279                            "200": {
280                                "description": "camelCase tag"
281                            }
282                        }
283                    }
284                },
285                "/pascal": {
286                    "get": {
287                        "operationId": "pascalCaseOperation",
288                        "tags": ["UserManagement"],
289                        "responses": {
290                            "200": {
291                                "description": "PascalCase tag"
292                            }
293                        }
294                    }
295                },
296                "/snake": {
297                    "get": {
298                        "operationId": "snakeCaseOperation",
299                        "tags": ["user_management"],
300                        "responses": {
301                            "200": {
302                                "description": "snake_case tag"
303                            }
304                        }
305                    }
306                },
307                "/screaming": {
308                    "get": {
309                        "operationId": "screamingCaseOperation",
310                        "tags": ["USER_MANAGEMENT"],
311                        "responses": {
312                            "200": {
313                                "description": "SCREAMING_SNAKE_CASE tag"
314                            }
315                        }
316                    }
317                },
318                "/kebab": {
319                    "get": {
320                        "operationId": "kebabCaseOperation",
321                        "tags": ["user-management"],
322                        "responses": {
323                            "200": {
324                                "description": "kebab-case tag"
325                            }
326                        }
327                    }
328                },
329                "/mixed": {
330                    "get": {
331                        "operationId": "mixedCaseOperation",
332                        "tags": ["XMLHttpRequest", "HTTPSConnection", "APIKey"],
333                        "responses": {
334                            "200": {
335                                "description": "Mixed case with acronyms"
336                            }
337                        }
338                    }
339                }
340            }
341        });
342
343        Spec::from_value(spec_json).expect("Failed to create test spec")
344    }
345
346    fn create_test_spec_with_methods() -> Spec {
347        let spec_json = json!({
348            "openapi": "3.0.3",
349            "info": {
350                "title": "Test API with Multiple Methods",
351                "version": "1.0.0"
352            },
353            "paths": {
354                "/users": {
355                    "get": {
356                        "operationId": "listUsers",
357                        "tags": ["user"],
358                        "responses": {
359                            "200": {
360                                "description": "List of users"
361                            }
362                        }
363                    },
364                    "post": {
365                        "operationId": "createUser",
366                        "tags": ["user"],
367                        "responses": {
368                            "201": {
369                                "description": "User created"
370                            }
371                        }
372                    },
373                    "put": {
374                        "operationId": "updateUser",
375                        "tags": ["user"],
376                        "responses": {
377                            "200": {
378                                "description": "User updated"
379                            }
380                        }
381                    },
382                    "delete": {
383                        "operationId": "deleteUser",
384                        "tags": ["user"],
385                        "responses": {
386                            "204": {
387                                "description": "User deleted"
388                            }
389                        }
390                    }
391                },
392                "/pets": {
393                    "get": {
394                        "operationId": "listPets",
395                        "tags": ["pet"],
396                        "responses": {
397                            "200": {
398                                "description": "List of pets"
399                            }
400                        }
401                    },
402                    "post": {
403                        "operationId": "createPet",
404                        "tags": ["pet"],
405                        "responses": {
406                            "201": {
407                                "description": "Pet created"
408                            }
409                        }
410                    },
411                    "patch": {
412                        "operationId": "patchPet",
413                        "tags": ["pet"],
414                        "responses": {
415                            "200": {
416                                "description": "Pet patched"
417                            }
418                        }
419                    }
420                },
421                "/health": {
422                    "head": {
423                        "operationId": "healthCheck",
424                        "tags": ["health"],
425                        "responses": {
426                            "200": {
427                                "description": "Health check"
428                            }
429                        }
430                    },
431                    "options": {
432                        "operationId": "healthOptions",
433                        "tags": ["health"],
434                        "responses": {
435                            "200": {
436                                "description": "Health options"
437                            }
438                        }
439                    }
440                }
441            }
442        });
443
444        Spec::from_value(spec_json).expect("Failed to create test spec")
445    }
446
447    #[test]
448    fn test_tag_filtering_no_filter() {
449        let spec = create_test_spec_with_tags();
450        let tools = spec
451            .to_tool_metadata(None, None, false, false)
452            .expect("Failed to generate tools");
453
454        // All operations should be included
455        assert_eq!(tools.len(), 5);
456
457        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
458        assert!(tool_names.contains(&"listPets"));
459        assert!(tool_names.contains(&"createPet"));
460        assert!(tool_names.contains(&"listUsers"));
461        assert!(tool_names.contains(&"adminPanel"));
462        assert!(tool_names.contains(&"publicEndpoint"));
463    }
464
465    #[test]
466    fn test_tag_filtering_single_tag() {
467        let spec = create_test_spec_with_tags();
468        let filter_tags = vec!["pet".to_string()];
469        let tools = spec
470            .to_tool_metadata(Some(&filter_tags), None, false, false)
471            .expect("Failed to generate tools");
472
473        // Only pet operations should be included
474        assert_eq!(tools.len(), 2);
475
476        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
477        assert!(tool_names.contains(&"listPets"));
478        assert!(tool_names.contains(&"createPet"));
479        assert!(!tool_names.contains(&"listUsers"));
480        assert!(!tool_names.contains(&"adminPanel"));
481        assert!(!tool_names.contains(&"publicEndpoint"));
482    }
483
484    #[test]
485    fn test_tag_filtering_multiple_tags() {
486        let spec = create_test_spec_with_tags();
487        let filter_tags = vec!["pet".to_string(), "user".to_string()];
488        let tools = spec
489            .to_tool_metadata(Some(&filter_tags), None, false, false)
490            .expect("Failed to generate tools");
491
492        // Pet and user operations should be included
493        assert_eq!(tools.len(), 3);
494
495        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
496        assert!(tool_names.contains(&"listPets"));
497        assert!(tool_names.contains(&"createPet"));
498        assert!(tool_names.contains(&"listUsers"));
499        assert!(!tool_names.contains(&"adminPanel"));
500        assert!(!tool_names.contains(&"publicEndpoint"));
501    }
502
503    #[test]
504    fn test_tag_filtering_or_logic() {
505        let spec = create_test_spec_with_tags();
506        let filter_tags = vec!["list".to_string()]; // listPets has both "pet" and "list" tags
507        let tools = spec
508            .to_tool_metadata(Some(&filter_tags), None, false, false)
509            .expect("Failed to generate tools");
510
511        // Only operations with "list" tag should be included
512        assert_eq!(tools.len(), 1);
513
514        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
515        assert!(tool_names.contains(&"listPets")); // Has both "pet" and "list" tags
516        assert!(!tool_names.contains(&"createPet")); // Only has "pet" tag
517    }
518
519    #[test]
520    fn test_tag_filtering_no_matching_tags() {
521        let spec = create_test_spec_with_tags();
522        let filter_tags = vec!["nonexistent".to_string()];
523        let tools = spec
524            .to_tool_metadata(Some(&filter_tags), None, false, false)
525            .expect("Failed to generate tools");
526
527        // No operations should be included
528        assert_eq!(tools.len(), 0);
529    }
530
531    #[test]
532    fn test_tag_filtering_excludes_operations_without_tags() {
533        let spec = create_test_spec_with_tags();
534        let filter_tags = vec!["admin".to_string()];
535        let tools = spec
536            .to_tool_metadata(Some(&filter_tags), None, false, false)
537            .expect("Failed to generate tools");
538
539        // Only admin operations should be included, public endpoint (no tags) should be excluded
540        assert_eq!(tools.len(), 1);
541
542        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
543        assert!(tool_names.contains(&"adminPanel"));
544        assert!(!tool_names.contains(&"publicEndpoint")); // No tags, should be excluded
545    }
546
547    #[test]
548    fn test_tag_normalization_all_cases_match() {
549        let spec = create_test_spec_with_mixed_case_tags();
550        let filter_tags = vec!["user-management".to_string()]; // kebab-case filter
551        let tools = spec
552            .to_tool_metadata(Some(&filter_tags), None, false, false)
553            .expect("Failed to generate tools");
554
555        // All userManagement variants should match user-management filter
556        assert_eq!(tools.len(), 5);
557
558        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
559        assert!(tool_names.contains(&"camelCaseOperation")); // userManagement
560        assert!(tool_names.contains(&"pascalCaseOperation")); // UserManagement
561        assert!(tool_names.contains(&"snakeCaseOperation")); // user_management
562        assert!(tool_names.contains(&"screamingCaseOperation")); // USER_MANAGEMENT
563        assert!(tool_names.contains(&"kebabCaseOperation")); // user-management
564        assert!(!tool_names.contains(&"mixedCaseOperation")); // Different tags
565    }
566
567    #[test]
568    fn test_tag_normalization_camel_case_filter() {
569        let spec = create_test_spec_with_mixed_case_tags();
570        let filter_tags = vec!["userManagement".to_string()]; // camelCase filter
571        let tools = spec
572            .to_tool_metadata(Some(&filter_tags), None, false, false)
573            .expect("Failed to generate tools");
574
575        // All userManagement variants should match camelCase filter
576        assert_eq!(tools.len(), 5);
577
578        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
579        assert!(tool_names.contains(&"camelCaseOperation"));
580        assert!(tool_names.contains(&"pascalCaseOperation"));
581        assert!(tool_names.contains(&"snakeCaseOperation"));
582        assert!(tool_names.contains(&"screamingCaseOperation"));
583        assert!(tool_names.contains(&"kebabCaseOperation"));
584    }
585
586    #[test]
587    fn test_tag_normalization_snake_case_filter() {
588        let spec = create_test_spec_with_mixed_case_tags();
589        let filter_tags = vec!["user_management".to_string()]; // snake_case filter
590        let tools = spec
591            .to_tool_metadata(Some(&filter_tags), None, false, false)
592            .expect("Failed to generate tools");
593
594        // All userManagement variants should match snake_case filter
595        assert_eq!(tools.len(), 5);
596    }
597
598    #[test]
599    fn test_tag_normalization_acronyms() {
600        let spec = create_test_spec_with_mixed_case_tags();
601        let filter_tags = vec!["xml-http-request".to_string()]; // kebab-case filter for acronym
602        let tools = spec
603            .to_tool_metadata(Some(&filter_tags), None, false, false)
604            .expect("Failed to generate tools");
605
606        // Should match XMLHttpRequest tag
607        assert_eq!(tools.len(), 1);
608
609        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
610        assert!(tool_names.contains(&"mixedCaseOperation"));
611    }
612
613    #[test]
614    fn test_tag_normalization_multiple_mixed_filters() {
615        let spec = create_test_spec_with_mixed_case_tags();
616        let filter_tags = vec![
617            "user-management".to_string(), // kebab-case
618            "HTTPSConnection".to_string(), // PascalCase with acronym
619        ];
620        let tools = spec
621            .to_tool_metadata(Some(&filter_tags), None, false, false)
622            .expect("Failed to generate tools");
623
624        // Should match all userManagement variants + mixedCaseOperation (for HTTPSConnection)
625        assert_eq!(tools.len(), 6);
626
627        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
628        assert!(tool_names.contains(&"camelCaseOperation"));
629        assert!(tool_names.contains(&"pascalCaseOperation"));
630        assert!(tool_names.contains(&"snakeCaseOperation"));
631        assert!(tool_names.contains(&"screamingCaseOperation"));
632        assert!(tool_names.contains(&"kebabCaseOperation"));
633        assert!(tool_names.contains(&"mixedCaseOperation"));
634    }
635
636    #[test]
637    fn test_tag_filtering_empty_filter_list() {
638        let spec = create_test_spec_with_tags();
639        let filter_tags: Vec<String> = vec![];
640        let tools = spec
641            .to_tool_metadata(Some(&filter_tags), None, false, false)
642            .expect("Failed to generate tools");
643
644        // Empty filter should exclude all operations
645        assert_eq!(tools.len(), 0);
646    }
647
648    #[test]
649    fn test_tag_filtering_complex_scenario() {
650        let spec = create_test_spec_with_tags();
651        let filter_tags = vec!["management".to_string(), "list".to_string()];
652        let tools = spec
653            .to_tool_metadata(Some(&filter_tags), None, false, false)
654            .expect("Failed to generate tools");
655
656        // Should include adminPanel (has "management") and listPets (has "list")
657        assert_eq!(tools.len(), 2);
658
659        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
660        assert!(tool_names.contains(&"adminPanel"));
661        assert!(tool_names.contains(&"listPets"));
662        assert!(!tool_names.contains(&"createPet"));
663        assert!(!tool_names.contains(&"listUsers"));
664        assert!(!tool_names.contains(&"publicEndpoint"));
665    }
666
667    #[test]
668    fn test_method_filtering_no_filter() {
669        let spec = create_test_spec_with_methods();
670        let tools = spec
671            .to_tool_metadata(None, None, false, false)
672            .expect("Failed to generate tools");
673
674        // All operations should be included (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)
675        assert_eq!(tools.len(), 9);
676
677        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
678        assert!(tool_names.contains(&"listUsers")); // GET /users
679        assert!(tool_names.contains(&"createUser")); // POST /users
680        assert!(tool_names.contains(&"updateUser")); // PUT /users
681        assert!(tool_names.contains(&"deleteUser")); // DELETE /users
682        assert!(tool_names.contains(&"listPets")); // GET /pets
683        assert!(tool_names.contains(&"createPet")); // POST /pets
684        assert!(tool_names.contains(&"patchPet")); // PATCH /pets
685        assert!(tool_names.contains(&"healthCheck")); // HEAD /health
686        assert!(tool_names.contains(&"healthOptions")); // OPTIONS /health
687    }
688
689    #[test]
690    fn test_method_filtering_single_method() {
691        use reqwest::Method;
692
693        let spec = create_test_spec_with_methods();
694        let filter_methods = vec![Method::GET];
695        let tools = spec
696            .to_tool_metadata(None, Some(&filter_methods), false, false)
697            .expect("Failed to generate tools");
698
699        // Only GET operations should be included
700        assert_eq!(tools.len(), 2);
701
702        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
703        assert!(tool_names.contains(&"listUsers")); // GET /users
704        assert!(tool_names.contains(&"listPets")); // GET /pets
705        assert!(!tool_names.contains(&"createUser")); // POST /users
706        assert!(!tool_names.contains(&"updateUser")); // PUT /users
707        assert!(!tool_names.contains(&"deleteUser")); // DELETE /users
708        assert!(!tool_names.contains(&"createPet")); // POST /pets
709        assert!(!tool_names.contains(&"patchPet")); // PATCH /pets
710        assert!(!tool_names.contains(&"healthCheck")); // HEAD /health
711        assert!(!tool_names.contains(&"healthOptions")); // OPTIONS /health
712    }
713
714    #[test]
715    fn test_method_filtering_multiple_methods() {
716        use reqwest::Method;
717
718        let spec = create_test_spec_with_methods();
719        let filter_methods = vec![Method::GET, Method::POST];
720        let tools = spec
721            .to_tool_metadata(None, Some(&filter_methods), false, false)
722            .expect("Failed to generate tools");
723
724        // Only GET and POST operations should be included
725        assert_eq!(tools.len(), 4);
726
727        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
728        assert!(tool_names.contains(&"listUsers")); // GET /users
729        assert!(tool_names.contains(&"createUser")); // POST /users
730        assert!(tool_names.contains(&"listPets")); // GET /pets
731        assert!(tool_names.contains(&"createPet")); // POST /pets
732        assert!(!tool_names.contains(&"updateUser")); // PUT /users
733        assert!(!tool_names.contains(&"deleteUser")); // DELETE /users
734        assert!(!tool_names.contains(&"patchPet")); // PATCH /pets
735        assert!(!tool_names.contains(&"healthCheck")); // HEAD /health
736        assert!(!tool_names.contains(&"healthOptions")); // OPTIONS /health
737    }
738
739    #[test]
740    fn test_method_filtering_uncommon_methods() {
741        use reqwest::Method;
742
743        let spec = create_test_spec_with_methods();
744        let filter_methods = vec![Method::HEAD, Method::OPTIONS, Method::PATCH];
745        let tools = spec
746            .to_tool_metadata(None, Some(&filter_methods), false, false)
747            .expect("Failed to generate tools");
748
749        // Only HEAD, OPTIONS, and PATCH operations should be included
750        assert_eq!(tools.len(), 3);
751
752        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
753        assert!(tool_names.contains(&"patchPet")); // PATCH /pets
754        assert!(tool_names.contains(&"healthCheck")); // HEAD /health
755        assert!(tool_names.contains(&"healthOptions")); // OPTIONS /health
756        assert!(!tool_names.contains(&"listUsers")); // GET /users
757        assert!(!tool_names.contains(&"createUser")); // POST /users
758        assert!(!tool_names.contains(&"updateUser")); // PUT /users
759        assert!(!tool_names.contains(&"deleteUser")); // DELETE /users
760        assert!(!tool_names.contains(&"listPets")); // GET /pets
761        assert!(!tool_names.contains(&"createPet")); // POST /pets
762    }
763
764    #[test]
765    fn test_method_and_tag_filtering_combined() {
766        use reqwest::Method;
767
768        let spec = create_test_spec_with_methods();
769        let filter_tags = vec!["user".to_string()];
770        let filter_methods = vec![Method::GET, Method::POST];
771        let tools = spec
772            .to_tool_metadata(Some(&filter_tags), Some(&filter_methods), false, false)
773            .expect("Failed to generate tools");
774
775        // Only user operations with GET and POST methods should be included
776        assert_eq!(tools.len(), 2);
777
778        let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
779        assert!(tool_names.contains(&"listUsers")); // GET /users (has user tag)
780        assert!(tool_names.contains(&"createUser")); // POST /users (has user tag)
781        assert!(!tool_names.contains(&"updateUser")); // PUT /users (user tag but not GET/POST)
782        assert!(!tool_names.contains(&"deleteUser")); // DELETE /users (user tag but not GET/POST)
783        assert!(!tool_names.contains(&"listPets")); // GET /pets (GET method but not user tag)
784        assert!(!tool_names.contains(&"createPet")); // POST /pets (POST method but not user tag)
785        assert!(!tool_names.contains(&"patchPet")); // PATCH /pets (neither user tag nor GET/POST)
786        assert!(!tool_names.contains(&"healthCheck")); // HEAD /health (neither user tag nor GET/POST)
787        assert!(!tool_names.contains(&"healthOptions")); // OPTIONS /health (neither user tag nor GET/POST)
788    }
789
790    #[test]
791    fn test_method_filtering_no_matching_methods() {
792        use reqwest::Method;
793
794        let spec = create_test_spec_with_methods();
795        let filter_methods = vec![Method::TRACE]; // No TRACE operations in the spec
796        let tools = spec
797            .to_tool_metadata(None, Some(&filter_methods), false, false)
798            .expect("Failed to generate tools");
799
800        // No operations should be included
801        assert_eq!(tools.len(), 0);
802    }
803
804    #[test]
805    fn test_method_filtering_empty_filter_list() {
806        let spec = create_test_spec_with_methods();
807        let filter_methods: Vec<reqwest::Method> = vec![];
808        let tools = spec
809            .to_tool_metadata(None, Some(&filter_methods), false, false)
810            .expect("Failed to generate tools");
811
812        // Empty filter should exclude all operations
813        assert_eq!(tools.len(), 0);
814    }
815}