1use std::collections::HashMap;
28
29use fastmcp_core::{McpContext, McpOutcome, McpResult, Outcome};
30use fastmcp_protocol::{Content, Tool};
31
32use crate::handler::{BoxFuture, BoxedToolHandler, ToolHandler};
33
34#[derive(Debug, Clone, Copy, Default)]
36pub struct NotSet;
37
38#[derive(Debug, Clone, Default)]
43pub struct ArgTransform {
44 pub name: Option<String>,
46 pub description: Option<String>,
48 pub default: Option<serde_json::Value>,
50 pub hide: bool,
53 pub required: Option<bool>,
56 pub type_schema: Option<serde_json::Value>,
58}
59
60impl ArgTransform {
61 #[must_use]
63 pub fn new() -> Self {
64 <Self as Default>::default()
65 }
66
67 #[must_use]
69 pub fn name(mut self, name: impl Into<String>) -> Self {
70 self.name = Some(name.into());
71 self
72 }
73
74 #[must_use]
76 pub fn description(mut self, desc: impl Into<String>) -> Self {
77 self.description = Some(desc.into());
78 self
79 }
80
81 #[must_use]
83 pub fn default(mut self, value: impl Into<serde_json::Value>) -> Self {
84 self.default = Some(value.into());
85 self
86 }
87
88 #[must_use]
90 pub fn default_str(self, value: impl Into<String>) -> Self {
91 self.default(serde_json::Value::String(value.into()))
92 }
93
94 #[must_use]
96 pub fn default_int(self, value: i64) -> Self {
97 self.default(serde_json::Value::Number(value.into()))
98 }
99
100 #[must_use]
102 pub fn default_bool(self, value: bool) -> Self {
103 self.default(serde_json::Value::Bool(value))
104 }
105
106 #[must_use]
111 pub fn hide(mut self) -> Self {
112 self.hide = true;
113 self
114 }
115
116 #[must_use]
118 pub fn required(mut self) -> Self {
119 self.required = Some(true);
120 self
121 }
122
123 #[must_use]
125 pub fn type_schema(mut self, schema: serde_json::Value) -> Self {
126 self.type_schema = Some(schema);
127 self
128 }
129
130 #[must_use]
132 pub fn drop_with_default(value: impl Into<serde_json::Value>) -> Self {
133 Self::new().default(value).hide()
134 }
135}
136
137pub struct TransformedTool {
145 parent: BoxedToolHandler,
147 definition: Tool,
149 arg_transforms: HashMap<String, ArgTransform>,
151 name_mapping: HashMap<String, String>,
153}
154
155impl TransformedTool {
156 pub fn from_tool<H: ToolHandler + 'static>(tool: H) -> TransformedToolBuilder {
158 TransformedToolBuilder::new(Box::new(tool))
159 }
160
161 pub fn from_boxed(tool: BoxedToolHandler) -> TransformedToolBuilder {
163 TransformedToolBuilder::new(tool)
164 }
165
166 #[must_use]
168 pub fn parent_definition(&self) -> Tool {
169 self.parent.definition()
170 }
171
172 #[must_use]
174 pub fn arg_transforms(&self) -> &HashMap<String, ArgTransform> {
175 &self.arg_transforms
176 }
177
178 fn transform_arguments(&self, arguments: serde_json::Value) -> McpResult<serde_json::Value> {
180 let mut args = match arguments {
181 serde_json::Value::Object(map) => map,
182 serde_json::Value::Null => serde_json::Map::new(),
183 _ => {
184 return Err(fastmcp_core::McpError::invalid_params(
185 "Arguments must be an object",
186 ));
187 }
188 };
189
190 let mut result = serde_json::Map::new();
191
192 for (original_name, transform) in &self.arg_transforms {
194 let new_name = transform.name.as_ref().unwrap_or(original_name);
195
196 if let Some(value) = args.remove(new_name) {
198 result.insert(original_name.clone(), value);
200 } else if let Some(default) = &transform.default {
201 result.insert(original_name.clone(), default.clone());
203 } else if transform.hide {
204 return Err(fastmcp_core::McpError::invalid_params(format!(
206 "Hidden argument '{}' requires a default value",
207 original_name
208 )));
209 }
210 }
212
213 for (key, value) in args {
215 if let Some(original) = self.name_mapping.get(&key) {
217 result.insert(original.clone(), value);
218 } else {
219 result.insert(key, value);
220 }
221 }
222
223 Ok(serde_json::Value::Object(result))
224 }
225}
226
227impl std::fmt::Debug for TransformedTool {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 f.debug_struct("TransformedTool")
230 .field("definition", &self.definition)
231 .field("arg_transforms", &self.arg_transforms)
232 .finish_non_exhaustive()
233 }
234}
235
236impl ToolHandler for TransformedTool {
237 fn definition(&self) -> Tool {
238 self.definition.clone()
239 }
240
241 fn call(&self, ctx: &McpContext, arguments: serde_json::Value) -> McpResult<Vec<Content>> {
242 let transformed_args = self.transform_arguments(arguments)?;
243 self.parent.call(ctx, transformed_args)
244 }
245
246 fn call_async<'a>(
247 &'a self,
248 ctx: &'a McpContext,
249 arguments: serde_json::Value,
250 ) -> BoxFuture<'a, McpOutcome<Vec<Content>>> {
251 Box::pin(async move {
252 let transformed_args = match self.transform_arguments(arguments) {
253 Ok(args) => args,
254 Err(e) => return Outcome::Err(e),
255 };
256 self.parent.call_async(ctx, transformed_args).await
257 })
258 }
259}
260
261pub struct TransformedToolBuilder {
263 parent: BoxedToolHandler,
264 name: Option<String>,
265 description: Option<String>,
266 arg_transforms: HashMap<String, ArgTransform>,
267}
268
269impl TransformedToolBuilder {
270 pub fn new(parent: BoxedToolHandler) -> Self {
272 Self {
273 parent,
274 name: None,
275 description: None,
276 arg_transforms: HashMap::new(),
277 }
278 }
279
280 #[must_use]
282 pub fn name(mut self, name: impl Into<String>) -> Self {
283 self.name = Some(name.into());
284 self
285 }
286
287 #[must_use]
289 pub fn description(mut self, desc: impl Into<String>) -> Self {
290 self.description = Some(desc.into());
291 self
292 }
293
294 #[must_use]
298 pub fn transform_arg(
299 mut self,
300 original_name: impl Into<String>,
301 transform: ArgTransform,
302 ) -> Self {
303 self.arg_transforms.insert(original_name.into(), transform);
304 self
305 }
306
307 #[must_use]
309 pub fn rename_arg(self, original_name: impl Into<String>, new_name: impl Into<String>) -> Self {
310 self.transform_arg(original_name, ArgTransform::new().name(new_name))
311 }
312
313 #[must_use]
315 pub fn hide_arg(
316 self,
317 original_name: impl Into<String>,
318 default: impl Into<serde_json::Value>,
319 ) -> Self {
320 self.transform_arg(original_name, ArgTransform::drop_with_default(default))
321 }
322
323 #[must_use]
325 pub fn build(self) -> TransformedTool {
326 let parent_def = self.parent.definition();
327
328 let mut name_mapping = HashMap::new();
330 for (original, transform) in &self.arg_transforms {
331 if let Some(new_name) = &transform.name {
332 name_mapping.insert(new_name.clone(), original.clone());
333 }
334 }
335
336 let definition = self.build_definition(&parent_def);
338
339 TransformedTool {
340 parent: self.parent,
341 definition,
342 arg_transforms: self.arg_transforms,
343 name_mapping,
344 }
345 }
346
347 fn build_definition(&self, parent: &Tool) -> Tool {
349 let name = self.name.clone().unwrap_or_else(|| parent.name.clone());
350 let description = self
351 .description
352 .clone()
353 .or_else(|| parent.description.clone());
354
355 let input_schema = self.transform_schema(&parent.input_schema);
357
358 Tool {
359 name,
360 description,
361 input_schema,
362 output_schema: parent.output_schema.clone(),
363 icon: parent.icon.clone(),
364 version: parent.version.clone(),
365 tags: parent.tags.clone(),
366 annotations: parent.annotations.clone(),
367 }
368 }
369
370 fn transform_schema(&self, original: &serde_json::Value) -> serde_json::Value {
372 let mut schema = original.clone();
373
374 let Some(obj) = schema.as_object_mut() else {
375 return schema;
376 };
377
378 if !obj.contains_key("properties") {
382 obj.insert(String::from("properties"), serde_json::json!({}));
383 }
384 if !obj.contains_key("required") {
385 obj.insert(String::from("required"), serde_json::json!([]));
386 }
387
388 let capacity = self.arg_transforms.len();
391 let mut props_to_remove: Vec<String> = Vec::with_capacity(capacity);
392 let mut props_to_add: Vec<(String, serde_json::Value)> = Vec::with_capacity(capacity);
393 let mut required_renames: Vec<(String, String)> = Vec::with_capacity(capacity);
394 let mut required_removes: Vec<String> = Vec::with_capacity(capacity);
395
396 {
398 let props = obj["properties"].as_object().unwrap();
399
400 for (original_name, transform) in &self.arg_transforms {
401 if transform.hide {
402 props_to_remove.push(original_name.clone());
403 required_removes.push(original_name.clone());
404 continue;
405 }
406
407 if let Some(prop_schema) = props.get(original_name).cloned() {
408 let new_name = transform.name.as_ref().unwrap_or(original_name);
409 let mut new_schema = prop_schema;
410
411 if let (Some(desc), Some(schema_obj)) =
413 (&transform.description, new_schema.as_object_mut())
414 {
415 schema_obj.insert(String::from("description"), serde_json::json!(desc));
416 }
417
418 if let Some(type_schema) = &transform.type_schema {
420 new_schema = type_schema.clone();
421 }
422
423 if let (Some(default), Some(schema_obj)) =
425 (&transform.default, new_schema.as_object_mut())
426 {
427 schema_obj.insert(String::from("default"), default.clone());
428 }
429
430 if new_name != original_name {
431 props_to_remove.push(original_name.clone());
432 props_to_add.push((new_name.clone(), new_schema));
433 required_renames.push((original_name.clone(), new_name.clone()));
434 } else {
435 props_to_add.push((original_name.clone(), new_schema));
437 }
438 }
439 }
440 }
441
442 if let Some(props) = obj.get_mut("properties").and_then(|p| p.as_object_mut()) {
444 for name in &props_to_remove {
445 props.remove(name);
446 }
447 for (name, prop_schema) in props_to_add {
448 props.insert(name, prop_schema);
449 }
450 }
451
452 if let Some(required) = obj.get_mut("required").and_then(|r| r.as_array_mut()) {
454 for (old_name, new_name) in required_renames {
456 if let Some(idx) = required.iter().position(|v| v.as_str() == Some(&old_name)) {
457 required[idx] = serde_json::json!(new_name);
458 }
459 }
460 required.retain(|v| {
462 v.as_str()
463 .is_none_or(|s| !required_removes.iter().any(|r| r == s))
464 });
465 }
466
467 schema
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474 use fastmcp_protocol::Content;
475
476 struct SearchToolFixture {
477 name: String,
478 description: Option<String>,
479 schema: serde_json::Value,
480 }
481
482 impl SearchToolFixture {
483 fn new(name: &str) -> Self {
484 Self {
485 name: name.to_string(),
486 description: Some("Search tool".to_string()),
487 schema: serde_json::json!({
488 "type": "object",
489 "properties": {
490 "q": {
491 "type": "string",
492 "description": "Query"
493 },
494 "n": {
495 "type": "integer",
496 "description": "Limit"
497 }
498 },
499 "required": ["q"]
500 }),
501 }
502 }
503 }
504
505 impl ToolHandler for SearchToolFixture {
506 fn definition(&self) -> Tool {
507 Tool {
508 name: self.name.clone(),
509 description: self.description.clone(),
510 input_schema: self.schema.clone(),
511 output_schema: None,
512 icon: None,
513 version: None,
514 tags: vec![],
515 annotations: None,
516 }
517 }
518
519 fn call(&self, _ctx: &McpContext, arguments: serde_json::Value) -> McpResult<Vec<Content>> {
520 Ok(vec![Content::Text {
521 text: format!("Search called with: {}", arguments),
522 }])
523 }
524 }
525
526 #[test]
527 fn test_rename_tool() {
528 let tool = SearchToolFixture::new("search");
529 let transformed = TransformedTool::from_tool(tool)
530 .name("semantic_search")
531 .description("Search semantically")
532 .build();
533
534 let def = transformed.definition();
535 assert_eq!(def.name, "semantic_search");
536 assert_eq!(def.description, Some("Search semantically".to_string()));
537 }
538
539 #[test]
540 fn test_rename_arg() {
541 let tool = SearchToolFixture::new("search");
542 let transformed = TransformedTool::from_tool(tool)
543 .rename_arg("q", "query")
544 .build();
545
546 let def = transformed.definition();
547 let props = def.input_schema["properties"].as_object().unwrap();
548
549 assert!(!props.contains_key("q"));
551 assert!(props.contains_key("query"));
553 }
554
555 #[test]
556 fn test_hide_arg() {
557 let tool = SearchToolFixture::new("search");
558 let transformed = TransformedTool::from_tool(tool).hide_arg("n", 10).build();
559
560 let def = transformed.definition();
561 let props = def.input_schema["properties"].as_object().unwrap();
562
563 assert!(!props.contains_key("n"));
565 assert!(props.contains_key("q"));
567 }
568
569 #[test]
570 fn test_transform_arguments() {
571 let tool = SearchToolFixture::new("search");
572 let transformed = TransformedTool::from_tool(tool)
573 .rename_arg("q", "query")
574 .hide_arg("n", 10)
575 .build();
576
577 let input = serde_json::json!({
579 "query": "hello world"
580 });
581
582 let result = transformed.transform_arguments(input).unwrap();
584 let obj = result.as_object().unwrap();
585
586 assert_eq!(obj.get("q").unwrap(), "hello world");
587 assert_eq!(obj.get("n").unwrap(), 10);
588 }
589
590 #[test]
591 fn test_arg_transform_builder() {
592 let transform = ArgTransform::new()
593 .name("search_query")
594 .description("The search query string")
595 .default_str("*")
596 .required();
597
598 assert_eq!(transform.name, Some("search_query".to_string()));
599 assert_eq!(
600 transform.description,
601 Some("The search query string".to_string())
602 );
603 assert_eq!(transform.default, Some(serde_json::json!("*")));
604 assert_eq!(transform.required, Some(true));
605 assert!(!transform.hide);
606 }
607
608 #[test]
611 fn arg_transform_default_int() {
612 let t = ArgTransform::new().default_int(42);
613 assert_eq!(t.default, Some(serde_json::json!(42)));
614 }
615
616 #[test]
617 fn arg_transform_default_bool() {
618 let t = ArgTransform::new().default_bool(true);
619 assert_eq!(t.default, Some(serde_json::json!(true)));
620 }
621
622 #[test]
623 fn arg_transform_type_schema() {
624 let schema = serde_json::json!({"type": "number", "minimum": 0});
625 let t = ArgTransform::new().type_schema(schema.clone());
626 assert_eq!(t.type_schema, Some(schema));
627 }
628
629 #[test]
630 fn arg_transform_drop_with_default() {
631 let t = ArgTransform::drop_with_default("auto");
632 assert!(t.hide);
633 assert_eq!(t.default, Some(serde_json::json!("auto")));
634 }
635
636 #[test]
637 fn arg_transform_hide_sets_flag() {
638 let t = ArgTransform::new().hide();
639 assert!(t.hide);
640 }
641
642 #[test]
643 fn arg_transform_debug() {
644 let t = ArgTransform::new().name("x");
645 let debug = format!("{:?}", t);
646 assert!(debug.contains("ArgTransform"));
647 }
648
649 #[test]
650 fn arg_transform_clone() {
651 let t = ArgTransform::new().name("x").default_int(5);
652 let c = t.clone();
653 assert_eq!(c.name, Some("x".to_string()));
654 assert_eq!(c.default, Some(serde_json::json!(5)));
655 }
656
657 #[test]
660 fn transformed_tool_parent_definition() {
661 let tool = SearchToolFixture::new("original");
662 let transformed = TransformedTool::from_tool(tool).name("renamed").build();
663 let parent_def = transformed.parent_definition();
664 assert_eq!(parent_def.name, "original");
665 }
666
667 #[test]
668 fn transformed_tool_arg_transforms_accessor() {
669 let tool = SearchToolFixture::new("search");
670 let transformed = TransformedTool::from_tool(tool)
671 .rename_arg("q", "query")
672 .build();
673 let transforms = transformed.arg_transforms();
674 assert!(transforms.contains_key("q"));
675 }
676
677 #[test]
678 fn transformed_tool_debug_format() {
679 let tool = SearchToolFixture::new("search");
680 let transformed = TransformedTool::from_tool(tool).name("dbg_tool").build();
681 let debug = format!("{:?}", transformed);
682 assert!(debug.contains("TransformedTool"));
683 assert!(debug.contains("dbg_tool"));
684 }
685
686 #[test]
687 fn transformed_tool_from_boxed() {
688 let tool = Box::new(SearchToolFixture::new("boxed")) as BoxedToolHandler;
689 let transformed = TransformedTool::from_boxed(tool).name("unboxed").build();
690 assert_eq!(transformed.definition().name, "unboxed");
691 }
692
693 #[test]
696 fn transform_arguments_null_treated_as_empty() {
697 let tool = SearchToolFixture::new("search");
698 let transformed = TransformedTool::from_tool(tool).hide_arg("n", 10).build();
699
700 let result = transformed
701 .transform_arguments(serde_json::Value::Null)
702 .unwrap();
703 let obj = result.as_object().unwrap();
704 assert_eq!(obj.get("n").unwrap(), 10);
705 }
706
707 #[test]
708 fn transform_arguments_non_object_returns_error() {
709 let tool = SearchToolFixture::new("search");
710 let transformed = TransformedTool::from_tool(tool).build();
711
712 let result = transformed.transform_arguments(serde_json::json!("bad"));
713 assert!(result.is_err());
714 let err = result.unwrap_err();
715 assert!(err.message.contains("Arguments must be an object"));
716 }
717
718 #[test]
719 fn transform_arguments_passthrough_unknown_args() {
720 let tool = SearchToolFixture::new("search");
721 let transformed = TransformedTool::from_tool(tool)
722 .rename_arg("q", "query")
723 .build();
724
725 let input = serde_json::json!({
726 "query": "test",
727 "extra": "value"
728 });
729 let result = transformed.transform_arguments(input).unwrap();
730 let obj = result.as_object().unwrap();
731 assert_eq!(obj.get("q").unwrap(), "test");
732 assert_eq!(obj.get("extra").unwrap(), "value");
733 }
734
735 #[test]
736 fn transform_arguments_hidden_without_default_errors() {
737 let tool = SearchToolFixture::new("search");
738 let transformed = TransformedTool::from_tool(tool)
739 .transform_arg("q", ArgTransform::new().hide())
740 .build();
741
742 let result = transformed.transform_arguments(serde_json::json!({}));
743 assert!(result.is_err());
744 assert!(
745 result
746 .unwrap_err()
747 .message
748 .contains("Hidden argument 'q' requires a default value")
749 );
750 }
751
752 #[test]
755 fn transformed_tool_call_delegates_with_mapped_args() {
756 let tool = SearchToolFixture::new("search");
757 let transformed = TransformedTool::from_tool(tool)
758 .rename_arg("q", "query")
759 .build();
760
761 let cx = asupersync::Cx::for_testing();
762 let ctx = McpContext::new(cx, 1);
763 let result = transformed
764 .call(&ctx, serde_json::json!({"query": "hello"}))
765 .unwrap();
766 assert_eq!(result.len(), 1);
767 }
768
769 #[test]
770 fn transformed_tool_call_with_invalid_args_returns_error() {
771 let tool = SearchToolFixture::new("search");
772 let transformed = TransformedTool::from_tool(tool).build();
773
774 let cx = asupersync::Cx::for_testing();
775 let ctx = McpContext::new(cx, 1);
776 let result = transformed.call(&ctx, serde_json::json!("string_not_object"));
777 assert!(result.is_err());
778 }
779
780 #[test]
783 fn builder_no_name_keeps_parent_name() {
784 let tool = SearchToolFixture::new("original_name");
785 let transformed = TransformedTool::from_tool(tool).build();
786 assert_eq!(transformed.definition().name, "original_name");
787 }
788
789 #[test]
790 fn builder_no_description_keeps_parent_description() {
791 let tool = SearchToolFixture::new("s");
792 let transformed = TransformedTool::from_tool(tool).build();
793 assert_eq!(
794 transformed.definition().description,
795 Some("Search tool".to_string())
796 );
797 }
798
799 #[test]
802 fn transform_schema_applies_description_override() {
803 let tool = SearchToolFixture::new("s");
804 let transformed = TransformedTool::from_tool(tool)
805 .transform_arg("q", ArgTransform::new().description("Full search query"))
806 .build();
807
808 let def = transformed.definition();
809 let q_schema = &def.input_schema["properties"]["q"];
810 assert_eq!(q_schema["description"], "Full search query");
811 }
812
813 #[test]
816 fn not_set_debug() {
817 let n = NotSet;
818 let debug = format!("{:?}", n);
819 assert!(debug.contains("NotSet"));
820 }
821
822 #[test]
823 fn not_set_clone_copy() {
824 let n = NotSet;
825 let cloned = n.clone();
826 let copied = n; let _ = (cloned, copied);
828 }
829
830 #[test]
831 fn not_set_default() {
832 let _ = NotSet;
833 }
834
835 #[test]
838 fn arg_transform_new_is_all_none() {
839 let t = ArgTransform::new();
840 assert!(t.name.is_none());
841 assert!(t.description.is_none());
842 assert!(t.default.is_none());
843 assert!(!t.hide);
844 assert!(t.required.is_none());
845 assert!(t.type_schema.is_none());
846 }
847
848 #[test]
849 fn arg_transform_default_trait() {
850 let t = <ArgTransform as Default>::default();
851 assert!(t.name.is_none());
852 assert!(!t.hide);
853 }
854
855 #[test]
858 fn transform_schema_applies_type_override() {
859 let tool = SearchToolFixture::new("s");
860 let transformed = TransformedTool::from_tool(tool)
861 .transform_arg(
862 "q",
863 ArgTransform::new().type_schema(serde_json::json!({"type": "number"})),
864 )
865 .build();
866
867 let def = transformed.definition();
868 let q_schema = &def.input_schema["properties"]["q"];
869 assert_eq!(q_schema["type"], "number");
870 }
871
872 #[test]
875 fn transform_schema_applies_default_value() {
876 let tool = SearchToolFixture::new("s");
877 let transformed = TransformedTool::from_tool(tool)
878 .transform_arg("n", ArgTransform::new().default_int(25))
879 .build();
880
881 let def = transformed.definition();
882 let n_schema = &def.input_schema["properties"]["n"];
883 assert_eq!(n_schema["default"], 25);
884 }
885
886 #[test]
889 fn transform_schema_rename_updates_required() {
890 let tool = SearchToolFixture::new("s");
891 let transformed = TransformedTool::from_tool(tool)
892 .rename_arg("q", "query")
893 .build();
894
895 let def = transformed.definition();
896 let required = def.input_schema["required"].as_array().unwrap();
897 assert!(required.iter().any(|v| v == "query"));
898 assert!(!required.iter().any(|v| v == "q"));
899 }
900
901 #[test]
904 fn transform_schema_hide_removes_from_required() {
905 let tool = SearchToolFixture::new("s");
907 let transformed = TransformedTool::from_tool(tool)
908 .hide_arg("q", "default-query")
909 .build();
910
911 let def = transformed.definition();
912 let required = def.input_schema["required"].as_array().unwrap();
913 assert!(!required.iter().any(|v| v == "q"));
914 }
915
916 #[test]
919 fn combined_rename_description_default() {
920 let tool = SearchToolFixture::new("search");
921 let transformed = TransformedTool::from_tool(tool)
922 .transform_arg(
923 "n",
924 ArgTransform::new()
925 .name("limit")
926 .description("Max results")
927 .default_int(10),
928 )
929 .build();
930
931 let def = transformed.definition();
932 let props = def.input_schema["properties"].as_object().unwrap();
933 assert!(!props.contains_key("n"));
934 let limit = props.get("limit").unwrap();
935 assert_eq!(limit["description"], "Max results");
936 assert_eq!(limit["default"], 10);
937 }
938
939 #[test]
942 fn build_definition_preserves_parent_output_schema() {
943 struct ToolWithOutputSchema;
944 impl ToolHandler for ToolWithOutputSchema {
945 fn definition(&self) -> Tool {
946 Tool {
947 name: "parent".to_string(),
948 description: None,
949 input_schema: serde_json::json!({"type": "object"}),
950 output_schema: Some(serde_json::json!({"type": "string"})),
951 icon: None,
952 version: Some("2.0".to_string()),
953 tags: vec!["tag1".to_string()],
954 annotations: None,
955 }
956 }
957 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
958 Ok(vec![])
959 }
960 }
961
962 let transformed = TransformedTool::from_tool(ToolWithOutputSchema)
963 .name("child")
964 .build();
965 let def = transformed.definition();
966 assert_eq!(
967 def.output_schema,
968 Some(serde_json::json!({"type": "string"}))
969 );
970 assert_eq!(def.version, Some("2.0".to_string()));
971 assert_eq!(def.tags, vec!["tag1".to_string()]);
972 }
973
974 #[test]
977 fn transform_schema_non_object_returned_as_is() {
978 struct ArraySchemaTool;
979 impl ToolHandler for ArraySchemaTool {
980 fn definition(&self) -> Tool {
981 Tool {
982 name: "arr".to_string(),
983 description: None,
984 input_schema: serde_json::json!("not an object"),
985 output_schema: None,
986 icon: None,
987 version: None,
988 tags: vec![],
989 annotations: None,
990 }
991 }
992 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
993 Ok(vec![])
994 }
995 }
996
997 let transformed = TransformedTool::from_tool(ArraySchemaTool)
998 .rename_arg("x", "y")
999 .build();
1000 let def = transformed.definition();
1001 assert_eq!(def.input_schema, serde_json::json!("not an object"));
1003 }
1004
1005 #[test]
1008 fn transform_schema_adds_properties_and_required_if_missing() {
1009 struct MinimalSchemaTool;
1010 impl ToolHandler for MinimalSchemaTool {
1011 fn definition(&self) -> Tool {
1012 Tool {
1013 name: "min".to_string(),
1014 description: None,
1015 input_schema: serde_json::json!({"type": "object"}),
1016 output_schema: None,
1017 icon: None,
1018 version: None,
1019 tags: vec![],
1020 annotations: None,
1021 }
1022 }
1023 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
1024 Ok(vec![])
1025 }
1026 }
1027
1028 let transformed = TransformedTool::from_tool(MinimalSchemaTool).build();
1029 let def = transformed.definition();
1030 assert!(def.input_schema["properties"].is_object());
1031 assert!(def.input_schema["required"].is_array());
1032 }
1033
1034 #[test]
1037 fn transformed_tool_call_injects_hidden_defaults() {
1038 let tool = SearchToolFixture::new("search");
1039 let transformed = TransformedTool::from_tool(tool)
1040 .rename_arg("q", "query")
1041 .hide_arg("n", 5)
1042 .build();
1043
1044 let cx = asupersync::Cx::for_testing();
1045 let ctx = McpContext::new(cx, 1);
1046 let result = transformed
1047 .call(&ctx, serde_json::json!({"query": "test"}))
1048 .unwrap();
1049 assert_eq!(result.len(), 1);
1051 if let Content::Text { text } = &result[0] {
1052 assert!(text.contains("\"n\":5"));
1053 assert!(text.contains("\"q\":\"test\""));
1054 } else {
1055 panic!("expected text content");
1056 }
1057 }
1058
1059 #[test]
1062 fn transform_arg_with_noop_keeps_original() {
1063 let tool = SearchToolFixture::new("search");
1064 let transformed = TransformedTool::from_tool(tool)
1065 .transform_arg("q", ArgTransform::new())
1066 .build();
1067
1068 let def = transformed.definition();
1069 let props = def.input_schema["properties"].as_object().unwrap();
1070 assert!(props.contains_key("q"));
1072 }
1073
1074 #[test]
1077 fn transform_arg_for_nonexistent_arg_is_ignored() {
1078 let tool = SearchToolFixture::new("search");
1079 let transformed = TransformedTool::from_tool(tool)
1080 .rename_arg("nonexistent", "renamed")
1081 .build();
1082
1083 let def = transformed.definition();
1084 let props = def.input_schema["properties"].as_object().unwrap();
1085 assert!(props.contains_key("q"));
1087 assert!(props.contains_key("n"));
1088 assert!(!props.contains_key("renamed"));
1090 }
1091
1092 #[test]
1093 fn call_async_delegates_with_mapped_args() {
1094 use fastmcp_core::block_on;
1095
1096 let tool = SearchToolFixture::new("search");
1097 let transformed = TransformedTool::from_tool(tool)
1098 .rename_arg("q", "query")
1099 .hide_arg("n", 7)
1100 .build();
1101
1102 let cx = asupersync::Cx::for_testing();
1103 let ctx = McpContext::new(cx, 1);
1104 let result = block_on(transformed.call_async(&ctx, serde_json::json!({"query": "async"})));
1105 let content = result.unwrap();
1106 assert_eq!(content.len(), 1);
1107 if let Content::Text { text } = &content[0] {
1108 assert!(text.contains("\"q\":\"async\""));
1109 assert!(text.contains("\"n\":7"));
1110 } else {
1111 panic!("expected text content");
1112 }
1113 }
1114
1115 #[test]
1116 fn transform_arguments_no_value_no_default_not_hidden_skipped() {
1117 let tool = SearchToolFixture::new("search");
1118 let transformed = TransformedTool::from_tool(tool)
1119 .transform_arg("n", ArgTransform::new().description("ignored desc"))
1120 .build();
1121
1122 let result = transformed
1124 .transform_arguments(serde_json::json!({"q": "hello"}))
1125 .unwrap();
1126 let obj = result.as_object().unwrap();
1127 assert_eq!(obj.get("q").unwrap(), "hello");
1128 assert!(
1129 obj.get("n").is_none(),
1130 "missing arg without default should be skipped"
1131 );
1132 }
1133
1134 #[test]
1135 fn transform_arguments_default_used_without_hide() {
1136 let tool = SearchToolFixture::new("search");
1137 let transformed = TransformedTool::from_tool(tool)
1138 .transform_arg("n", ArgTransform::new().default_int(99))
1139 .build();
1140
1141 let result = transformed
1143 .transform_arguments(serde_json::json!({"q": "test"}))
1144 .unwrap();
1145 let obj = result.as_object().unwrap();
1146 assert_eq!(obj.get("n").unwrap(), 99);
1147 }
1148
1149 #[test]
1150 fn build_definition_parent_no_description_returns_none() {
1151 struct NoDescTool;
1152 impl ToolHandler for NoDescTool {
1153 fn definition(&self) -> Tool {
1154 Tool {
1155 name: "nodesc".to_string(),
1156 description: None,
1157 input_schema: serde_json::json!({"type": "object"}),
1158 output_schema: None,
1159 icon: None,
1160 version: None,
1161 tags: vec![],
1162 annotations: None,
1163 }
1164 }
1165 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
1166 Ok(vec![])
1167 }
1168 }
1169
1170 let transformed = TransformedTool::from_tool(NoDescTool).build();
1171 assert!(transformed.definition().description.is_none());
1172 }
1173
1174 #[test]
1175 fn transform_schema_preserves_unrenamed_in_required() {
1176 struct TwoReqTool;
1178 impl ToolHandler for TwoReqTool {
1179 fn definition(&self) -> Tool {
1180 Tool {
1181 name: "two".to_string(),
1182 description: None,
1183 input_schema: serde_json::json!({
1184 "type": "object",
1185 "properties": {
1186 "a": {"type": "string"},
1187 "b": {"type": "string"}
1188 },
1189 "required": ["a", "b"]
1190 }),
1191 output_schema: None,
1192 icon: None,
1193 version: None,
1194 tags: vec![],
1195 annotations: None,
1196 }
1197 }
1198 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
1199 Ok(vec![])
1200 }
1201 }
1202
1203 let transformed = TransformedTool::from_tool(TwoReqTool)
1204 .rename_arg("a", "alpha")
1205 .build();
1206 let def = transformed.definition();
1207 let required = def.input_schema["required"].as_array().unwrap();
1208 assert!(
1209 required.iter().any(|v| v == "alpha"),
1210 "renamed arg in required"
1211 );
1212 assert!(
1213 required.iter().any(|v| v == "b"),
1214 "unrenamed arg still in required"
1215 );
1216 assert!(
1217 !required.iter().any(|v| v == "a"),
1218 "old name removed from required"
1219 );
1220 }
1221
1222 #[test]
1223 fn type_schema_replaces_entire_property() {
1224 let tool = SearchToolFixture::new("s");
1225 let transformed = TransformedTool::from_tool(tool)
1226 .transform_arg(
1227 "q",
1228 ArgTransform::new()
1229 .type_schema(serde_json::json!({"type": "array", "items": {"type": "string"}})),
1230 )
1231 .build();
1232
1233 let def = transformed.definition();
1234 let q_schema = &def.input_schema["properties"]["q"];
1235 assert_eq!(q_schema["type"], "array");
1237 assert!(q_schema["items"].is_object());
1238 assert!(q_schema.get("description").is_none());
1240 }
1241}