1use async_trait::async_trait;
8use serde_json::Value;
9use std::collections::HashMap;
10use std::time::Instant;
11
12use crate::core::error::{McpError, McpResult};
13use crate::core::tool_metadata::{
14 CategoryFilter, ImprovedToolMetadata, ToolBehaviorHints, ToolCategory, ToolDeprecation,
15};
16use crate::core::validation::{ParameterValidator, ValidationConfig};
17use crate::protocol::types::{ContentBlock, ToolInfo, ToolInputSchema, ToolResult};
18
19#[async_trait]
21pub trait ToolHandler: Send + Sync {
22 async fn call(&self, arguments: HashMap<String, Value>) -> McpResult<ToolResult>;
30}
31
32pub struct Tool {
34 pub info: ToolInfo,
36 pub handler: Box<dyn ToolHandler>,
38 pub enabled: bool,
40 pub validator: Option<ParameterValidator>,
42 pub improved_metadata: ImprovedToolMetadata,
44}
45
46impl Tool {
47 pub fn new<H>(
55 name: String,
56 description: Option<String>,
57 input_schema: Value,
58 handler: H,
59 ) -> Self
60 where
61 H: ToolHandler + 'static,
62 {
63 let validator = if input_schema.is_object() {
65 Some(ParameterValidator::new(input_schema.clone()))
66 } else {
67 None
68 };
69
70 Self {
71 info: ToolInfo {
72 name,
73 description,
74 input_schema: ToolInputSchema {
75 schema_type: "object".to_string(),
76 properties: input_schema
77 .get("properties")
78 .and_then(|p| p.as_object())
79 .map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect()),
80 required: input_schema
81 .get("required")
82 .and_then(|r| r.as_array())
83 .map(|arr| {
84 arr.iter()
85 .filter_map(|v| v.as_str().map(String::from))
86 .collect()
87 }),
88 additional_properties: input_schema
89 .as_object()
90 .unwrap_or(&serde_json::Map::new())
91 .iter()
92 .filter(|(k, _)| !["type", "properties", "required"].contains(&k.as_str()))
93 .map(|(k, v)| (k.clone(), v.clone()))
94 .collect(),
95 },
96 output_schema: None,
97 annotations: None,
98 title: None,
99 meta: None,
100 },
101 handler: Box::new(handler),
102 enabled: true,
103 validator,
104 improved_metadata: ImprovedToolMetadata::new(),
105 }
106 }
107
108 pub fn with_validation<H>(
110 name: String,
111 description: Option<String>,
112 input_schema: Value,
113 handler: H,
114 validation_config: ValidationConfig,
115 ) -> Self
116 where
117 H: ToolHandler + 'static,
118 {
119 let mut tool = Self::new(name, description, input_schema.clone(), handler);
120 if input_schema.is_object() {
121 tool.validator = Some(ParameterValidator::with_config(
122 input_schema,
123 validation_config,
124 ));
125 }
126 tool
127 }
128
129 pub fn enable(&mut self) {
131 self.enabled = true;
132 }
133
134 pub fn disable(&mut self) {
136 self.enabled = false;
137 }
138
139 pub fn is_enabled(&self) -> bool {
141 self.enabled
142 }
143
144 pub async fn call(&self, mut arguments: HashMap<String, Value>) -> McpResult<ToolResult> {
152 if !self.enabled {
153 return Err(McpError::validation(format!(
154 "Tool '{}' is disabled",
155 self.info.name
156 )));
157 }
158
159 if let Some(warning) = self.improved_metadata.deprecation_warning() {
161 eprintln!("Warning: {warning}");
162 }
163
164 if let Some(ref validator) = self.validator {
166 validator.validate_and_coerce(&mut arguments).map_err(|e| {
167 McpError::validation(format!(
168 "Tool '{}' parameter validation failed: {}",
169 self.info.name, e
170 ))
171 })?;
172 }
173
174 let start_time = Instant::now();
176 let result = self.handler.call(arguments).await;
177 let execution_time = start_time.elapsed();
178
179 match &result {
181 Ok(_) => self.improved_metadata.record_success(execution_time),
182 Err(_) => self.improved_metadata.record_error(execution_time),
183 }
184
185 result
186 }
187
188 pub async fn call_unchecked(&self, arguments: HashMap<String, Value>) -> McpResult<ToolResult> {
190 if !self.enabled {
191 return Err(McpError::validation(format!(
192 "Tool '{}' is disabled",
193 self.info.name
194 )));
195 }
196
197 self.handler.call(arguments).await
198 }
199
200 pub fn validate_parameters(&self, arguments: &mut HashMap<String, Value>) -> McpResult<()> {
202 if let Some(ref validator) = self.validator {
203 validator.validate_and_coerce(arguments).map_err(|e| {
204 McpError::validation(format!(
205 "Tool '{}' parameter validation failed: {}",
206 self.info.name, e
207 ))
208 })
209 } else {
210 Ok(())
211 }
212 }
213
214 pub fn set_behavior_hints(&mut self, hints: ToolBehaviorHints) {
218 self.improved_metadata.behavior_hints = hints;
219 }
220
221 pub fn behavior_hints(&self) -> &ToolBehaviorHints {
223 &self.improved_metadata.behavior_hints
224 }
225
226 pub fn set_category(&mut self, category: ToolCategory) {
228 self.improved_metadata.category = Some(category);
229 }
230
231 pub fn category(&self) -> Option<&ToolCategory> {
233 self.improved_metadata.category.as_ref()
234 }
235
236 pub fn set_version(&mut self, version: String) {
238 self.improved_metadata.version = Some(version);
239 }
240
241 pub fn version(&self) -> Option<&String> {
243 self.improved_metadata.version.as_ref()
244 }
245
246 pub fn set_author(&mut self, author: String) {
248 self.improved_metadata.author = Some(author);
249 }
250
251 pub fn author(&self) -> Option<&String> {
253 self.improved_metadata.author.as_ref()
254 }
255
256 pub fn deprecate(&mut self, deprecation: ToolDeprecation) {
258 self.improved_metadata.deprecation = Some(deprecation);
259 }
260
261 pub fn is_deprecated(&self) -> bool {
263 self.improved_metadata.is_deprecated()
264 }
265
266 pub fn deprecation_warning(&self) -> Option<String> {
268 self.improved_metadata.deprecation_warning()
269 }
270
271 pub fn performance_metrics(&self) -> crate::core::tool_metadata::ToolPerformanceMetrics {
273 self.improved_metadata.get_performance_snapshot()
274 }
275
276 pub fn add_custom_metadata(&mut self, key: String, value: serde_json::Value) {
278 self.improved_metadata.custom.insert(key, value);
279 }
280
281 pub fn get_custom_metadata(&self, key: &str) -> Option<&serde_json::Value> {
283 self.improved_metadata.custom.get(key)
284 }
285
286 pub fn matches_category_filter(&self, filter: &CategoryFilter) -> bool {
288 if let Some(ref category) = self.improved_metadata.category {
289 category.matches_filter(filter)
290 } else {
291 filter.primary.is_none() && filter.secondary.is_none() && filter.tags.is_empty()
293 }
294 }
295
296 pub fn is_cacheable(&self) -> bool {
298 self.improved_metadata
299 .behavior_hints
300 .cacheable
301 .unwrap_or(false)
302 || (self
303 .improved_metadata
304 .behavior_hints
305 .read_only
306 .unwrap_or(false)
307 && self
308 .improved_metadata
309 .behavior_hints
310 .idempotent
311 .unwrap_or(false))
312 }
313
314 pub fn is_destructive(&self) -> bool {
316 self.improved_metadata
317 .behavior_hints
318 .destructive
319 .unwrap_or(false)
320 }
321
322 pub fn is_read_only(&self) -> bool {
324 self.improved_metadata
325 .behavior_hints
326 .read_only
327 .unwrap_or(false)
328 }
329
330 pub fn is_idempotent(&self) -> bool {
332 self.improved_metadata
333 .behavior_hints
334 .idempotent
335 .unwrap_or(false)
336 }
337
338 pub fn requires_auth(&self) -> bool {
340 self.improved_metadata
341 .behavior_hints
342 .requires_auth
343 .unwrap_or(false)
344 }
345}
346
347impl std::fmt::Debug for Tool {
348 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349 f.debug_struct("Tool")
350 .field("info", &self.info)
351 .field("enabled", &self.enabled)
352 .field("has_validator", &self.validator.is_some())
353 .field("deprecated", &self.is_deprecated())
354 .field("category", &self.improved_metadata.category)
355 .field("version", &self.improved_metadata.version)
356 .field("execution_count", &self.improved_metadata.execution_count())
357 .field("success_rate", &self.improved_metadata.success_rate())
358 .finish()
359 }
360}
361
362#[macro_export]
391macro_rules! tool {
392 ($name:expr_2021, $schema:expr_2021, $handler:expr_2021) => {
393 $crate::core::tool::Tool::new($name.to_string(), None, $schema, $handler)
394 };
395 ($name:expr_2021, $description:expr_2021, $schema:expr_2021, $handler:expr_2021) => {
396 $crate::core::tool::Tool::new(
397 $name.to_string(),
398 Some($description.to_string()),
399 $schema,
400 $handler,
401 )
402 };
403}
404
405pub struct EchoTool;
409
410#[async_trait]
411impl ToolHandler for EchoTool {
412 async fn call(&self, arguments: HashMap<String, Value>) -> McpResult<ToolResult> {
413 let message = arguments
414 .get("message")
415 .and_then(|v| v.as_str())
416 .unwrap_or("Hello, World!");
417
418 Ok(ToolResult {
419 content: vec![ContentBlock::Text {
420 text: message.to_string(),
421 annotations: None,
422 meta: None,
423 }],
424 is_error: None,
425 structured_content: None,
426 meta: None,
427 })
428 }
429}
430
431pub struct AdditionTool;
433
434#[async_trait]
435impl ToolHandler for AdditionTool {
436 async fn call(&self, arguments: HashMap<String, Value>) -> McpResult<ToolResult> {
437 let a = arguments
438 .get("a")
439 .and_then(|v| v.as_f64())
440 .ok_or_else(|| McpError::validation("Missing or invalid 'a' parameter"))?;
441
442 let b = arguments
443 .get("b")
444 .and_then(|v| v.as_f64())
445 .ok_or_else(|| McpError::validation("Missing or invalid 'b' parameter"))?;
446
447 let result = a + b;
448
449 Ok(ToolResult {
450 content: vec![ContentBlock::Text {
451 text: result.to_string(),
452 annotations: None,
453 meta: None,
454 }],
455 is_error: None,
456 structured_content: None,
457 meta: None,
458 })
459 }
460}
461
462pub struct TimestampTool;
464
465#[async_trait]
466impl ToolHandler for TimestampTool {
467 async fn call(&self, _arguments: HashMap<String, Value>) -> McpResult<ToolResult> {
468 use std::time::{SystemTime, UNIX_EPOCH};
469
470 let timestamp = SystemTime::now()
471 .duration_since(UNIX_EPOCH)
472 .map_err(|e| McpError::internal(e.to_string()))?
473 .as_secs();
474
475 Ok(ToolResult {
476 content: vec![ContentBlock::Text {
477 text: timestamp.to_string(),
478 annotations: None,
479 meta: None,
480 }],
481 is_error: None,
482 structured_content: None,
483 meta: None,
484 })
485 }
486}
487
488pub struct ToolBuilder {
490 name: String,
491 description: Option<String>,
492 input_schema: Option<Value>,
493 validation_config: Option<ValidationConfig>,
494 title: Option<String>,
495 behavior_hints: ToolBehaviorHints,
496 category: Option<ToolCategory>,
497 version: Option<String>,
498 author: Option<String>,
499 deprecation: Option<ToolDeprecation>,
500 custom_metadata: HashMap<String, serde_json::Value>,
501}
502
503impl ToolBuilder {
504 pub fn new<S: Into<String>>(name: S) -> Self {
506 Self {
507 name: name.into(),
508 description: None,
509 input_schema: None,
510 validation_config: None,
511 title: None,
512 behavior_hints: ToolBehaviorHints::new(),
513 category: None,
514 version: None,
515 author: None,
516 deprecation: None,
517 custom_metadata: HashMap::new(),
518 }
519 }
520
521 pub fn description<S: Into<String>>(mut self, description: S) -> Self {
523 self.description = Some(description.into());
524 self
525 }
526
527 pub fn title<S: Into<String>>(mut self, title: S) -> Self {
529 self.title = Some(title.into());
530 self
531 }
532
533 pub fn schema(mut self, schema: Value) -> Self {
535 self.input_schema = Some(schema);
536 self
537 }
538
539 pub fn validation_config(mut self, config: ValidationConfig) -> Self {
541 self.validation_config = Some(config);
542 self
543 }
544
545 pub fn strict_validation(mut self) -> Self {
547 self.validation_config = Some(ValidationConfig {
548 allow_additional: false,
549 coerce_types: false,
550 detailed_errors: true,
551 max_string_length: Some(1000),
552 max_array_length: Some(100),
553 max_object_properties: Some(50),
554 });
555 self
556 }
557
558 pub fn permissive_validation(mut self) -> Self {
560 self.validation_config = Some(ValidationConfig {
561 allow_additional: true,
562 coerce_types: true,
563 detailed_errors: false,
564 max_string_length: None,
565 max_array_length: None,
566 max_object_properties: None,
567 });
568 self
569 }
570
571 pub fn behavior_hints(mut self, hints: ToolBehaviorHints) -> Self {
575 self.behavior_hints = hints;
576 self
577 }
578
579 pub fn read_only(mut self) -> Self {
581 self.behavior_hints = self.behavior_hints.read_only();
582 self
583 }
584
585 pub fn destructive(mut self) -> Self {
587 self.behavior_hints = self.behavior_hints.destructive();
588 self
589 }
590
591 pub fn idempotent(mut self) -> Self {
593 self.behavior_hints = self.behavior_hints.idempotent();
594 self
595 }
596
597 pub fn requires_auth(mut self) -> Self {
599 self.behavior_hints = self.behavior_hints.requires_auth();
600 self
601 }
602
603 pub fn long_running(mut self) -> Self {
605 self.behavior_hints = self.behavior_hints.long_running();
606 self
607 }
608
609 pub fn resource_intensive(mut self) -> Self {
611 self.behavior_hints = self.behavior_hints.resource_intensive();
612 self
613 }
614
615 pub fn cacheable(mut self) -> Self {
617 self.behavior_hints = self.behavior_hints.cacheable();
618 self
619 }
620
621 pub fn category(mut self, category: ToolCategory) -> Self {
623 self.category = Some(category);
624 self
625 }
626
627 pub fn category_simple(mut self, primary: String, secondary: Option<String>) -> Self {
629 let mut cat = ToolCategory::new(primary);
630 if let Some(sec) = secondary {
631 cat = cat.with_secondary(sec);
632 }
633 self.category = Some(cat);
634 self
635 }
636
637 pub fn tag(mut self, tag: String) -> Self {
639 if let Some(ref mut category) = self.category {
640 category.tags.insert(tag);
641 } else {
642 let mut cat = ToolCategory::new("general".to_string());
643 cat.tags.insert(tag);
644 self.category = Some(cat);
645 }
646 self
647 }
648
649 pub fn version<S: Into<String>>(mut self, version: S) -> Self {
651 self.version = Some(version.into());
652 self
653 }
654
655 pub fn author<S: Into<String>>(mut self, author: S) -> Self {
657 self.author = Some(author.into());
658 self
659 }
660
661 pub fn deprecated(mut self, deprecation: ToolDeprecation) -> Self {
663 self.deprecation = Some(deprecation);
664 self
665 }
666
667 pub fn deprecated_simple<S: Into<String>>(mut self, reason: S) -> Self {
669 self.deprecation = Some(ToolDeprecation::new(reason.into()));
670 self
671 }
672
673 pub fn custom_metadata<S: Into<String>>(mut self, key: S, value: serde_json::Value) -> Self {
675 self.custom_metadata.insert(key.into(), value);
676 self
677 }
678
679 pub fn build<H>(self, handler: H) -> McpResult<Tool>
681 where
682 H: ToolHandler + 'static,
683 {
684 let schema = self.input_schema.unwrap_or_else(|| {
685 serde_json::json!({
686 "type": "object",
687 "properties": {},
688 "additionalProperties": true
689 })
690 });
691
692 let mut tool = if let Some(config) = self.validation_config {
693 Tool::with_validation(self.name, self.description, schema, handler, config)
694 } else {
695 Tool::new(self.name, self.description, schema, handler)
696 };
697
698 if let Some(title) = self.title {
700 tool.info.title = Some(title);
701 }
702
703 let mut improved_metadata =
705 ImprovedToolMetadata::new().with_behavior_hints(self.behavior_hints);
706
707 if let Some(category) = self.category {
708 improved_metadata = improved_metadata.with_category(category);
709 }
710
711 if let Some(version) = self.version {
712 improved_metadata = improved_metadata.with_version(version);
713 }
714
715 if let Some(author) = self.author {
716 improved_metadata = improved_metadata.with_author(author);
717 }
718
719 if let Some(deprecation) = self.deprecation {
720 improved_metadata = improved_metadata.deprecated(deprecation);
721 }
722
723 for (key, value) in self.custom_metadata {
725 improved_metadata = improved_metadata.with_custom_field(key, value);
726 }
727
728 tool.improved_metadata = improved_metadata;
729
730 Ok(tool)
731 }
732
733 pub fn build_with_validation_chain<H>(
735 self,
736 handler: H,
737 validation_fn: impl Fn(&mut HashMap<String, Value>) -> McpResult<()> + Send + Sync + 'static,
738 ) -> McpResult<ValidationChainTool>
739 where
740 H: ToolHandler + 'static,
741 {
742 let tool = self.build(handler)?;
743 Ok(ValidationChainTool {
744 tool,
745 custom_validator: Box::new(validation_fn),
746 })
747 }
748}
749
750type ValidationFunction = Box<dyn Fn(&mut HashMap<String, Value>) -> McpResult<()> + Send + Sync>;
753
754pub struct ValidationChainTool {
755 tool: Tool,
756 custom_validator: ValidationFunction,
757}
758
759#[async_trait]
760impl ToolHandler for ValidationChainTool {
761 async fn call(&self, mut arguments: HashMap<String, Value>) -> McpResult<ToolResult> {
762 (self.custom_validator)(&mut arguments)?;
764
765 self.tool.call(arguments).await
767 }
768}
769
770#[macro_export]
776macro_rules! validated_tool {
777 (
778 name: $name:expr_2021,
779 description: $desc:expr_2021,
780 parameters: {
781 $( $param_name:ident: $param_type:ident $( ( $( $constraint:ident: $value:expr_2021 ),* ) )? ),*
782 },
783 handler: $handler:expr_2021
784 ) => {{
785 use $crate::core::validation::{create_tool_schema, param_schema};
786
787 let params = vec![
788 $(
789 {
790 let base_schema = param_schema!($param_type stringify!($param_name));
791 $(
793 )?
796 base_schema
797 }
798 ),*
799 ];
800
801 let required = vec![ $( stringify!($param_name) ),* ];
802 let schema = create_tool_schema(params, required);
803
804 $crate::core::tool::Tool::new(
805 $name.to_string(),
806 Some($desc.to_string()),
807 schema,
808 $handler
809 )
810 }};
811}
812
813pub fn create_string_tool<H>(
815 name: &str,
816 description: &str,
817 param_name: &str,
818 param_description: &str,
819 handler: H,
820) -> Tool
821where
822 H: ToolHandler + 'static,
823{
824 use serde_json::json;
825
826 let schema = json!({
827 "type": "object",
828 "properties": {
829 param_name: {
830 "type": "string",
831 "description": param_description
832 }
833 },
834 "required": [param_name]
835 });
836
837 Tool::new(
838 name.to_string(),
839 Some(description.to_string()),
840 schema,
841 handler,
842 )
843}
844
845pub fn create_typed_tool<H>(
847 name: &str,
848 description: &str,
849 parameters: Vec<(&str, &str, Value)>, required: Vec<&str>,
851 handler: H,
852) -> Tool
853where
854 H: ToolHandler + 'static,
855{
856 use serde_json::{json, Map};
857
858 let mut properties = Map::new();
859 for (param_name, param_desc, param_schema) in parameters {
860 let mut schema_with_desc = param_schema;
861 if let Some(obj) = schema_with_desc.as_object_mut() {
862 obj.insert("description".to_string(), json!(param_desc));
863 }
864 properties.insert(param_name.to_string(), schema_with_desc);
865 }
866
867 let schema = json!({
868 "type": "object",
869 "properties": properties,
870 "required": required
871 });
872
873 Tool::new(
874 name.to_string(),
875 Some(description.to_string()),
876 schema,
877 handler,
878 )
879}
880
881pub trait ValidatedToolHandler: ToolHandler {
883 fn parameter_schema() -> Value;
885
886 fn validation_config() -> ValidationConfig {
888 ValidationConfig::default()
889 }
890
891 fn create_tool(name: String, description: Option<String>, handler: Self) -> Tool
893 where
894 Self: Sized + 'static,
895 {
896 Tool::with_validation(
897 name,
898 description,
899 Self::parameter_schema(),
900 handler,
901 Self::validation_config(),
902 )
903 }
904}
905
906pub struct CalculatorTool;
912
913#[async_trait]
914impl ToolHandler for CalculatorTool {
915 async fn call(&self, arguments: HashMap<String, Value>) -> McpResult<ToolResult> {
916 let operation = arguments
917 .get("operation")
918 .and_then(|v| v.as_str())
919 .ok_or_else(|| McpError::validation("Missing 'operation' parameter"))?;
920
921 let a = arguments
922 .get("a")
923 .and_then(|v| v.as_f64())
924 .ok_or_else(|| McpError::validation("Missing or invalid 'a' parameter"))?;
925
926 let b = arguments
927 .get("b")
928 .and_then(|v| v.as_f64())
929 .ok_or_else(|| McpError::validation("Missing or invalid 'b' parameter"))?;
930
931 let result = match operation {
932 "add" => a + b,
933 "subtract" => a - b,
934 "multiply" => a * b,
935 "divide" => {
936 if b == 0.0 {
937 return Ok(ToolResult {
938 content: vec![ContentBlock::Text {
939 text: "Error: Division by zero".to_string(),
940 annotations: None,
941 meta: None,
942 }],
943 is_error: Some(true),
944 structured_content: Some(serde_json::json!({
945 "error": "division_by_zero",
946 "message": "Cannot divide by zero"
947 })),
948 meta: None,
949 });
950 }
951 a / b
952 }
953 _ => {
954 return Err(McpError::validation(format!(
955 "Unsupported operation: {operation}"
956 )));
957 }
958 };
959
960 Ok(ToolResult {
961 content: vec![ContentBlock::Text {
962 text: result.to_string(),
963 annotations: None,
964 meta: None,
965 }],
966 is_error: None,
967 structured_content: Some(serde_json::json!({
968 "operation": operation,
969 "operands": [a, b],
970 "result": result
971 })),
972 meta: None,
973 })
974 }
975}
976
977impl ValidatedToolHandler for CalculatorTool {
978 fn parameter_schema() -> Value {
979 use crate::core::validation::create_tool_schema;
980 use crate::param_schema;
981
982 create_tool_schema(
983 vec![
984 param_schema!(enum "operation", values: ["add", "subtract", "multiply", "divide"]),
985 param_schema!(number "a", min: -1000000, max: 1000000),
986 param_schema!(number "b", min: -1000000, max: 1000000),
987 ],
988 vec!["operation", "a", "b"],
989 )
990 }
991
992 fn validation_config() -> ValidationConfig {
993 ValidationConfig {
994 allow_additional: false,
995 coerce_types: true,
996 detailed_errors: true,
997 max_string_length: Some(20),
998 max_array_length: Some(10),
999 max_object_properties: Some(10),
1000 }
1001 }
1002}
1003
1004pub struct TextProcessorTool;
1006
1007#[async_trait]
1008impl ToolHandler for TextProcessorTool {
1009 async fn call(&self, arguments: HashMap<String, Value>) -> McpResult<ToolResult> {
1010 let text = arguments
1011 .get("text")
1012 .and_then(|v| v.as_str())
1013 .ok_or_else(|| McpError::validation("Missing 'text' parameter"))?;
1014
1015 let operation = arguments
1016 .get("operation")
1017 .and_then(|v| v.as_str())
1018 .unwrap_or("uppercase");
1019
1020 let result = match operation {
1021 "uppercase" => text.to_uppercase(),
1022 "lowercase" => text.to_lowercase(),
1023 "reverse" => text.chars().rev().collect(),
1024 "word_count" => text.split_whitespace().count().to_string(),
1025 "char_count" => text.len().to_string(),
1026 _ => {
1027 return Err(McpError::validation(format!(
1028 "Unsupported operation: {operation}"
1029 )));
1030 }
1031 };
1032
1033 Ok(ToolResult {
1034 content: vec![ContentBlock::Text {
1035 text: result.clone(),
1036 annotations: None,
1037 meta: None,
1038 }],
1039 is_error: None,
1040 structured_content: Some(serde_json::json!({
1041 "original_text": text,
1042 "operation": operation,
1043 "result": result,
1044 "length": text.len()
1045 })),
1046 meta: None,
1047 })
1048 }
1049}
1050
1051impl ValidatedToolHandler for TextProcessorTool {
1052 fn parameter_schema() -> Value {
1053 use crate::core::validation::create_tool_schema;
1054 use crate::param_schema;
1055
1056 create_tool_schema(
1057 vec![
1058 param_schema!(string "text", min: 1, max: 10000),
1059 param_schema!(enum "operation", values: ["uppercase", "lowercase", "reverse", "word_count", "char_count"]),
1060 ],
1061 vec!["text"],
1062 )
1063 }
1064}
1065
1066#[cfg(test)]
1067mod tests {
1068 use super::*;
1069 use crate::Content;
1070 use serde_json::json;
1071
1072 #[tokio::test]
1073 async fn test_echo_tool() {
1074 let tool = EchoTool;
1075 let mut args = HashMap::new();
1076 args.insert("message".to_string(), json!("test message"));
1077
1078 let result = tool.call(args).await.unwrap();
1079 match &result.content[0] {
1080 Content::Text { text, .. } => assert_eq!(text, "test message"),
1081 _ => panic!("Expected text content"),
1082 }
1083 }
1084
1085 #[tokio::test]
1086 async fn test_addition_tool() {
1087 let tool = AdditionTool;
1088 let mut args = HashMap::new();
1089 args.insert("a".to_string(), json!(5.0));
1090 args.insert("b".to_string(), json!(3.0));
1091
1092 let result = tool.call(args).await.unwrap();
1093 match &result.content[0] {
1094 Content::Text { text, .. } => assert_eq!(text, "8"),
1095 _ => panic!("Expected text content"),
1096 }
1097 }
1098
1099 #[test]
1100 fn test_tool_creation() {
1101 let tool = Tool::new(
1102 "test_tool".to_string(),
1103 Some("Test tool".to_string()),
1104 json!({"type": "object"}),
1105 EchoTool,
1106 );
1107
1108 assert_eq!(tool.info.name, "test_tool");
1109 assert_eq!(tool.info.description, Some("Test tool".to_string()));
1110 assert!(tool.is_enabled());
1111 }
1112
1113 #[test]
1114 fn test_tool_enable_disable() {
1115 let mut tool = Tool::new(
1116 "test_tool".to_string(),
1117 None,
1118 json!({"type": "object"}),
1119 EchoTool,
1120 );
1121
1122 assert!(tool.is_enabled());
1123
1124 tool.disable();
1125 assert!(!tool.is_enabled());
1126
1127 tool.enable();
1128 assert!(tool.is_enabled());
1129 }
1130
1131 #[tokio::test]
1132 async fn test_disabled_tool() {
1133 let mut tool = Tool::new(
1134 "test_tool".to_string(),
1135 None,
1136 json!({"type": "object"}),
1137 EchoTool,
1138 );
1139
1140 tool.disable();
1141
1142 let result = tool.call(HashMap::new()).await;
1143 assert!(result.is_err());
1144 match result.unwrap_err() {
1145 McpError::Validation(msg) => assert!(msg.contains("disabled")),
1146 _ => panic!("Expected validation error"),
1147 }
1148 }
1149
1150 #[test]
1151 fn test_tool_builder() {
1152 let tool = ToolBuilder::new("test")
1153 .description("A test tool")
1154 .schema(json!({"type": "object", "properties": {"x": {"type": "number"}}}))
1155 .build(EchoTool)
1156 .unwrap();
1157
1158 assert_eq!(tool.info.name, "test");
1159 assert_eq!(tool.info.description, Some("A test tool".to_string()));
1160 assert!(tool.validator.is_some());
1161 }
1162
1163 #[test]
1164 fn test_improved_tool_builder() {
1165 let tool = ToolBuilder::new("improved_test")
1166 .title("improved Test Tool")
1167 .description("A test tool with improved features")
1168 .strict_validation()
1169 .schema(json!({
1170 "type": "object",
1171 "properties": {
1172 "name": {"type": "string", "minLength": 2},
1173 "age": {"type": "integer", "minimum": 0}
1174 },
1175 "required": ["name"]
1176 }))
1177 .build(EchoTool)
1178 .unwrap();
1179
1180 assert_eq!(tool.info.name, "improved_test");
1181 assert_eq!(tool.info.title, Some("improved Test Tool".to_string()));
1182 assert!(tool.validator.is_some());
1183 }
1184
1185 #[tokio::test]
1186 async fn test_parameter_validation() {
1187 let schema = json!({
1188 "type": "object",
1189 "properties": {
1190 "name": {"type": "string", "minLength": 2},
1191 "age": {"type": "integer", "minimum": 0, "maximum": 150}
1192 },
1193 "required": ["name", "age"]
1194 });
1195
1196 let tool = Tool::new(
1197 "validation_test".to_string(),
1198 Some("Test validation".to_string()),
1199 schema,
1200 EchoTool,
1201 );
1202
1203 let mut valid_args = HashMap::new();
1205 valid_args.insert("name".to_string(), json!("Alice"));
1206 valid_args.insert("age".to_string(), json!(25));
1207 assert!(tool.validate_parameters(&mut valid_args).is_ok());
1208
1209 let mut invalid_args = HashMap::new();
1211 invalid_args.insert("name".to_string(), json!("Bob"));
1212 assert!(tool.validate_parameters(&mut invalid_args).is_err());
1213
1214 let mut coercible_args = HashMap::new();
1216 coercible_args.insert("name".to_string(), json!("Charlie"));
1217 coercible_args.insert("age".to_string(), json!("30")); assert!(tool.validate_parameters(&mut coercible_args).is_ok());
1219 assert_eq!(coercible_args.get("age").unwrap().as_i64(), Some(30));
1221 }
1222
1223 #[tokio::test]
1224 async fn test_calculator_tool() {
1225 let tool = CalculatorTool::create_tool(
1226 "calculator".to_string(),
1227 Some("complete calculator".to_string()),
1228 CalculatorTool,
1229 );
1230
1231 let mut args = HashMap::new();
1233 args.insert("operation".to_string(), json!("add"));
1234 args.insert("a".to_string(), json!(5));
1235 args.insert("b".to_string(), json!(3));
1236
1237 let result = tool.call(args).await.unwrap();
1238 assert_eq!(
1239 result.content[0],
1240 ContentBlock::Text {
1241 text: "8".to_string(),
1242 annotations: None,
1243 meta: None,
1244 }
1245 );
1246 assert!(result.structured_content.is_some());
1247
1248 let mut args = HashMap::new();
1250 args.insert("operation".to_string(), json!("divide"));
1251 args.insert("a".to_string(), json!(10));
1252 args.insert("b".to_string(), json!(0));
1253
1254 let result = tool.call(args).await.unwrap();
1255 assert_eq!(result.is_error, Some(true));
1256 if let ContentBlock::Text { text, .. } = &result.content[0] {
1257 assert!(text.contains("Division by zero"));
1258 } else {
1259 panic!("Expected text content");
1260 }
1261 }
1262
1263 #[tokio::test]
1264 async fn test_text_processor_tool() {
1265 let tool = TextProcessorTool::create_tool(
1266 "text_processor".to_string(),
1267 Some("Text processing utility".to_string()),
1268 TextProcessorTool,
1269 );
1270
1271 let mut args = HashMap::new();
1273 args.insert("text".to_string(), json!("hello world"));
1274 args.insert("operation".to_string(), json!("uppercase"));
1275
1276 let result = tool.call(args.clone()).await.unwrap();
1277 assert_eq!(
1278 result.content[0],
1279 ContentBlock::Text {
1280 text: "HELLO WORLD".to_string(),
1281 annotations: None,
1282 meta: None,
1283 }
1284 );
1285
1286 args.insert("operation".to_string(), json!("word_count"));
1288 let result = tool.call(args).await.unwrap();
1289 assert_eq!(
1290 result.content[0],
1291 ContentBlock::Text {
1292 text: "2".to_string(),
1293 annotations: None,
1294 meta: None,
1295 }
1296 );
1297 }
1298
1299 #[test]
1300 fn test_create_typed_tool() {
1301 let tool = create_typed_tool(
1302 "typed_test",
1303 "A typed parameter test tool",
1304 vec![
1305 (
1306 "username",
1307 "User's name",
1308 json!({"type": "string", "minLength": 3}),
1309 ),
1310 (
1311 "age",
1312 "User's age",
1313 json!({"type": "integer", "minimum": 0}),
1314 ),
1315 (
1316 "active",
1317 "Whether user is active",
1318 json!({"type": "boolean"}),
1319 ),
1320 ],
1321 vec!["username", "age"],
1322 EchoTool,
1323 );
1324
1325 assert_eq!(tool.info.name, "typed_test");
1326 assert!(tool.validator.is_some());
1327
1328 let schema = &tool.info.input_schema;
1330 assert!(schema.properties.is_some());
1331 let props = schema.properties.as_ref().unwrap();
1332 assert!(props.contains_key("username"));
1333 assert!(props.contains_key("age"));
1334 assert!(props.contains_key("active"));
1335 }
1336
1337 #[test]
1338 fn test_validation_config_options() {
1339 let strict_tool = ToolBuilder::new("strict")
1341 .strict_validation()
1342 .build(EchoTool)
1343 .unwrap();
1344 assert!(strict_tool.validator.is_some());
1345
1346 let permissive_tool = ToolBuilder::new("permissive")
1348 .permissive_validation()
1349 .build(EchoTool)
1350 .unwrap();
1351 assert!(permissive_tool.validator.is_some());
1352 }
1353}
1354
1355pub trait ParameterExt {
1361 fn get_string(&self, key: &str) -> McpResult<&str>;
1363
1364 fn get_optional_string(&self, key: &str) -> Option<&str>;
1366
1367 fn get_number(&self, key: &str) -> McpResult<f64>;
1369
1370 fn get_optional_number(&self, key: &str) -> Option<f64>;
1372
1373 fn get_integer(&self, key: &str) -> McpResult<i64>;
1375
1376 fn get_optional_integer(&self, key: &str) -> Option<i64>;
1378
1379 fn get_boolean(&self, key: &str) -> McpResult<bool>;
1381
1382 fn get_optional_boolean(&self, key: &str) -> Option<bool>;
1384}
1385
1386impl ParameterExt for HashMap<String, Value> {
1387 fn get_string(&self, key: &str) -> McpResult<&str> {
1388 self.get(key).and_then(|v| v.as_str()).ok_or_else(|| {
1389 McpError::validation(format!("Missing or invalid string parameter: {key}"))
1390 })
1391 }
1392
1393 fn get_optional_string(&self, key: &str) -> Option<&str> {
1394 self.get(key).and_then(|v| v.as_str())
1395 }
1396
1397 fn get_number(&self, key: &str) -> McpResult<f64> {
1398 self.get(key).and_then(|v| v.as_f64()).ok_or_else(|| {
1399 McpError::validation(format!("Missing or invalid number parameter: {key}"))
1400 })
1401 }
1402
1403 fn get_optional_number(&self, key: &str) -> Option<f64> {
1404 self.get(key).and_then(|v| v.as_f64())
1405 }
1406
1407 fn get_integer(&self, key: &str) -> McpResult<i64> {
1408 self.get(key).and_then(|v| v.as_i64()).ok_or_else(|| {
1409 McpError::validation(format!("Missing or invalid integer parameter: {key}"))
1410 })
1411 }
1412
1413 fn get_optional_integer(&self, key: &str) -> Option<i64> {
1414 self.get(key).and_then(|v| v.as_i64())
1415 }
1416
1417 fn get_boolean(&self, key: &str) -> McpResult<bool> {
1418 self.get(key).and_then(|v| v.as_bool()).ok_or_else(|| {
1419 McpError::validation(format!("Missing or invalid boolean parameter: {key}"))
1420 })
1421 }
1422
1423 fn get_optional_boolean(&self, key: &str) -> Option<bool> {
1424 self.get(key).and_then(|v| v.as_bool())
1425 }
1426}
1427
1428#[cfg(test)]
1429mod improved_tests {
1430 use super::*;
1431 use crate::core::tool_metadata::*;
1432 use crate::prelude::ToolHandler;
1433 use std::time::Duration;
1434 use tokio;
1435
1436 struct TestHandler {
1438 result: String,
1439 should_fail: bool,
1440 }
1441
1442 #[async_trait]
1443 impl ToolHandler for TestHandler {
1444 async fn call(&self, _arguments: HashMap<String, Value>) -> McpResult<ToolResult> {
1445 if self.should_fail {
1446 Err(McpError::validation("Test error".to_string()))
1447 } else {
1448 Ok(ToolResult {
1449 content: vec![ContentBlock::Text {
1450 text: self.result.clone(),
1451 annotations: None,
1452 meta: None,
1453 }],
1454 is_error: None,
1455 structured_content: None,
1456 meta: None,
1457 })
1458 }
1459 }
1460 }
1461
1462 #[tokio::test]
1463 async fn test_improved_tool_builder() {
1464 let handler = TestHandler {
1465 result: "test result".to_string(),
1466 should_fail: false,
1467 };
1468
1469 let tool = ToolBuilder::new("test_tool")
1470 .description("A test tool")
1471 .title("Test Tool")
1472 .version("1.0.0")
1473 .author("Test Author")
1474 .read_only()
1475 .idempotent()
1476 .cacheable()
1477 .category_simple("data".to_string(), Some("analysis".to_string()))
1478 .tag("testing".to_string())
1479 .tag("utility".to_string())
1480 .custom_metadata("priority".to_string(), serde_json::Value::from("high"))
1481 .build(handler)
1482 .expect("Failed to build tool");
1483
1484 assert_eq!(tool.info.name, "test_tool");
1485 assert_eq!(tool.info.description, Some("A test tool".to_string()));
1486 assert_eq!(tool.info.title, Some("Test Tool".to_string()));
1487 assert_eq!(tool.version(), Some(&"1.0.0".to_string()));
1488 assert_eq!(tool.author(), Some(&"Test Author".to_string()));
1489 assert!(tool.is_read_only());
1490 assert!(tool.is_idempotent());
1491 assert!(tool.is_cacheable());
1492 assert!(!tool.is_destructive());
1493 assert!(!tool.requires_auth());
1494
1495 let category = tool.category().unwrap();
1496 assert_eq!(category.primary, "data");
1497 assert_eq!(category.secondary, Some("analysis".to_string()));
1498 assert!(category.tags.contains("testing"));
1499 assert!(category.tags.contains("utility"));
1500
1501 let custom_priority = tool.get_custom_metadata("priority");
1502 assert_eq!(custom_priority, Some(&serde_json::Value::from("high")));
1503 }
1504
1505 #[tokio::test]
1506 async fn test_performance_tracking() {
1507 let handler = TestHandler {
1508 result: "success".to_string(),
1509 should_fail: false,
1510 };
1511
1512 let tool = ToolBuilder::new("performance_test")
1513 .build(handler)
1514 .expect("Failed to build tool");
1515
1516 let metrics = tool.performance_metrics();
1518 assert_eq!(metrics.execution_count, 0);
1519 assert_eq!(metrics.success_count, 0);
1520 assert_eq!(metrics.error_count, 0);
1521
1522 let result = tool.call(HashMap::new()).await;
1524 assert!(result.is_ok());
1525
1526 let metrics = tool.performance_metrics();
1528 assert_eq!(metrics.execution_count, 1);
1529 assert_eq!(metrics.success_count, 1);
1530 assert_eq!(metrics.error_count, 0);
1531 assert_eq!(metrics.success_rate, 100.0);
1532 assert!(metrics.average_execution_time > Duration::from_nanos(0));
1533 }
1534
1535 #[tokio::test]
1536 async fn test_performance_tracking_with_errors() {
1537 let handler = TestHandler {
1538 result: "".to_string(),
1539 should_fail: true,
1540 };
1541
1542 let tool = ToolBuilder::new("error_test")
1543 .build(handler)
1544 .expect("Failed to build tool");
1545
1546 let result = tool.call(HashMap::new()).await;
1548 assert!(result.is_err());
1549
1550 let metrics = tool.performance_metrics();
1552 assert_eq!(metrics.execution_count, 1);
1553 assert_eq!(metrics.success_count, 0);
1554 assert_eq!(metrics.error_count, 1);
1555 assert_eq!(metrics.success_rate, 0.0);
1556 }
1557
1558 #[tokio::test]
1559 async fn test_deprecation_warning() {
1560 let handler = TestHandler {
1561 result: "deprecated result".to_string(),
1562 should_fail: false,
1563 };
1564
1565 let deprecation = ToolDeprecation::new("This tool is outdated".to_string())
1566 .with_replacement("new_tool".to_string())
1567 .with_severity(DeprecationSeverity::High);
1568
1569 let tool = ToolBuilder::new("deprecated_tool")
1570 .deprecated(deprecation)
1571 .build(handler)
1572 .expect("Failed to build tool");
1573
1574 assert!(tool.is_deprecated());
1575 let warning = tool.deprecation_warning().unwrap();
1576 assert!(warning.contains("deprecated"));
1577 assert!(warning.contains("outdated"));
1578 assert!(warning.contains("new_tool"));
1579 }
1580
1581 #[tokio::test]
1582 async fn test_category_filtering() {
1583 let category = ToolCategory::new("file".to_string())
1584 .with_secondary("read".to_string())
1585 .with_tag("filesystem".to_string())
1586 .with_tag("utility".to_string());
1587
1588 let handler = TestHandler {
1589 result: "filtered result".to_string(),
1590 should_fail: false,
1591 };
1592
1593 let tool = ToolBuilder::new("filterable_tool")
1594 .category(category)
1595 .build(handler)
1596 .expect("Failed to build tool");
1597
1598 let filter = CategoryFilter::new().with_primary("file".to_string());
1600 assert!(tool.matches_category_filter(&filter));
1601
1602 let filter = CategoryFilter::new().with_primary("network".to_string());
1603 assert!(!tool.matches_category_filter(&filter));
1604
1605 let filter = CategoryFilter::new().with_tag("filesystem".to_string());
1607 assert!(tool.matches_category_filter(&filter));
1608
1609 let filter = CategoryFilter::new().with_tag("nonexistent".to_string());
1610 assert!(!tool.matches_category_filter(&filter));
1611
1612 let filter = CategoryFilter::new().with_secondary("read".to_string());
1614 assert!(tool.matches_category_filter(&filter));
1615
1616 let filter = CategoryFilter::new().with_secondary("write".to_string());
1617 assert!(!tool.matches_category_filter(&filter));
1618 }
1619
1620 #[tokio::test]
1621 async fn test_behavior_hints() {
1622 let hints = ToolBehaviorHints::new()
1623 .read_only()
1624 .idempotent()
1625 .cacheable()
1626 .requires_auth()
1627 .long_running()
1628 .resource_intensive();
1629
1630 let handler = TestHandler {
1631 result: "hints result".to_string(),
1632 should_fail: false,
1633 };
1634
1635 let tool = ToolBuilder::new("hints_tool")
1636 .behavior_hints(hints)
1637 .build(handler)
1638 .expect("Failed to build tool");
1639
1640 assert!(tool.is_read_only());
1641 assert!(tool.is_idempotent());
1642 assert!(tool.is_cacheable());
1643 assert!(tool.requires_auth());
1644 assert!(!tool.is_destructive());
1645
1646 let behavior_hints = tool.behavior_hints();
1647 assert_eq!(behavior_hints.read_only, Some(true));
1648 assert_eq!(behavior_hints.idempotent, Some(true));
1649 assert_eq!(behavior_hints.cacheable, Some(true));
1650 assert_eq!(behavior_hints.requires_auth, Some(true));
1651 assert_eq!(behavior_hints.long_running, Some(true));
1652 assert_eq!(behavior_hints.resource_intensive, Some(true));
1653 assert_eq!(behavior_hints.destructive, None);
1654 }
1655
1656 #[tokio::test]
1657 async fn test_tool_enabling_disabling() {
1658 let handler = TestHandler {
1659 result: "enabled result".to_string(),
1660 should_fail: false,
1661 };
1662
1663 let mut tool = ToolBuilder::new("enable_test")
1664 .build(handler)
1665 .expect("Failed to build tool");
1666
1667 assert!(tool.is_enabled());
1668
1669 tool.disable();
1671 assert!(!tool.is_enabled());
1672
1673 let result = tool.call(HashMap::new()).await;
1675 assert!(result.is_err());
1676 assert!(result.unwrap_err().to_string().contains("disabled"));
1677
1678 tool.enable();
1680 assert!(tool.is_enabled());
1681
1682 let result = tool.call(HashMap::new()).await;
1684 assert!(result.is_ok());
1685 }
1686
1687 #[tokio::test]
1688 async fn test_custom_metadata() {
1689 let handler = TestHandler {
1690 result: "metadata result".to_string(),
1691 should_fail: false,
1692 };
1693
1694 let mut tool = ToolBuilder::new("metadata_tool")
1695 .custom_metadata("priority".to_string(), serde_json::Value::from("high"))
1696 .custom_metadata("team".to_string(), serde_json::Value::from("backend"))
1697 .build(handler)
1698 .expect("Failed to build tool");
1699
1700 assert_eq!(
1701 tool.get_custom_metadata("priority"),
1702 Some(&serde_json::Value::from("high"))
1703 );
1704 assert_eq!(
1705 tool.get_custom_metadata("team"),
1706 Some(&serde_json::Value::from("backend"))
1707 );
1708 assert_eq!(tool.get_custom_metadata("nonexistent"), None);
1709
1710 tool.add_custom_metadata(
1712 "environment".to_string(),
1713 serde_json::Value::from("production"),
1714 );
1715 assert_eq!(
1716 tool.get_custom_metadata("environment"),
1717 Some(&serde_json::Value::from("production"))
1718 );
1719 }
1720
1721 #[test]
1722 fn test_tool_debug_format() {
1723 let handler = TestHandler {
1724 result: "debug result".to_string(),
1725 should_fail: false,
1726 };
1727
1728 let tool = ToolBuilder::new("debug_tool")
1729 .version("2.0.0")
1730 .category_simple("debug".to_string(), None)
1731 .build(handler)
1732 .expect("Failed to build tool");
1733
1734 let debug_str = format!("{tool:?}");
1735 assert!(debug_str.contains("debug_tool"));
1736 assert!(debug_str.contains("enabled"));
1737 assert!(debug_str.contains("execution_count"));
1738 assert!(debug_str.contains("success_rate"));
1739 }
1740}