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