Skip to main content

fastmcp_rust/testing/fixtures/
tools.rs

1//! Sample tool definitions for testing.
2//!
3//! Provides pre-built tool fixtures with various characteristics:
4//! - Simple tools for basic testing
5//! - Complex tools for schema validation
6//! - Slow tools for timeout testing
7//! - Error-prone tools for error handling tests
8
9use fastmcp_protocol::{Tool, ToolAnnotations};
10use serde_json::json;
11
12/// Creates a simple greeting tool.
13///
14/// Takes a `name` parameter and returns a greeting message.
15///
16/// # Example
17///
18/// ```ignore
19/// use fastmcp_rust::testing::fixtures::tools::greeting_tool;
20///
21/// let tool = greeting_tool();
22/// assert_eq!(tool.name, "greeting");
23/// ```
24#[must_use]
25pub fn greeting_tool() -> Tool {
26    Tool {
27        name: "greeting".to_string(),
28        description: Some("Returns a greeting for the given name".to_string()),
29        input_schema: json!({
30            "type": "object",
31            "properties": {
32                "name": {
33                    "type": "string",
34                    "description": "The name to greet"
35                }
36            },
37            "required": ["name"]
38        }),
39        output_schema: Some(json!({
40            "type": "object",
41            "properties": {
42                "message": { "type": "string" }
43            }
44        })),
45        icon: None,
46        version: Some("1.0.0".to_string()),
47        tags: vec!["greeting".to_string(), "simple".to_string()],
48        annotations: Some(ToolAnnotations::new().read_only(true)),
49    }
50}
51
52/// Creates a calculator tool for arithmetic operations.
53///
54/// Takes `a`, `b`, and `operation` parameters.
55///
56/// # Example
57///
58/// ```ignore
59/// use fastmcp_rust::testing::fixtures::tools::calculator_tool;
60///
61/// let tool = calculator_tool();
62/// assert_eq!(tool.name, "calculator");
63/// ```
64#[must_use]
65pub fn calculator_tool() -> Tool {
66    Tool {
67        name: "calculator".to_string(),
68        description: Some("Performs basic arithmetic operations".to_string()),
69        input_schema: json!({
70            "type": "object",
71            "properties": {
72                "a": {
73                    "type": "number",
74                    "description": "First operand"
75                },
76                "b": {
77                    "type": "number",
78                    "description": "Second operand"
79                },
80                "operation": {
81                    "type": "string",
82                    "enum": ["add", "subtract", "multiply", "divide"],
83                    "description": "The operation to perform"
84                }
85            },
86            "required": ["a", "b", "operation"]
87        }),
88        output_schema: Some(json!({
89            "type": "object",
90            "properties": {
91                "result": { "type": "number" }
92            }
93        })),
94        icon: None,
95        version: Some("1.0.0".to_string()),
96        tags: vec!["math".to_string(), "calculation".to_string()],
97        annotations: Some(ToolAnnotations::new().read_only(true).idempotent(true)),
98    }
99}
100
101/// Creates a slow tool for timeout testing.
102///
103/// Takes a `delay_ms` parameter specifying how long to sleep.
104///
105/// # Example
106///
107/// ```ignore
108/// use fastmcp_rust::testing::fixtures::tools::slow_tool;
109///
110/// let tool = slow_tool();
111/// // Use in timeout tests
112/// ```
113#[must_use]
114pub fn slow_tool() -> Tool {
115    Tool {
116        name: "slow_operation".to_string(),
117        description: Some("A deliberately slow operation for timeout testing".to_string()),
118        input_schema: json!({
119            "type": "object",
120            "properties": {
121                "delay_ms": {
122                    "type": "integer",
123                    "minimum": 0,
124                    "maximum": 60000,
125                    "description": "How long to delay in milliseconds"
126                }
127            },
128            "required": ["delay_ms"]
129        }),
130        output_schema: Some(json!({
131            "type": "object",
132            "properties": {
133                "actual_delay_ms": { "type": "integer" }
134            }
135        })),
136        icon: None,
137        version: Some("1.0.0".to_string()),
138        tags: vec!["testing".to_string(), "timeout".to_string()],
139        annotations: Some(ToolAnnotations::new().read_only(true)),
140    }
141}
142
143/// Creates a file write tool for testing destructive operations.
144///
145/// # Example
146///
147/// ```ignore
148/// use fastmcp_rust::testing::fixtures::tools::file_write_tool;
149///
150/// let tool = file_write_tool();
151/// assert!(tool.annotations.as_ref().unwrap().destructive.unwrap());
152/// ```
153#[must_use]
154pub fn file_write_tool() -> Tool {
155    Tool {
156        name: "file_write".to_string(),
157        description: Some("Writes content to a file".to_string()),
158        input_schema: json!({
159            "type": "object",
160            "properties": {
161                "path": {
162                    "type": "string",
163                    "description": "File path to write to"
164                },
165                "content": {
166                    "type": "string",
167                    "description": "Content to write"
168                },
169                "append": {
170                    "type": "boolean",
171                    "default": false,
172                    "description": "Whether to append or overwrite"
173                }
174            },
175            "required": ["path", "content"]
176        }),
177        output_schema: Some(json!({
178            "type": "object",
179            "properties": {
180                "bytes_written": { "type": "integer" },
181                "path": { "type": "string" }
182            }
183        })),
184        icon: None,
185        version: Some("1.0.0".to_string()),
186        tags: vec!["file".to_string(), "io".to_string()],
187        annotations: Some(ToolAnnotations::new().destructive(true).idempotent(false)),
188    }
189}
190
191/// Creates a tool with complex nested schema.
192///
193/// Useful for testing schema validation edge cases.
194#[must_use]
195pub fn complex_schema_tool() -> Tool {
196    Tool {
197        name: "complex_operation".to_string(),
198        description: Some("A tool with complex nested input schema".to_string()),
199        input_schema: json!({
200            "type": "object",
201            "properties": {
202                "config": {
203                    "type": "object",
204                    "properties": {
205                        "name": { "type": "string" },
206                        "settings": {
207                            "type": "object",
208                            "properties": {
209                                "enabled": { "type": "boolean" },
210                                "threshold": { "type": "number", "minimum": 0, "maximum": 100 }
211                            },
212                            "required": ["enabled"]
213                        },
214                        "tags": {
215                            "type": "array",
216                            "items": { "type": "string" },
217                            "minItems": 1
218                        }
219                    },
220                    "required": ["name", "settings"]
221                },
222                "items": {
223                    "type": "array",
224                    "items": {
225                        "type": "object",
226                        "properties": {
227                            "id": { "type": "string" },
228                            "value": { "oneOf": [
229                                { "type": "string" },
230                                { "type": "number" },
231                                { "type": "boolean" }
232                            ]}
233                        },
234                        "required": ["id", "value"]
235                    }
236                }
237            },
238            "required": ["config"]
239        }),
240        output_schema: None,
241        icon: None,
242        version: Some("2.0.0".to_string()),
243        tags: vec!["complex".to_string(), "nested".to_string()],
244        annotations: None,
245    }
246}
247
248/// Creates a minimal tool with no optional fields.
249///
250/// Useful for testing minimum viable tool definitions.
251#[must_use]
252pub fn minimal_tool() -> Tool {
253    Tool {
254        name: "minimal".to_string(),
255        description: None,
256        input_schema: json!({ "type": "object" }),
257        output_schema: None,
258        icon: None,
259        version: None,
260        tags: vec![],
261        annotations: None,
262    }
263}
264
265/// Creates a tool that simulates errors.
266///
267/// The `error_type` parameter controls what kind of error to simulate.
268#[must_use]
269pub fn error_tool() -> Tool {
270    Tool {
271        name: "error_simulator".to_string(),
272        description: Some("Simulates various error conditions for testing".to_string()),
273        input_schema: json!({
274            "type": "object",
275            "properties": {
276                "error_type": {
277                    "type": "string",
278                    "enum": ["invalid_params", "internal", "timeout", "not_found"],
279                    "description": "Type of error to simulate"
280                },
281                "message": {
282                    "type": "string",
283                    "description": "Custom error message"
284                }
285            },
286            "required": ["error_type"]
287        }),
288        output_schema: None,
289        icon: None,
290        version: Some("1.0.0".to_string()),
291        tags: vec!["testing".to_string(), "error".to_string()],
292        annotations: None,
293    }
294}
295
296/// Returns a collection of all sample tools.
297///
298/// # Example
299///
300/// ```ignore
301/// use fastmcp_rust::testing::fixtures::tools::all_sample_tools;
302///
303/// let tools = all_sample_tools();
304/// assert!(tools.len() >= 5);
305/// ```
306#[must_use]
307pub fn all_sample_tools() -> Vec<Tool> {
308    vec![
309        greeting_tool(),
310        calculator_tool(),
311        slow_tool(),
312        file_write_tool(),
313        complex_schema_tool(),
314        minimal_tool(),
315        error_tool(),
316    ]
317}
318
319/// Builder for customizing tool fixtures.
320///
321/// # Example
322///
323/// ```ignore
324/// use fastmcp_rust::testing::fixtures::tools::ToolBuilder;
325///
326/// let tool = ToolBuilder::new("custom_tool")
327///     .description("A custom tool for testing")
328///     .with_string_param("input", "The input string", true)
329///     .with_number_param("count", "Number of times", false)
330///     .build();
331/// ```
332#[derive(Debug, Clone)]
333pub struct ToolBuilder {
334    name: String,
335    description: Option<String>,
336    properties: serde_json::Map<String, serde_json::Value>,
337    required: Vec<String>,
338    output_schema: Option<serde_json::Value>,
339    version: Option<String>,
340    tags: Vec<String>,
341    annotations: Option<ToolAnnotations>,
342}
343
344impl ToolBuilder {
345    /// Creates a new tool builder with the given name.
346    #[must_use]
347    pub fn new(name: impl Into<String>) -> Self {
348        Self {
349            name: name.into(),
350            description: None,
351            properties: serde_json::Map::new(),
352            required: Vec::new(),
353            output_schema: None,
354            version: None,
355            tags: Vec::new(),
356            annotations: None,
357        }
358    }
359
360    /// Sets the tool description.
361    #[must_use]
362    pub fn description(mut self, desc: impl Into<String>) -> Self {
363        self.description = Some(desc.into());
364        self
365    }
366
367    /// Adds a string parameter.
368    #[must_use]
369    pub fn with_string_param(
370        mut self,
371        name: impl Into<String>,
372        desc: impl Into<String>,
373        required: bool,
374    ) -> Self {
375        let name = name.into();
376        self.properties.insert(
377            name.clone(),
378            json!({
379                "type": "string",
380                "description": desc.into()
381            }),
382        );
383        if required {
384            self.required.push(name);
385        }
386        self
387    }
388
389    /// Adds a number parameter.
390    #[must_use]
391    pub fn with_number_param(
392        mut self,
393        name: impl Into<String>,
394        desc: impl Into<String>,
395        required: bool,
396    ) -> Self {
397        let name = name.into();
398        self.properties.insert(
399            name.clone(),
400            json!({
401                "type": "number",
402                "description": desc.into()
403            }),
404        );
405        if required {
406            self.required.push(name);
407        }
408        self
409    }
410
411    /// Adds a boolean parameter.
412    #[must_use]
413    pub fn with_bool_param(
414        mut self,
415        name: impl Into<String>,
416        desc: impl Into<String>,
417        required: bool,
418    ) -> Self {
419        let name = name.into();
420        self.properties.insert(
421            name.clone(),
422            json!({
423                "type": "boolean",
424                "description": desc.into()
425            }),
426        );
427        if required {
428            self.required.push(name);
429        }
430        self
431    }
432
433    /// Sets the output schema.
434    #[must_use]
435    pub fn output_schema(mut self, schema: serde_json::Value) -> Self {
436        self.output_schema = Some(schema);
437        self
438    }
439
440    /// Sets the version.
441    #[must_use]
442    pub fn version(mut self, version: impl Into<String>) -> Self {
443        self.version = Some(version.into());
444        self
445    }
446
447    /// Adds tags.
448    #[must_use]
449    pub fn tags(mut self, tags: Vec<String>) -> Self {
450        self.tags = tags;
451        self
452    }
453
454    /// Sets the annotations.
455    #[must_use]
456    pub fn annotations(mut self, annotations: ToolAnnotations) -> Self {
457        self.annotations = Some(annotations);
458        self
459    }
460
461    /// Builds the tool.
462    #[must_use]
463    pub fn build(self) -> Tool {
464        let input_schema = json!({
465            "type": "object",
466            "properties": self.properties,
467            "required": self.required
468        });
469
470        Tool {
471            name: self.name,
472            description: self.description,
473            input_schema,
474            output_schema: self.output_schema,
475            icon: None,
476            version: self.version,
477            tags: self.tags,
478            annotations: self.annotations,
479        }
480    }
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486
487    #[test]
488    fn test_greeting_tool() {
489        let tool = greeting_tool();
490        assert_eq!(tool.name, "greeting");
491        assert!(tool.description.is_some());
492        assert!(tool.input_schema.get("properties").is_some());
493    }
494
495    #[test]
496    fn test_calculator_tool() {
497        let tool = calculator_tool();
498        assert_eq!(tool.name, "calculator");
499        let props = tool.input_schema.get("properties").unwrap();
500        assert!(props.get("a").is_some());
501        assert!(props.get("b").is_some());
502        assert!(props.get("operation").is_some());
503    }
504
505    #[test]
506    fn test_slow_tool() {
507        let tool = slow_tool();
508        assert_eq!(tool.name, "slow_operation");
509        let props = tool.input_schema.get("properties").unwrap();
510        assert!(props.get("delay_ms").is_some());
511    }
512
513    #[test]
514    fn test_file_write_tool_annotations() {
515        let tool = file_write_tool();
516        let annotations = tool.annotations.as_ref().unwrap();
517        assert_eq!(annotations.destructive, Some(true));
518        assert_eq!(annotations.idempotent, Some(false));
519    }
520
521    #[test]
522    fn test_minimal_tool() {
523        let tool = minimal_tool();
524        assert_eq!(tool.name, "minimal");
525        assert!(tool.description.is_none());
526        assert!(tool.version.is_none());
527        assert!(tool.tags.is_empty());
528    }
529
530    #[test]
531    fn test_all_sample_tools() {
532        let tools = all_sample_tools();
533        assert!(tools.len() >= 5);
534
535        // Verify uniqueness of names
536        let names: Vec<_> = tools.iter().map(|t| &t.name).collect();
537        let unique: std::collections::HashSet<_> = names.iter().collect();
538        assert_eq!(names.len(), unique.len());
539    }
540
541    #[test]
542    fn test_tool_builder_basic() {
543        let tool = ToolBuilder::new("test_tool")
544            .description("A test tool")
545            .with_string_param("input", "The input", true)
546            .build();
547
548        assert_eq!(tool.name, "test_tool");
549        assert_eq!(tool.description, Some("A test tool".to_string()));
550    }
551
552    #[test]
553    fn test_tool_builder_with_all_param_types() {
554        let tool = ToolBuilder::new("multi_param")
555            .with_string_param("text", "Text input", true)
556            .with_number_param("count", "Count", false)
557            .with_bool_param("enabled", "Enable flag", false)
558            .build();
559
560        let props = tool.input_schema.get("properties").unwrap();
561        assert!(props.get("text").is_some());
562        assert!(props.get("count").is_some());
563        assert!(props.get("enabled").is_some());
564    }
565
566    #[test]
567    fn test_tool_builder_with_annotations() {
568        let tool = ToolBuilder::new("annotated")
569            .annotations(ToolAnnotations::new().read_only(true).idempotent(true))
570            .build();
571
572        let annotations = tool.annotations.as_ref().unwrap();
573        assert_eq!(annotations.read_only, Some(true));
574        assert_eq!(annotations.idempotent, Some(true));
575    }
576
577    // =========================================================================
578    // Additional coverage tests (bd-3w50)
579    // =========================================================================
580
581    #[test]
582    fn error_tool_fields() {
583        let tool = error_tool();
584        assert_eq!(tool.name, "error_simulator");
585        assert!(tool.description.is_some());
586        assert_eq!(tool.version, Some("1.0.0".to_string()));
587        assert!(tool.tags.contains(&"error".to_string()));
588        assert!(tool.annotations.is_none());
589
590        let props = tool.input_schema.get("properties").unwrap();
591        assert!(props.get("error_type").is_some());
592        assert!(props.get("message").is_some());
593    }
594
595    #[test]
596    fn complex_schema_tool_fields() {
597        let tool = complex_schema_tool();
598        assert_eq!(tool.name, "complex_operation");
599        assert_eq!(tool.version, Some("2.0.0".to_string()));
600        assert!(tool.output_schema.is_none());
601        assert!(tool.annotations.is_none());
602        assert!(tool.tags.contains(&"nested".to_string()));
603
604        let props = tool.input_schema.get("properties").unwrap();
605        assert!(props.get("config").is_some());
606        assert!(props.get("items").is_some());
607    }
608
609    #[test]
610    fn greeting_tool_annotations_read_only() {
611        let tool = greeting_tool();
612        let annotations = tool.annotations.as_ref().unwrap();
613        assert_eq!(annotations.read_only, Some(true));
614        assert!(tool.tags.contains(&"greeting".to_string()));
615        assert_eq!(tool.version, Some("1.0.0".to_string()));
616    }
617
618    #[test]
619    fn calculator_tool_annotations_idempotent() {
620        let tool = calculator_tool();
621        let annotations = tool.annotations.as_ref().unwrap();
622        assert_eq!(annotations.read_only, Some(true));
623        assert_eq!(annotations.idempotent, Some(true));
624        assert!(tool.tags.contains(&"math".to_string()));
625    }
626
627    #[test]
628    fn tool_builder_version_tags_output_schema() {
629        let tool = ToolBuilder::new("full")
630            .version("3.0.0")
631            .tags(vec!["a".to_string(), "b".to_string()])
632            .output_schema(json!({"type": "string"}))
633            .build();
634
635        assert_eq!(tool.version, Some("3.0.0".to_string()));
636        assert_eq!(tool.tags.len(), 2);
637        assert!(tool.output_schema.is_some());
638    }
639
640    #[test]
641    fn tool_builder_debug_and_clone() {
642        let builder = ToolBuilder::new("dbg")
643            .description("test")
644            .with_string_param("x", "desc", true);
645        let debug = format!("{builder:?}");
646        assert!(debug.contains("ToolBuilder"));
647        assert!(debug.contains("dbg"));
648
649        let cloned = builder.clone();
650        let tool = cloned.build();
651        assert_eq!(tool.name, "dbg");
652    }
653
654    #[test]
655    fn tool_builder_required_params_tracked() {
656        let tool = ToolBuilder::new("req")
657            .with_string_param("a", "desc-a", true)
658            .with_number_param("b", "desc-b", false)
659            .with_bool_param("c", "desc-c", true)
660            .build();
661
662        let required = tool.input_schema.get("required").unwrap();
663        let required_arr = required.as_array().unwrap();
664        assert_eq!(required_arr.len(), 2);
665        assert!(required_arr.contains(&json!("a")));
666        assert!(required_arr.contains(&json!("c")));
667    }
668
669    #[test]
670    fn all_sample_tools_count() {
671        let tools = all_sample_tools();
672        assert_eq!(tools.len(), 7);
673    }
674}