1use std::collections::HashMap;
28use std::time::Duration;
29
30use asupersync::Cx;
31use fastmcp_core::{McpContext, McpOutcome, McpResult, Outcome};
32use fastmcp_protocol::common_types::{OpenMetadata, RawIcon};
33use fastmcp_protocol::{
34 CompleteResult, Content, FinalCallToolResult, FinalTool, Icon, Tool, ToolAnnotations,
35};
36
37use crate::bidirectional::MrtrCompletedInputs;
38use crate::handler::{
39 BoxFuture, BoxedToolHandler, FinalToolOutcome, FinalToolSchemaAuthority, ToolErrorKind,
40 ToolHandler, UpstreamFinalToolSchemaRegistration,
41};
42
43#[derive(Debug, Clone, Copy, Default)]
45pub struct NotSet;
46
47#[derive(Debug, Clone, Default)]
52pub struct ArgTransform {
53 pub name: Option<String>,
55 pub description: Option<String>,
57 pub default: Option<serde_json::Value>,
59 pub hide: bool,
62 pub required: Option<bool>,
65 pub type_schema: Option<serde_json::Value>,
67}
68
69impl ArgTransform {
70 #[must_use]
72 pub fn new() -> Self {
73 <Self as Default>::default()
74 }
75
76 #[must_use]
78 pub fn name(mut self, name: impl Into<String>) -> Self {
79 self.name = Some(name.into());
80 self
81 }
82
83 #[must_use]
85 pub fn description(mut self, desc: impl Into<String>) -> Self {
86 self.description = Some(desc.into());
87 self
88 }
89
90 #[must_use]
92 pub fn default(mut self, value: impl Into<serde_json::Value>) -> Self {
93 self.default = Some(value.into());
94 self
95 }
96
97 #[must_use]
99 pub fn default_str(self, value: impl Into<String>) -> Self {
100 self.default(serde_json::Value::String(value.into()))
101 }
102
103 #[must_use]
105 pub fn default_int(self, value: i64) -> Self {
106 self.default(serde_json::Value::Number(value.into()))
107 }
108
109 #[must_use]
111 pub fn default_bool(self, value: bool) -> Self {
112 self.default(serde_json::Value::Bool(value))
113 }
114
115 #[must_use]
120 pub fn hide(mut self) -> Self {
121 self.hide = true;
122 self
123 }
124
125 #[must_use]
127 pub fn required(mut self) -> Self {
128 self.required = Some(true);
129 self
130 }
131
132 #[must_use]
134 pub fn type_schema(mut self, schema: serde_json::Value) -> Self {
135 self.type_schema = Some(schema);
136 self
137 }
138
139 #[must_use]
141 pub fn drop_with_default(value: impl Into<serde_json::Value>) -> Self {
142 Self::new().default(value).hide()
143 }
144}
145
146pub struct TransformedTool {
154 parent: BoxedToolHandler,
156 definition: Tool,
158 arg_transforms: HashMap<String, ArgTransform>,
160 name_mapping: HashMap<String, String>,
162}
163
164impl TransformedTool {
165 pub fn from_tool<H: ToolHandler + 'static>(tool: H) -> TransformedToolBuilder {
167 TransformedToolBuilder::new(Box::new(tool))
168 }
169
170 pub fn from_boxed(tool: BoxedToolHandler) -> TransformedToolBuilder {
172 TransformedToolBuilder::new(tool)
173 }
174
175 #[must_use]
177 pub fn parent_definition(&self) -> Tool {
178 self.parent.definition()
179 }
180
181 #[must_use]
183 pub fn arg_transforms(&self) -> &HashMap<String, ArgTransform> {
184 &self.arg_transforms
185 }
186
187 fn transform_arguments(&self, arguments: serde_json::Value) -> McpResult<serde_json::Value> {
189 let mut args = match arguments {
190 serde_json::Value::Object(map) => map,
191 serde_json::Value::Null => serde_json::Map::new(),
192 _ => {
193 return Err(fastmcp_core::McpError::invalid_params(
194 "Arguments must be an object",
195 ));
196 }
197 };
198
199 let mut result = serde_json::Map::new();
200
201 for (original_name, transform) in &self.arg_transforms {
203 let new_name = transform.name.as_ref().unwrap_or(original_name);
204
205 if transform.hide {
206 args.remove(new_name);
211 args.remove(original_name);
212 if let Some(default) = &transform.default {
213 result.insert(original_name.clone(), default.clone());
214 continue;
215 }
216 return Err(fastmcp_core::McpError::invalid_params(format!(
217 "Hidden argument '{original_name}' requires a default value"
218 )));
219 }
220
221 if let Some(value) = args.remove(new_name) {
222 result.insert(original_name.clone(), value);
223 } else if let Some(default) = &transform.default {
224 result.insert(original_name.clone(), default.clone());
225 }
226 if new_name != original_name {
229 args.remove(original_name);
230 }
231 }
232
233 for (key, value) in args {
235 if let Some(original) = self.name_mapping.get(&key) {
237 result.insert(original.clone(), value);
238 } else {
239 result.insert(key, value);
240 }
241 }
242
243 Ok(serde_json::Value::Object(result))
244 }
245}
246
247impl std::fmt::Debug for TransformedTool {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 f.debug_struct("TransformedTool")
250 .field("definition", &self.definition)
251 .field("arg_transforms", &self.arg_transforms)
252 .finish_non_exhaustive()
253 }
254}
255
256impl ToolHandler for TransformedTool {
257 fn definition(&self) -> Tool {
258 self.definition.clone()
259 }
260
261 fn icon(&self) -> Option<&Icon> {
262 self.parent.icon()
263 }
264
265 fn version(&self) -> Option<&str> {
266 self.parent.version()
267 }
268
269 fn tags(&self) -> &[String] {
270 self.parent.tags()
271 }
272
273 fn annotations(&self) -> Option<&ToolAnnotations> {
274 self.parent.annotations()
275 }
276
277 fn output_schema(&self) -> Option<serde_json::Value> {
278 self.parent.output_schema()
279 }
280
281 fn final_title(&self) -> Option<&str> {
282 self.parent.final_title()
283 }
284
285 fn final_icons(&self) -> Option<&[RawIcon]> {
286 self.parent.final_icons()
287 }
288
289 fn final_metadata(&self) -> Option<&OpenMetadata> {
290 self.parent.final_metadata()
291 }
292
293 fn final_definition(&self) -> Option<FinalTool> {
294 let mut definition = self.parent.final_definition()?;
295 definition.name.clone_from(&self.definition.name);
296 definition
297 .description
298 .clone_from(&self.definition.description);
299 definition.input_schema =
300 transform_input_schema(&self.arg_transforms, &definition.input_schema);
301 Some(definition)
302 }
303
304 fn final_tool_schema_authority(&self) -> FinalToolSchemaAuthority {
305 self.parent.final_tool_schema_authority()
306 }
307
308 fn upstream_final_tool_schema_registration(
309 &self,
310 ) -> Option<UpstreamFinalToolSchemaRegistration> {
311 self.parent.upstream_final_tool_schema_registration()
312 }
313
314 fn final_tool_error_structured_content(
315 &self,
316 kind: ToolErrorKind,
317 ) -> Option<serde_json::Value> {
318 self.parent.final_tool_error_structured_content(kind)
319 }
320
321 fn declares_final_tasks(&self) -> bool {
322 self.parent.declares_final_tasks()
323 }
324
325 fn declares_final_mrtr(&self) -> bool {
326 self.parent.declares_final_mrtr()
327 }
328
329 fn timeout(&self) -> Option<Duration> {
330 self.parent.timeout()
331 }
332
333 fn call(&self, ctx: &McpContext, arguments: serde_json::Value) -> McpResult<Vec<Content>> {
334 let transformed_args = self.transform_arguments(arguments)?;
335 self.parent.call(ctx, transformed_args)
336 }
337
338 fn call_async<'a>(
339 &'a self,
340 ctx: &'a McpContext,
341 arguments: serde_json::Value,
342 ) -> BoxFuture<'a, McpOutcome<Vec<Content>>> {
343 Box::pin(async move {
344 let transformed_args = match self.transform_arguments(arguments) {
345 Ok(args) => args,
346 Err(error) => return Outcome::Err(error),
347 };
348 self.parent.call_async(ctx, transformed_args).await
349 })
350 }
351
352 fn call_async_in_request<'a>(
353 &'a self,
354 ctx: &'a McpContext,
355 request_cx: &'a Cx,
356 arguments: serde_json::Value,
357 ) -> BoxFuture<'a, McpOutcome<Vec<Content>>> {
358 Box::pin(async move {
359 let transformed_args = match self.transform_arguments(arguments) {
360 Ok(args) => args,
361 Err(error) => return Outcome::Err(error),
362 };
363 self.parent
364 .call_async_in_request(ctx, request_cx, transformed_args)
365 .await
366 })
367 }
368
369 fn call_final(
370 &self,
371 ctx: &McpContext,
372 arguments: serde_json::Value,
373 ) -> McpResult<CompleteResult<FinalCallToolResult>> {
374 self.parent
375 .call_final(ctx, self.transform_arguments(arguments)?)
376 }
377
378 fn call_final_async<'a>(
379 &'a self,
380 ctx: &'a McpContext,
381 arguments: serde_json::Value,
382 ) -> BoxFuture<'a, McpOutcome<CompleteResult<FinalCallToolResult>>> {
383 Box::pin(async move {
384 let transformed_args = match self.transform_arguments(arguments) {
385 Ok(args) => args,
386 Err(error) => return Outcome::Err(error),
387 };
388 self.parent.call_final_async(ctx, transformed_args).await
389 })
390 }
391
392 fn call_final_async_in_request<'a>(
393 &'a self,
394 ctx: &'a McpContext,
395 request_cx: &'a Cx,
396 arguments: serde_json::Value,
397 ) -> BoxFuture<'a, McpOutcome<CompleteResult<FinalCallToolResult>>> {
398 Box::pin(async move {
399 let transformed_args = match self.transform_arguments(arguments) {
400 Ok(args) => args,
401 Err(error) => return Outcome::Err(error),
402 };
403 self.parent
404 .call_final_async_in_request(ctx, request_cx, transformed_args)
405 .await
406 })
407 }
408
409 fn call_final_outcome(
410 &self,
411 ctx: &McpContext,
412 arguments: serde_json::Value,
413 ) -> McpResult<FinalToolOutcome> {
414 self.parent
415 .call_final_outcome(ctx, self.transform_arguments(arguments)?)
416 }
417
418 fn call_final_outcome_async<'a>(
419 &'a self,
420 ctx: &'a McpContext,
421 arguments: serde_json::Value,
422 ) -> BoxFuture<'a, McpOutcome<FinalToolOutcome>> {
423 Box::pin(async move {
424 let transformed_args = match self.transform_arguments(arguments) {
425 Ok(args) => args,
426 Err(error) => return Outcome::Err(error),
427 };
428 self.parent
429 .call_final_outcome_async(ctx, transformed_args)
430 .await
431 })
432 }
433
434 fn call_final_outcome_async_in_request<'a>(
435 &'a self,
436 ctx: &'a McpContext,
437 request_cx: &'a Cx,
438 arguments: serde_json::Value,
439 ) -> BoxFuture<'a, McpOutcome<FinalToolOutcome>> {
440 Box::pin(async move {
441 let transformed_args = match self.transform_arguments(arguments) {
442 Ok(args) => args,
443 Err(error) => return Outcome::Err(error),
444 };
445 self.parent
446 .call_final_outcome_async_in_request(ctx, request_cx, transformed_args)
447 .await
448 })
449 }
450
451 fn call_final_outcome_async_resuming_in_request<'a>(
452 &'a self,
453 ctx: &'a McpContext,
454 request_cx: &'a Cx,
455 arguments: serde_json::Value,
456 resume_inputs: Option<&'a MrtrCompletedInputs>,
457 ) -> BoxFuture<'a, McpOutcome<FinalToolOutcome>> {
458 Box::pin(async move {
459 let transformed_args = match self.transform_arguments(arguments) {
460 Ok(args) => args,
461 Err(error) => return Outcome::Err(error),
462 };
463 self.parent
464 .call_final_outcome_async_resuming_in_request(
465 ctx,
466 request_cx,
467 transformed_args,
468 resume_inputs,
469 )
470 .await
471 })
472 }
473}
474
475pub struct TransformedToolBuilder {
477 parent: BoxedToolHandler,
478 name: Option<String>,
479 description: Option<String>,
480 arg_transforms: HashMap<String, ArgTransform>,
481}
482
483impl TransformedToolBuilder {
484 pub fn new(parent: BoxedToolHandler) -> Self {
486 Self {
487 parent,
488 name: None,
489 description: None,
490 arg_transforms: HashMap::new(),
491 }
492 }
493
494 #[must_use]
496 pub fn name(mut self, name: impl Into<String>) -> Self {
497 self.name = Some(name.into());
498 self
499 }
500
501 #[must_use]
503 pub fn description(mut self, desc: impl Into<String>) -> Self {
504 self.description = Some(desc.into());
505 self
506 }
507
508 #[must_use]
512 pub fn transform_arg(
513 mut self,
514 original_name: impl Into<String>,
515 transform: ArgTransform,
516 ) -> Self {
517 self.arg_transforms.insert(original_name.into(), transform);
518 self
519 }
520
521 #[must_use]
523 pub fn rename_arg(self, original_name: impl Into<String>, new_name: impl Into<String>) -> Self {
524 self.transform_arg(original_name, ArgTransform::new().name(new_name))
525 }
526
527 #[must_use]
529 pub fn hide_arg(
530 self,
531 original_name: impl Into<String>,
532 default: impl Into<serde_json::Value>,
533 ) -> Self {
534 self.transform_arg(original_name, ArgTransform::drop_with_default(default))
535 }
536
537 #[must_use]
539 pub fn build(self) -> TransformedTool {
540 let parent_def = self.parent.definition();
541
542 let mut name_mapping = HashMap::new();
544 for (original, transform) in &self.arg_transforms {
545 if let Some(new_name) = &transform.name {
546 name_mapping.insert(new_name.clone(), original.clone());
547 }
548 }
549
550 let definition = self.build_definition(&parent_def);
552
553 TransformedTool {
554 parent: self.parent,
555 definition,
556 arg_transforms: self.arg_transforms,
557 name_mapping,
558 }
559 }
560
561 fn build_definition(&self, parent: &Tool) -> Tool {
563 let name = self.name.clone().unwrap_or_else(|| parent.name.clone());
564 let description = self
565 .description
566 .clone()
567 .or_else(|| parent.description.clone());
568
569 let input_schema = transform_input_schema(&self.arg_transforms, &parent.input_schema);
571
572 Tool {
573 name,
574 description,
575 input_schema,
576 output_schema: parent.output_schema.clone(),
577 icon: parent.icon.clone(),
578 version: parent.version.clone(),
579 tags: parent.tags.clone(),
580 annotations: parent.annotations.clone(),
581 }
582 }
583}
584
585fn transform_input_schema(
587 arg_transforms: &HashMap<String, ArgTransform>,
588 original: &serde_json::Value,
589) -> serde_json::Value {
590 let mut schema = original.clone();
591
592 let Some(obj) = schema.as_object_mut() else {
593 return schema;
594 };
595
596 if !obj
601 .get("properties")
602 .is_some_and(serde_json::Value::is_object)
603 {
604 obj.insert(String::from("properties"), serde_json::json!({}));
605 }
606 if !obj.get("required").is_some_and(serde_json::Value::is_array) {
607 obj.insert(String::from("required"), serde_json::json!([]));
608 }
609
610 let capacity = arg_transforms.len();
613 let mut props_to_remove: Vec<String> = Vec::with_capacity(capacity);
614 let mut props_to_add: Vec<(String, serde_json::Value)> = Vec::with_capacity(capacity);
615 let mut required_renames: Vec<(String, String)> = Vec::with_capacity(capacity);
616 let mut required_removes: Vec<String> = Vec::with_capacity(capacity);
617
618 {
620 let Some(props) = obj.get("properties").and_then(serde_json::Value::as_object) else {
621 return schema;
622 };
623
624 for (original_name, transform) in arg_transforms {
625 if transform.hide {
626 props_to_remove.push(original_name.clone());
627 required_removes.push(original_name.clone());
628 continue;
629 }
630
631 if let Some(prop_schema) = props.get(original_name).cloned() {
632 let new_name = transform.name.as_ref().unwrap_or(original_name);
633 let mut new_schema = prop_schema;
634
635 if let (Some(desc), Some(schema_obj)) =
637 (&transform.description, new_schema.as_object_mut())
638 {
639 schema_obj.insert(String::from("description"), serde_json::json!(desc));
640 }
641
642 if let Some(type_schema) = &transform.type_schema {
644 new_schema = type_schema.clone();
645 }
646
647 if let (Some(default), Some(schema_obj)) =
649 (&transform.default, new_schema.as_object_mut())
650 {
651 schema_obj.insert(String::from("default"), default.clone());
652 }
653
654 if new_name != original_name {
655 props_to_remove.push(original_name.clone());
656 props_to_add.push((new_name.clone(), new_schema));
657 required_renames.push((original_name.clone(), new_name.clone()));
658 } else {
659 props_to_add.push((original_name.clone(), new_schema));
661 }
662 }
663 }
664 }
665
666 if let Some(props) = obj.get_mut("properties").and_then(|p| p.as_object_mut()) {
668 for name in &props_to_remove {
669 props.remove(name);
670 }
671 for (name, prop_schema) in props_to_add {
672 props.insert(name, prop_schema);
673 }
674 }
675
676 if let Some(required) = obj.get_mut("required").and_then(|r| r.as_array_mut()) {
678 for (old_name, new_name) in required_renames {
680 if let Some(idx) = required.iter().position(|v| v.as_str() == Some(&old_name)) {
681 required[idx] = serde_json::json!(new_name);
682 }
683 }
684 required.retain(|v| {
686 v.as_str()
687 .is_none_or(|s| !required_removes.iter().any(|r| r == s))
688 });
689 }
690
691 schema
692}
693
694#[cfg(test)]
695mod tests {
696 use super::*;
697 use fastmcp_core::block_on;
698 use fastmcp_protocol::Content;
699 use fastmcp_protocol::common_types::ContentBlock;
700
701 struct SearchToolFixture {
702 name: String,
703 description: Option<String>,
704 schema: serde_json::Value,
705 }
706
707 impl SearchToolFixture {
708 fn new(name: &str) -> Self {
709 Self {
710 name: name.to_string(),
711 description: Some("Search tool".to_string()),
712 schema: serde_json::json!({
713 "type": "object",
714 "properties": {
715 "q": {
716 "type": "string",
717 "description": "Query"
718 },
719 "n": {
720 "type": "integer",
721 "description": "Limit"
722 }
723 },
724 "required": ["q"]
725 }),
726 }
727 }
728 }
729
730 impl ToolHandler for SearchToolFixture {
731 fn definition(&self) -> Tool {
732 Tool {
733 name: self.name.clone(),
734 description: self.description.clone(),
735 input_schema: self.schema.clone(),
736 output_schema: None,
737 icon: None,
738 version: None,
739 tags: vec![],
740 annotations: None,
741 }
742 }
743
744 fn call(&self, _ctx: &McpContext, arguments: serde_json::Value) -> McpResult<Vec<Content>> {
745 Ok(vec![Content::Text {
746 text: format!("Search called with: {}", arguments),
747 }])
748 }
749 }
750
751 #[test]
752 fn test_rename_tool() {
753 let tool = SearchToolFixture::new("search");
754 let transformed = TransformedTool::from_tool(tool)
755 .name("semantic_search")
756 .description("Search semantically")
757 .build();
758
759 let def = transformed.definition();
760 assert_eq!(def.name, "semantic_search");
761 assert_eq!(def.description, Some("Search semantically".to_string()));
762 }
763
764 #[test]
765 fn test_rename_arg() {
766 let tool = SearchToolFixture::new("search");
767 let transformed = TransformedTool::from_tool(tool)
768 .rename_arg("q", "query")
769 .build();
770
771 let def = transformed.definition();
772 let props = def.input_schema["properties"].as_object().unwrap();
773
774 assert!(!props.contains_key("q"));
776 assert!(props.contains_key("query"));
778 }
779
780 #[test]
781 fn test_hide_arg() {
782 let tool = SearchToolFixture::new("search");
783 let transformed = TransformedTool::from_tool(tool).hide_arg("n", 10).build();
784
785 let def = transformed.definition();
786 let props = def.input_schema["properties"].as_object().unwrap();
787
788 assert!(!props.contains_key("n"));
790 assert!(props.contains_key("q"));
792 }
793
794 #[test]
795 fn test_transform_arguments() {
796 let tool = SearchToolFixture::new("search");
797 let transformed = TransformedTool::from_tool(tool)
798 .rename_arg("q", "query")
799 .hide_arg("n", 10)
800 .build();
801
802 let input = serde_json::json!({
804 "query": "hello world"
805 });
806
807 let result = transformed.transform_arguments(input).unwrap();
809 let obj = result.as_object().unwrap();
810
811 assert_eq!(obj.get("q").unwrap(), "hello world");
812 assert_eq!(obj.get("n").unwrap(), 10);
813 }
814
815 #[test]
816 fn test_arg_transform_builder() {
817 let transform = ArgTransform::new()
818 .name("search_query")
819 .description("The search query string")
820 .default_str("*")
821 .required();
822
823 assert_eq!(transform.name, Some("search_query".to_string()));
824 assert_eq!(
825 transform.description,
826 Some("The search query string".to_string())
827 );
828 assert_eq!(transform.default, Some(serde_json::json!("*")));
829 assert_eq!(transform.required, Some(true));
830 assert!(!transform.hide);
831 }
832
833 #[test]
836 fn arg_transform_default_int() {
837 let t = ArgTransform::new().default_int(42);
838 assert_eq!(t.default, Some(serde_json::json!(42)));
839 }
840
841 #[test]
842 fn arg_transform_default_bool() {
843 let t = ArgTransform::new().default_bool(true);
844 assert_eq!(t.default, Some(serde_json::json!(true)));
845 }
846
847 #[test]
848 fn arg_transform_type_schema() {
849 let schema = serde_json::json!({"type": "number", "minimum": 0});
850 let t = ArgTransform::new().type_schema(schema.clone());
851 assert_eq!(t.type_schema, Some(schema));
852 }
853
854 #[test]
855 fn arg_transform_drop_with_default() {
856 let t = ArgTransform::drop_with_default("auto");
857 assert!(t.hide);
858 assert_eq!(t.default, Some(serde_json::json!("auto")));
859 }
860
861 #[test]
862 fn arg_transform_hide_sets_flag() {
863 let t = ArgTransform::new().hide();
864 assert!(t.hide);
865 }
866
867 #[test]
868 fn arg_transform_debug() {
869 let t = ArgTransform::new().name("x");
870 let debug = format!("{:?}", t);
871 assert!(debug.contains("ArgTransform"));
872 }
873
874 #[test]
875 fn arg_transform_clone() {
876 let t = ArgTransform::new().name("x").default_int(5);
877 let c = t.clone();
878 assert_eq!(c.name, Some("x".to_string()));
879 assert_eq!(c.default, Some(serde_json::json!(5)));
880 }
881
882 #[test]
885 fn transformed_tool_parent_definition() {
886 let tool = SearchToolFixture::new("original");
887 let transformed = TransformedTool::from_tool(tool).name("renamed").build();
888 let parent_def = transformed.parent_definition();
889 assert_eq!(parent_def.name, "original");
890 }
891
892 #[test]
893 fn transformed_tool_arg_transforms_accessor() {
894 let tool = SearchToolFixture::new("search");
895 let transformed = TransformedTool::from_tool(tool)
896 .rename_arg("q", "query")
897 .build();
898 let transforms = transformed.arg_transforms();
899 assert!(transforms.contains_key("q"));
900 }
901
902 #[test]
903 fn transformed_tool_debug_format() {
904 let tool = SearchToolFixture::new("search");
905 let transformed = TransformedTool::from_tool(tool).name("dbg_tool").build();
906 let debug = format!("{:?}", transformed);
907 assert!(debug.contains("TransformedTool"));
908 assert!(debug.contains("dbg_tool"));
909 }
910
911 #[test]
912 fn transformed_tool_from_boxed() {
913 let tool = Box::new(SearchToolFixture::new("boxed")) as BoxedToolHandler;
914 let transformed = TransformedTool::from_boxed(tool).name("unboxed").build();
915 assert_eq!(transformed.definition().name, "unboxed");
916 }
917
918 #[test]
921 fn transform_arguments_null_treated_as_empty() {
922 let tool = SearchToolFixture::new("search");
923 let transformed = TransformedTool::from_tool(tool).hide_arg("n", 10).build();
924
925 let result = transformed
926 .transform_arguments(serde_json::Value::Null)
927 .unwrap();
928 let obj = result.as_object().unwrap();
929 assert_eq!(obj.get("n").unwrap(), 10);
930 }
931
932 #[test]
933 fn transform_arguments_non_object_returns_error() {
934 let tool = SearchToolFixture::new("search");
935 let transformed = TransformedTool::from_tool(tool).build();
936
937 let result = transformed.transform_arguments(serde_json::json!("bad"));
938 assert!(result.is_err());
939 let err = result.unwrap_err();
940 assert!(err.message.contains("Arguments must be an object"));
941 }
942
943 #[test]
944 fn transform_arguments_passthrough_unknown_args() {
945 let tool = SearchToolFixture::new("search");
946 let transformed = TransformedTool::from_tool(tool)
947 .rename_arg("q", "query")
948 .build();
949
950 let input = serde_json::json!({
951 "query": "test",
952 "extra": "value"
953 });
954 let result = transformed.transform_arguments(input).unwrap();
955 let obj = result.as_object().unwrap();
956 assert_eq!(obj.get("q").unwrap(), "test");
957 assert_eq!(obj.get("extra").unwrap(), "value");
958 }
959
960 #[test]
961 fn transform_arguments_rename_ignores_original_name_leftover() {
962 let tool = SearchToolFixture::new("search");
963 let transformed = TransformedTool::from_tool(tool)
964 .rename_arg("q", "query")
965 .build();
966
967 let result = transformed
968 .transform_arguments(serde_json::json!({
969 "query": "mapped",
970 "q": "leftover"
971 }))
972 .unwrap();
973 let obj = result.as_object().unwrap();
974 assert_eq!(obj.get("q").unwrap(), "mapped");
975 assert!(
976 obj.len() == 1,
977 "the unpublished original name must not leak beside the mapped value: {obj:?}"
978 );
979 }
980
981 #[test]
982 fn transform_arguments_hidden_without_default_errors() {
983 let tool = SearchToolFixture::new("search");
984 let transformed = TransformedTool::from_tool(tool)
985 .transform_arg("q", ArgTransform::new().hide())
986 .build();
987
988 let result = transformed.transform_arguments(serde_json::json!({}));
989 assert!(result.is_err());
990 assert!(
991 result
992 .unwrap_err()
993 .message
994 .contains("Hidden argument 'q' requires a default value")
995 );
996 }
997
998 #[test]
999 fn transform_arguments_hidden_default_ignores_caller_supplied_value() {
1000 let tool = SearchToolFixture::new("search");
1001 let transformed = TransformedTool::from_tool(tool).hide_arg("n", 10).build();
1002
1003 let result = transformed
1004 .transform_arguments(serde_json::json!({"q": "hello", "n": 999}))
1005 .unwrap();
1006 let obj = result.as_object().unwrap();
1007 assert_eq!(obj.get("q").unwrap(), "hello");
1008 assert_eq!(
1009 obj.get("n").unwrap(),
1010 10,
1011 "a hidden argument must keep its server default"
1012 );
1013 }
1014
1015 #[test]
1016 fn transform_arguments_hidden_renamed_strips_both_published_and_original_names() {
1017 let tool = SearchToolFixture::new("search");
1018 let transformed = TransformedTool::from_tool(tool)
1019 .transform_arg(
1020 "n",
1021 ArgTransform::new().name("limit").default_int(10).hide(),
1022 )
1023 .build();
1024
1025 let result = transformed
1026 .transform_arguments(serde_json::json!({
1027 "q": "hello",
1028 "n": 1,
1029 "limit": 2
1030 }))
1031 .unwrap();
1032 let obj = result.as_object().unwrap();
1033 assert_eq!(obj.get("q").unwrap(), "hello");
1034 assert_eq!(obj.get("n").unwrap(), 10);
1035 assert!(
1036 obj.get("limit").is_none(),
1037 "the unpublished hidden name must not leak into parent arguments"
1038 );
1039 }
1040
1041 #[test]
1042 fn transform_arguments_hidden_without_default_rejects_caller_value() {
1043 let tool = SearchToolFixture::new("search");
1044 let transformed = TransformedTool::from_tool(tool)
1045 .transform_arg("q", ArgTransform::new().hide())
1046 .build();
1047
1048 let result = transformed.transform_arguments(serde_json::json!({"q": "injected"}));
1049 assert!(result.is_err());
1050 assert!(
1051 result
1052 .unwrap_err()
1053 .message
1054 .contains("Hidden argument 'q' requires a default value")
1055 );
1056 }
1057
1058 #[test]
1061 fn transformed_tool_call_delegates_with_mapped_args() {
1062 let tool = SearchToolFixture::new("search");
1063 let transformed = TransformedTool::from_tool(tool)
1064 .rename_arg("q", "query")
1065 .build();
1066
1067 let cx = asupersync::Cx::for_testing();
1068 let ctx = McpContext::new(cx, 1);
1069 let result = transformed
1070 .call(&ctx, serde_json::json!({"query": "hello"}))
1071 .unwrap();
1072 assert_eq!(result.len(), 1);
1073 }
1074
1075 #[test]
1076 fn transformed_tool_call_with_invalid_args_returns_error() {
1077 let tool = SearchToolFixture::new("search");
1078 let transformed = TransformedTool::from_tool(tool).build();
1079
1080 let cx = asupersync::Cx::for_testing();
1081 let ctx = McpContext::new(cx, 1);
1082 let result = transformed.call(&ctx, serde_json::json!("string_not_object"));
1083 assert!(result.is_err());
1084 }
1085
1086 #[test]
1089 fn builder_no_name_keeps_parent_name() {
1090 let tool = SearchToolFixture::new("original_name");
1091 let transformed = TransformedTool::from_tool(tool).build();
1092 assert_eq!(transformed.definition().name, "original_name");
1093 }
1094
1095 #[test]
1096 fn builder_no_description_keeps_parent_description() {
1097 let tool = SearchToolFixture::new("s");
1098 let transformed = TransformedTool::from_tool(tool).build();
1099 assert_eq!(
1100 transformed.definition().description,
1101 Some("Search tool".to_string())
1102 );
1103 }
1104
1105 #[test]
1108 fn transform_schema_applies_description_override() {
1109 let tool = SearchToolFixture::new("s");
1110 let transformed = TransformedTool::from_tool(tool)
1111 .transform_arg("q", ArgTransform::new().description("Full search query"))
1112 .build();
1113
1114 let def = transformed.definition();
1115 let q_schema = &def.input_schema["properties"]["q"];
1116 assert_eq!(q_schema["description"], "Full search query");
1117 }
1118
1119 #[test]
1122 fn not_set_debug() {
1123 let n = NotSet;
1124 let debug = format!("{:?}", n);
1125 assert!(debug.contains("NotSet"));
1126 }
1127
1128 #[test]
1129 fn not_set_clone_copy() {
1130 let n = NotSet;
1131 let cloned = n.clone();
1132 let copied = n; let _ = (cloned, copied);
1134 }
1135
1136 #[test]
1137 fn not_set_default() {
1138 let _ = NotSet;
1139 }
1140
1141 #[test]
1144 fn arg_transform_new_is_all_none() {
1145 let t = ArgTransform::new();
1146 assert!(t.name.is_none());
1147 assert!(t.description.is_none());
1148 assert!(t.default.is_none());
1149 assert!(!t.hide);
1150 assert!(t.required.is_none());
1151 assert!(t.type_schema.is_none());
1152 }
1153
1154 #[test]
1155 fn arg_transform_default_trait() {
1156 let t = <ArgTransform as Default>::default();
1157 assert!(t.name.is_none());
1158 assert!(!t.hide);
1159 }
1160
1161 #[test]
1164 fn transform_schema_replaces_malformed_properties_without_panicking() {
1165 struct MalformedSchemaTool;
1166 impl ToolHandler for MalformedSchemaTool {
1167 fn definition(&self) -> Tool {
1168 Tool {
1169 name: "malformed".to_string(),
1170 description: None,
1171 input_schema: serde_json::json!({
1172 "type": "object",
1173 "properties": true,
1174 "required": "q"
1175 }),
1176 output_schema: None,
1177 icon: None,
1178 version: None,
1179 tags: vec![],
1180 annotations: None,
1181 }
1182 }
1183 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
1184 Ok(vec![])
1185 }
1186 }
1187
1188 let transformed = TransformedTool::from_tool(MalformedSchemaTool)
1189 .hide_arg("secret", "server-owned")
1190 .build();
1191 assert_eq!(
1192 transformed.definition().input_schema["properties"],
1193 serde_json::json!({})
1194 );
1195 assert_eq!(
1196 transformed.definition().input_schema["required"],
1197 serde_json::json!([])
1198 );
1199 }
1200
1201 #[test]
1202 fn transform_schema_applies_type_override() {
1203 let tool = SearchToolFixture::new("s");
1204 let transformed = TransformedTool::from_tool(tool)
1205 .transform_arg(
1206 "q",
1207 ArgTransform::new().type_schema(serde_json::json!({"type": "number"})),
1208 )
1209 .build();
1210
1211 let def = transformed.definition();
1212 let q_schema = &def.input_schema["properties"]["q"];
1213 assert_eq!(q_schema["type"], "number");
1214 }
1215
1216 #[test]
1219 fn transform_schema_applies_default_value() {
1220 let tool = SearchToolFixture::new("s");
1221 let transformed = TransformedTool::from_tool(tool)
1222 .transform_arg("n", ArgTransform::new().default_int(25))
1223 .build();
1224
1225 let def = transformed.definition();
1226 let n_schema = &def.input_schema["properties"]["n"];
1227 assert_eq!(n_schema["default"], 25);
1228 }
1229
1230 #[test]
1233 fn transform_schema_rename_updates_required() {
1234 let tool = SearchToolFixture::new("s");
1235 let transformed = TransformedTool::from_tool(tool)
1236 .rename_arg("q", "query")
1237 .build();
1238
1239 let def = transformed.definition();
1240 let required = def.input_schema["required"].as_array().unwrap();
1241 assert!(required.iter().any(|v| v == "query"));
1242 assert!(!required.iter().any(|v| v == "q"));
1243 }
1244
1245 #[test]
1248 fn transform_schema_hide_removes_from_required() {
1249 let tool = SearchToolFixture::new("s");
1251 let transformed = TransformedTool::from_tool(tool)
1252 .hide_arg("q", "default-query")
1253 .build();
1254
1255 let def = transformed.definition();
1256 let required = def.input_schema["required"].as_array().unwrap();
1257 assert!(!required.iter().any(|v| v == "q"));
1258 }
1259
1260 #[test]
1263 fn combined_rename_description_default() {
1264 let tool = SearchToolFixture::new("search");
1265 let transformed = TransformedTool::from_tool(tool)
1266 .transform_arg(
1267 "n",
1268 ArgTransform::new()
1269 .name("limit")
1270 .description("Max results")
1271 .default_int(10),
1272 )
1273 .build();
1274
1275 let def = transformed.definition();
1276 let props = def.input_schema["properties"].as_object().unwrap();
1277 assert!(!props.contains_key("n"));
1278 let limit = props.get("limit").unwrap();
1279 assert_eq!(limit["description"], "Max results");
1280 assert_eq!(limit["default"], 10);
1281 }
1282
1283 #[test]
1286 fn build_definition_preserves_parent_output_schema() {
1287 struct ToolWithOutputSchema;
1288 impl ToolHandler for ToolWithOutputSchema {
1289 fn definition(&self) -> Tool {
1290 Tool {
1291 name: "parent".to_string(),
1292 description: None,
1293 input_schema: serde_json::json!({"type": "object"}),
1294 output_schema: Some(serde_json::json!({"type": "string"})),
1295 icon: None,
1296 version: Some("2.0".to_string()),
1297 tags: vec!["tag1".to_string()],
1298 annotations: None,
1299 }
1300 }
1301 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
1302 Ok(vec![])
1303 }
1304 }
1305
1306 let transformed = TransformedTool::from_tool(ToolWithOutputSchema)
1307 .name("child")
1308 .build();
1309 let def = transformed.definition();
1310 assert_eq!(
1311 def.output_schema,
1312 Some(serde_json::json!({"type": "string"}))
1313 );
1314 assert_eq!(def.version, Some("2.0".to_string()));
1315 assert_eq!(def.tags, vec!["tag1".to_string()]);
1316 }
1317
1318 #[test]
1321 fn transform_schema_non_object_returned_as_is() {
1322 struct ArraySchemaTool;
1323 impl ToolHandler for ArraySchemaTool {
1324 fn definition(&self) -> Tool {
1325 Tool {
1326 name: "arr".to_string(),
1327 description: None,
1328 input_schema: serde_json::json!("not an object"),
1329 output_schema: None,
1330 icon: None,
1331 version: None,
1332 tags: vec![],
1333 annotations: None,
1334 }
1335 }
1336 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
1337 Ok(vec![])
1338 }
1339 }
1340
1341 let transformed = TransformedTool::from_tool(ArraySchemaTool)
1342 .rename_arg("x", "y")
1343 .build();
1344 let def = transformed.definition();
1345 assert_eq!(def.input_schema, serde_json::json!("not an object"));
1347 }
1348
1349 #[test]
1352 fn transform_schema_adds_properties_and_required_if_missing() {
1353 struct MinimalSchemaTool;
1354 impl ToolHandler for MinimalSchemaTool {
1355 fn definition(&self) -> Tool {
1356 Tool {
1357 name: "min".to_string(),
1358 description: None,
1359 input_schema: serde_json::json!({"type": "object"}),
1360 output_schema: None,
1361 icon: None,
1362 version: None,
1363 tags: vec![],
1364 annotations: None,
1365 }
1366 }
1367 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
1368 Ok(vec![])
1369 }
1370 }
1371
1372 let transformed = TransformedTool::from_tool(MinimalSchemaTool).build();
1373 let def = transformed.definition();
1374 assert!(def.input_schema["properties"].is_object());
1375 assert!(def.input_schema["required"].is_array());
1376 }
1377
1378 #[test]
1381 fn transformed_tool_call_injects_hidden_defaults() {
1382 let tool = SearchToolFixture::new("search");
1383 let transformed = TransformedTool::from_tool(tool)
1384 .rename_arg("q", "query")
1385 .hide_arg("n", 5)
1386 .build();
1387
1388 let cx = asupersync::Cx::for_testing();
1389 let ctx = McpContext::new(cx, 1);
1390 let result = transformed
1391 .call(&ctx, serde_json::json!({"query": "test"}))
1392 .unwrap();
1393 assert_eq!(result.len(), 1);
1395 if let Content::Text { text } = &result[0] {
1396 assert!(text.contains("\"n\":5"));
1397 assert!(text.contains("\"q\":\"test\""));
1398 } else {
1399 panic!("expected text content");
1400 }
1401 }
1402
1403 #[test]
1406 fn transform_arg_with_noop_keeps_original() {
1407 let tool = SearchToolFixture::new("search");
1408 let transformed = TransformedTool::from_tool(tool)
1409 .transform_arg("q", ArgTransform::new())
1410 .build();
1411
1412 let def = transformed.definition();
1413 let props = def.input_schema["properties"].as_object().unwrap();
1414 assert!(props.contains_key("q"));
1416 }
1417
1418 #[test]
1421 fn transform_arg_for_nonexistent_arg_is_ignored() {
1422 let tool = SearchToolFixture::new("search");
1423 let transformed = TransformedTool::from_tool(tool)
1424 .rename_arg("nonexistent", "renamed")
1425 .build();
1426
1427 let def = transformed.definition();
1428 let props = def.input_schema["properties"].as_object().unwrap();
1429 assert!(props.contains_key("q"));
1431 assert!(props.contains_key("n"));
1432 assert!(!props.contains_key("renamed"));
1434 }
1435
1436 #[test]
1437 fn call_async_delegates_with_mapped_args() {
1438 use fastmcp_core::block_on;
1439
1440 let tool = SearchToolFixture::new("search");
1441 let transformed = TransformedTool::from_tool(tool)
1442 .rename_arg("q", "query")
1443 .hide_arg("n", 7)
1444 .build();
1445
1446 let cx = asupersync::Cx::for_testing();
1447 let ctx = McpContext::new(cx, 1);
1448 let result = block_on(transformed.call_async(&ctx, serde_json::json!({"query": "async"})));
1449 let content = result.unwrap();
1450 assert_eq!(content.len(), 1);
1451 if let Content::Text { text } = &content[0] {
1452 assert!(text.contains("\"q\":\"async\""));
1453 assert!(text.contains("\"n\":7"));
1454 } else {
1455 panic!("expected text content");
1456 }
1457 }
1458
1459 #[test]
1460 fn transform_arguments_no_value_no_default_not_hidden_skipped() {
1461 let tool = SearchToolFixture::new("search");
1462 let transformed = TransformedTool::from_tool(tool)
1463 .transform_arg("n", ArgTransform::new().description("ignored desc"))
1464 .build();
1465
1466 let result = transformed
1468 .transform_arguments(serde_json::json!({"q": "hello"}))
1469 .unwrap();
1470 let obj = result.as_object().unwrap();
1471 assert_eq!(obj.get("q").unwrap(), "hello");
1472 assert!(
1473 obj.get("n").is_none(),
1474 "missing arg without default should be skipped"
1475 );
1476 }
1477
1478 #[test]
1479 fn transform_arguments_default_used_without_hide() {
1480 let tool = SearchToolFixture::new("search");
1481 let transformed = TransformedTool::from_tool(tool)
1482 .transform_arg("n", ArgTransform::new().default_int(99))
1483 .build();
1484
1485 let result = transformed
1487 .transform_arguments(serde_json::json!({"q": "test"}))
1488 .unwrap();
1489 let obj = result.as_object().unwrap();
1490 assert_eq!(obj.get("n").unwrap(), 99);
1491 }
1492
1493 #[test]
1494 fn build_definition_parent_no_description_returns_none() {
1495 struct NoDescTool;
1496 impl ToolHandler for NoDescTool {
1497 fn definition(&self) -> Tool {
1498 Tool {
1499 name: "nodesc".to_string(),
1500 description: None,
1501 input_schema: serde_json::json!({"type": "object"}),
1502 output_schema: None,
1503 icon: None,
1504 version: None,
1505 tags: vec![],
1506 annotations: None,
1507 }
1508 }
1509 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
1510 Ok(vec![])
1511 }
1512 }
1513
1514 let transformed = TransformedTool::from_tool(NoDescTool).build();
1515 assert!(transformed.definition().description.is_none());
1516 }
1517
1518 #[test]
1519 fn transform_schema_preserves_unrenamed_in_required() {
1520 struct TwoReqTool;
1522 impl ToolHandler for TwoReqTool {
1523 fn definition(&self) -> Tool {
1524 Tool {
1525 name: "two".to_string(),
1526 description: None,
1527 input_schema: serde_json::json!({
1528 "type": "object",
1529 "properties": {
1530 "a": {"type": "string"},
1531 "b": {"type": "string"}
1532 },
1533 "required": ["a", "b"]
1534 }),
1535 output_schema: None,
1536 icon: None,
1537 version: None,
1538 tags: vec![],
1539 annotations: None,
1540 }
1541 }
1542 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
1543 Ok(vec![])
1544 }
1545 }
1546
1547 let transformed = TransformedTool::from_tool(TwoReqTool)
1548 .rename_arg("a", "alpha")
1549 .build();
1550 let def = transformed.definition();
1551 let required = def.input_schema["required"].as_array().unwrap();
1552 assert!(
1553 required.iter().any(|v| v == "alpha"),
1554 "renamed arg in required"
1555 );
1556 assert!(
1557 required.iter().any(|v| v == "b"),
1558 "unrenamed arg still in required"
1559 );
1560 assert!(
1561 !required.iter().any(|v| v == "a"),
1562 "old name removed from required"
1563 );
1564 }
1565
1566 #[test]
1567 fn type_schema_replaces_entire_property() {
1568 let tool = SearchToolFixture::new("s");
1569 let transformed = TransformedTool::from_tool(tool)
1570 .transform_arg(
1571 "q",
1572 ArgTransform::new()
1573 .type_schema(serde_json::json!({"type": "array", "items": {"type": "string"}})),
1574 )
1575 .build();
1576
1577 let def = transformed.definition();
1578 let q_schema = &def.input_schema["properties"]["q"];
1579 assert_eq!(q_schema["type"], "array");
1581 assert!(q_schema["items"].is_object());
1582 assert!(q_schema.get("description").is_none());
1584 }
1585
1586 struct FinalAwareParent {
1587 recorded: std::sync::Arc<std::sync::Mutex<Option<serde_json::Value>>>,
1588 resume_hook_invoked: std::sync::Arc<std::sync::Mutex<bool>>,
1589 }
1590
1591 impl FinalAwareParent {
1592 fn new() -> Self {
1593 Self {
1594 recorded: std::sync::Arc::new(std::sync::Mutex::new(None)),
1595 resume_hook_invoked: std::sync::Arc::new(std::sync::Mutex::new(false)),
1596 }
1597 }
1598
1599 fn search_schema() -> serde_json::Value {
1600 serde_json::json!({
1601 "type": "object",
1602 "properties": {
1603 "q": { "type": "string" },
1604 "n": { "type": "integer" }
1605 },
1606 "required": ["q"]
1607 })
1608 }
1609 }
1610
1611 impl ToolHandler for FinalAwareParent {
1612 fn definition(&self) -> Tool {
1613 Tool {
1614 name: "search".to_string(),
1615 description: Some("Search tool".to_string()),
1616 input_schema: Self::search_schema(),
1617 output_schema: None,
1618 icon: None,
1619 version: None,
1620 tags: vec![],
1621 annotations: None,
1622 }
1623 }
1624
1625 fn timeout(&self) -> Option<Duration> {
1626 Some(Duration::from_secs(3))
1627 }
1628
1629 fn declares_final_mrtr(&self) -> bool {
1630 true
1631 }
1632
1633 fn final_title(&self) -> Option<&str> {
1634 Some("Search")
1635 }
1636
1637 fn final_definition(&self) -> Option<FinalTool> {
1638 Some(FinalTool {
1639 name: "search".to_string(),
1640 title: Some("Search".to_string()),
1641 description: Some("Search tool".to_string()),
1642 icons: None,
1643 input_schema: Self::search_schema(),
1644 output_schema: None,
1645 annotations: None,
1646 meta: None,
1647 })
1648 }
1649
1650 fn call(&self, _ctx: &McpContext, arguments: serde_json::Value) -> McpResult<Vec<Content>> {
1651 *self
1652 .recorded
1653 .lock()
1654 .expect("final-aware parent argument log is not poisoned") = Some(arguments);
1655 Ok(vec![Content::text("legacy")])
1656 }
1657
1658 fn call_final(
1659 &self,
1660 _ctx: &McpContext,
1661 arguments: serde_json::Value,
1662 ) -> McpResult<CompleteResult<FinalCallToolResult>> {
1663 *self
1664 .recorded
1665 .lock()
1666 .expect("final-aware parent argument log is not poisoned") = Some(arguments);
1667 crate::handler::promote_legacy_tool_content(vec![Content::text("final")])
1668 }
1669
1670 fn call_final_outcome_async_resuming_in_request<'a>(
1671 &'a self,
1672 ctx: &'a McpContext,
1673 _request_cx: &'a Cx,
1674 arguments: serde_json::Value,
1675 _resume_inputs: Option<&'a MrtrCompletedInputs>,
1676 ) -> BoxFuture<'a, McpOutcome<FinalToolOutcome>> {
1677 Box::pin(async move {
1678 *self
1679 .resume_hook_invoked
1680 .lock()
1681 .expect("final-aware parent resume log is not poisoned") = true;
1682 match self.call_final(ctx, arguments) {
1683 Ok(result) => Outcome::Ok(FinalToolOutcome::Complete(result)),
1684 Err(error) => Outcome::Err(error),
1685 }
1686 })
1687 }
1688 }
1689
1690 #[test]
1691 fn transformed_tool_forwards_timeout_and_final_definition() {
1692 let transformed = TransformedTool::from_tool(FinalAwareParent::new())
1693 .name("semantic_search")
1694 .rename_arg("q", "query")
1695 .hide_arg("n", 10)
1696 .build();
1697
1698 assert_eq!(transformed.timeout(), Some(Duration::from_secs(3)));
1699 assert!(transformed.declares_final_mrtr());
1700 assert_eq!(transformed.final_title(), Some("Search"));
1701
1702 let final_definition = transformed
1703 .final_definition()
1704 .expect("parent final definition must survive the transform wrapper");
1705 assert_eq!(final_definition.name, "semantic_search");
1706 assert_eq!(final_definition.title.as_deref(), Some("Search"));
1707 let properties = final_definition.input_schema["properties"]
1708 .as_object()
1709 .expect("final input schema keeps object properties");
1710 assert!(
1711 properties.contains_key("query"),
1712 "final catalog must publish the renamed argument: {properties:?}"
1713 );
1714 assert!(
1715 !properties.contains_key("q"),
1716 "final catalog must not keep the unpublished original name: {properties:?}"
1717 );
1718 assert!(
1719 !properties.contains_key("n"),
1720 "final catalog must hide the dropped argument: {properties:?}"
1721 );
1722 }
1723
1724 #[test]
1725 fn transformed_tool_call_final_uses_parent_final_hook_with_mapped_args() {
1726 let parent = FinalAwareParent::new();
1727 let recorded = std::sync::Arc::clone(&parent.recorded);
1728 let transformed = TransformedTool::from_tool(parent)
1729 .name("semantic_search")
1730 .rename_arg("q", "query")
1731 .hide_arg("n", 10)
1732 .build();
1733
1734 let cx = asupersync::Cx::for_testing();
1735 let ctx = McpContext::new(cx, 1);
1736 let result = transformed
1737 .call_final(&ctx, serde_json::json!({"query": "test"}))
1738 .expect("transformed final call must succeed");
1739 match result.payload.content.as_slice() {
1740 [ContentBlock::Text { text, .. }] => {
1741 assert_eq!(
1742 text, "final",
1743 "TransformedTool must invoke the parent final hook instead of promoting call()"
1744 );
1745 }
1746 other => panic!("expected one final text block, got {other:?}"),
1747 }
1748 let recorded = recorded
1749 .lock()
1750 .expect("final-aware parent argument log is not poisoned")
1751 .clone()
1752 .expect("parent final hook must observe mapped arguments");
1753 assert_eq!(recorded["q"], "test");
1754 assert_eq!(recorded["n"], 10);
1755 assert!(
1756 recorded.get("query").is_none(),
1757 "parent must see original names, not the published aliases: {recorded}"
1758 );
1759 }
1760
1761 #[test]
1762 fn transformed_tool_call_final_does_not_promote_legacy_call() {
1763 let parent = FinalAwareParent::new();
1764 let transformed = TransformedTool::from_tool(parent)
1765 .rename_arg("q", "query")
1766 .build();
1767
1768 let cx = asupersync::Cx::for_testing();
1769 let ctx = McpContext::new(cx, 1);
1770 let legacy = transformed
1771 .call(&ctx, serde_json::json!({"query": "legacy-path"}))
1772 .expect("legacy call still works");
1773 match &legacy[0] {
1774 Content::Text { text, .. } => assert_eq!(text, "legacy"),
1775 other => panic!("expected legacy text content, got {other:?}"),
1776 }
1777
1778 let final_result = transformed
1779 .call_final(&ctx, serde_json::json!({"query": "final-path"}))
1780 .expect("final call still works");
1781 match final_result.payload.content.as_slice() {
1782 [ContentBlock::Text { text, .. }] => {
1783 assert_eq!(text, "final");
1784 assert_ne!(
1785 text, "legacy",
1786 "the planted dimension is which parent hook ran"
1787 );
1788 }
1789 other => panic!("expected one final text block, got {other:?}"),
1790 }
1791 }
1792
1793 #[test]
1794 fn transformed_tool_forwards_mrtr_resume_hook() {
1795 let parent = FinalAwareParent::new();
1796 let resume_hook_invoked = std::sync::Arc::clone(&parent.resume_hook_invoked);
1797 let transformed = TransformedTool::from_tool(parent)
1798 .rename_arg("q", "query")
1799 .build();
1800
1801 let cx = asupersync::Cx::for_testing();
1802 let ctx = McpContext::new(cx.clone(), 1);
1803 let outcome = block_on(transformed.call_final_outcome_async_resuming_in_request(
1804 &ctx,
1805 &cx,
1806 serde_json::json!({"query": "resume"}),
1807 None,
1808 ));
1809 match outcome {
1810 Outcome::Ok(FinalToolOutcome::Complete(result)) => {
1811 match result.payload.content.as_slice() {
1812 [ContentBlock::Text { text, .. }] => {
1813 assert_eq!(text, "final");
1814 }
1815 other => panic!("expected one final text block, got {other:?}"),
1816 }
1817 }
1818 other => panic!(
1819 "expected a complete final outcome, got {}",
1820 match other {
1821 Outcome::Ok(_) => "Ok(non-complete)",
1822 Outcome::Err(_) => "Err",
1823 Outcome::Cancelled(_) => "Cancelled",
1824 Outcome::Panicked(_) => "Panicked",
1825 }
1826 ),
1827 }
1828 assert!(
1829 *resume_hook_invoked
1830 .lock()
1831 .expect("final-aware parent resume log is not poisoned"),
1832 "TransformedTool must call the parent MRTR resume hook instead of dropping resume inputs"
1833 );
1834 }
1835}