1use fastmcp_protocol::{Tool, ToolAnnotations};
10use serde_json::json;
11
12#[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#[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#[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#[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#[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#[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#[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#[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#[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 #[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 #[must_use]
362 pub fn description(mut self, desc: impl Into<String>) -> Self {
363 self.description = Some(desc.into());
364 self
365 }
366
367 #[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 #[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 #[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 #[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 #[must_use]
442 pub fn version(mut self, version: impl Into<String>) -> Self {
443 self.version = Some(version.into());
444 self
445 }
446
447 #[must_use]
449 pub fn tags(mut self, tags: Vec<String>) -> Self {
450 self.tags = tags;
451 self
452 }
453
454 #[must_use]
456 pub fn annotations(mut self, annotations: ToolAnnotations) -> Self {
457 self.annotations = Some(annotations);
458 self
459 }
460
461 #[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 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 #[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}