From 2d8a8eb610b2381b11618cf7d5166a60612d632b Mon Sep 17 00:00:00 2001 From: Atirna <288419661+atirna@users.noreply.github.com> Date: Thu, 3 Sep 2026 10:24:52 +0530 Subject: [PATCH] fix(macros): accept const paths and concat! in tool/prompt descriptions The description field of #[tool] and #[prompt] only accepted a bare string literal, while #[doc] on the same item accepts include_str! and #[schemars] on the argument struct accepts const paths. Parse the field as an expression through darling's PreservedStrExpr, which keeps a string literal a literal, so a const path or concat! now works too. Fixes #1175 --- crates/rmcp-macros/src/prompt.rs | 15 +++++++-------- crates/rmcp-macros/src/tool.rs | 19 +++++++++---------- crates/rmcp/tests/test_prompt_macros.rs | 17 +++++++++++++++++ crates/rmcp/tests/test_tool_macros.rs | 18 ++++++++++++++++++ 4 files changed, 51 insertions(+), 18 deletions(-) diff --git a/crates/rmcp-macros/src/prompt.rs b/crates/rmcp-macros/src/prompt.rs index ea35bc18c..3b3a9a40f 100644 --- a/crates/rmcp-macros/src/prompt.rs +++ b/crates/rmcp-macros/src/prompt.rs @@ -1,5 +1,5 @@ use darling::{FromMeta, ast::NestedMeta}; -use proc_macro2::{Span, TokenStream}; +use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{Expr, Ident, ImplItemFn, ReturnType}; @@ -12,8 +12,10 @@ pub struct PromptAttribute { pub name: Option, /// Human readable title of prompt pub title: Option, - /// Optional description of what the prompt does - pub description: Option, + /// Optional description of what the prompt does. A string literal, an + /// expression that evaluates to a `&'static str` (such as a `const` path + /// or `concat!`), or a `#[doc]` attribute on the function. + pub description: Option, /// Arguments that can be passed to the prompt pub arguments: Option, /// Optional icons for the prompt @@ -104,11 +106,8 @@ pub fn prompt(attr: TokenStream, input: TokenStream) -> syn::Result }; let name = attribute.name.unwrap_or_else(|| fn_ident.to_string()); - let description = if let Some(s) = attribute.description { - Some(Expr::Lit(syn::ExprLit { - attrs: Vec::new(), - lit: syn::Lit::Str(syn::LitStr::new(&s, Span::call_site())), - })) + let description = if let Some(description) = attribute.description { + Some(description.into()) } else { fn_item.attrs.iter().try_fold(None, extract_doc_line)? }; diff --git a/crates/rmcp-macros/src/tool.rs b/crates/rmcp-macros/src/tool.rs index c640dd5cf..ad88e86fb 100644 --- a/crates/rmcp-macros/src/tool.rs +++ b/crates/rmcp-macros/src/tool.rs @@ -1,7 +1,7 @@ use darling::{FromMeta, ast::NestedMeta}; -use proc_macro2::{Span, TokenStream}; +use proc_macro2::TokenStream; use quote::{ToTokens, format_ident, quote}; -use syn::{Expr, Ident, ImplItemFn, LitStr, ReturnType, parse_quote}; +use syn::{Expr, Ident, ImplItemFn, ReturnType, parse_quote}; use crate::common::extract_doc_line; @@ -65,7 +65,10 @@ pub struct ToolAttribute { pub name: Option, /// Human readable title of tool pub title: Option, - pub description: Option, + /// The description of the tool. A string literal, an expression that + /// evaluates to a `&'static str` (such as a `const` path or `concat!`), + /// or a `#[doc]` attribute on the function. + pub description: Option, /// A JSON Schema object defining the expected parameters for the tool pub input_schema: Option, /// An optional JSON Schema object defining the structure of the tool's output @@ -255,13 +258,9 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result { } }); - let description_expr = if let Some(s) = attribute.description { - Some(Expr::Lit(syn::ExprLit { - attrs: Vec::new(), - lit: syn::Lit::Str(LitStr::new(&s, Span::call_site())), - })) - } else { - fn_item.attrs.iter().try_fold(None, extract_doc_line)? + let description_expr = match attribute.description { + Some(description) => Some(description.into()), + None => fn_item.attrs.iter().try_fold(None, extract_doc_line)?, }; let resolved_tool_attr = ResolvedToolAttribute { name: attribute.name.unwrap_or_else(|| fn_ident.to_string()), diff --git a/crates/rmcp/tests/test_prompt_macros.rs b/crates/rmcp/tests/test_prompt_macros.rs index 7a00249a4..bbc8cb522 100644 --- a/crates/rmcp/tests/test_prompt_macros.rs +++ b/crates/rmcp/tests/test_prompt_macros.rs @@ -73,8 +73,15 @@ impl Server { "This is a prompt with no parameters.".to_string(), )] } + + #[prompt(description = CONST_PROMPT_DESCRIPTION)] + async fn const_description_prompt(&self) -> Vec { + vec![] + } } +const CONST_PROMPT_DESCRIPTION: &str = "Prompt description from a const"; + // define generic service trait pub trait DataService: Send + Sync + 'static { fn get_context(&self) -> String; @@ -155,6 +162,16 @@ async fn test_prompt_macros_with_empty_param() { ); } +#[test] +fn test_prompt_description_accepts_const_expr() { + // tests https://github.com/modelcontextprotocol/rust-sdk/issues/1175 + let prompt = Server::const_description_prompt_prompt_attr(); + assert_eq!( + prompt.description.as_deref(), + Some(CONST_PROMPT_DESCRIPTION) + ); +} + #[tokio::test] async fn test_prompt_macros_with_generics() { let mock_service = MockDataService; diff --git a/crates/rmcp/tests/test_tool_macros.rs b/crates/rmcp/tests/test_tool_macros.rs index 9b9530aa8..784014261 100644 --- a/crates/rmcp/tests/test_tool_macros.rs +++ b/crates/rmcp/tests/test_tool_macros.rs @@ -61,6 +61,24 @@ impl Server { #[tool] async fn empty_param(&self) {} + + #[tool(description = CONST_TOOL_DESCRIPTION)] + async fn const_description_tool(&self) {} + + #[tool(description = concat!("part-a-", "part-b"))] + async fn concat_description_tool(&self) {} +} + +const CONST_TOOL_DESCRIPTION: &str = "Description from a const"; + +#[test] +fn test_description_accepts_const_and_concat_exprs() { + // tests https://github.com/modelcontextprotocol/rust-sdk/issues/1175 + let tool = Server::const_description_tool_tool_attr(); + assert_eq!(tool.description.as_deref(), Some(CONST_TOOL_DESCRIPTION)); + + let tool = Server::concat_description_tool_tool_attr(); + assert_eq!(tool.description.as_deref(), Some("part-a-part-b")); } /// Generic service trait.