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