From a20edc07035269b01d9aea4e484a71247ea9ad5b Mon Sep 17 00:00:00 2001 From: Dale Seo <5466341+DaleSeo@users.noreply.github.com> Date: Thu, 12 Mar 2026 19:13:07 -0400 Subject: [PATCH] feat: add send attribute to tool macro --- crates/rmcp-macros/src/tool.rs | 55 ++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/crates/rmcp-macros/src/tool.rs b/crates/rmcp-macros/src/tool.rs index 56bf65a14..a74343154 100644 --- a/crates/rmcp-macros/src/tool.rs +++ b/crates/rmcp-macros/src/tool.rs @@ -95,6 +95,10 @@ pub struct ToolAttribute { pub icons: Option, /// Optional metadata for the tool pub meta: Option, + /// Whether the generated future should be `Send`. Defaults to `true`. + /// Set to `false` for tools that hold non-Send state (e.g., `Rc`, `RefCell`). + /// Note: tools with `send = false` are incompatible with the built-in tool router. + pub send: Option, } #[derive(FromMeta, Debug, Default)] @@ -330,9 +334,10 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result { }; let tool_attr_fn = resolved_tool_attr.into_fn(tool_attr_fn_ident)?; // modify the the input function + let is_send = attribute.send.unwrap_or(true); if fn_item.sig.asyncness.is_some() { // 1. remove asyncness from sig - // 2. make return type: `std::pin::Pin + Send + '_>>` + // 2. make return type: `std::pin::Pin (+ Send)? + '_>>` // 3. make body: { Box::pin(async move { #body }) } let new_output = syn::parse2::({ let mut lt = quote! { 'static }; @@ -345,12 +350,17 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result { } } } + let send_bound = if is_send { + quote! { Send + } + } else { + quote! {} + }; match &fn_item.sig.output { syn::ReturnType::Default => { - quote! { -> ::std::pin::Pin + Send + #lt>> } + quote! { -> ::std::pin::Pin + #send_bound #lt>> } } syn::ReturnType::Type(_, ty) => { - quote! { -> ::std::pin::Pin + Send + #lt>> } + quote! { -> ::std::pin::Pin + #send_bound #lt>> } } } })?; @@ -443,4 +453,43 @@ mod test { assert!(result_str.contains("include_str")); Ok(()) } + + #[test] + fn async_tool_future_includes_send_bound_by_default() -> syn::Result<()> { + let attr = quote! {}; + let input = quote! { + async fn my_tool(&self) -> String { + "hello".to_string() + } + }; + let result = tool(attr, input)?; + assert!(result.to_string().contains("Send")); + Ok(()) + } + + #[test] + fn async_tool_future_omits_send_bound_when_send_is_false() -> syn::Result<()> { + let attr = quote! { send = false }; + let input = quote! { + async fn my_tool(&self) -> String { + "hello".to_string() + } + }; + let result = tool(attr, input)?; + assert!(!result.to_string().contains("Send")); + Ok(()) + } + + #[test] + fn async_tool_future_includes_send_bound_when_send_is_true() -> syn::Result<()> { + let attr = quote! { send = true }; + let input = quote! { + async fn my_tool(&self) -> String { + "hello".to_string() + } + }; + let result = tool(attr, input)?; + assert!(result.to_string().contains("Send")); + Ok(()) + } }