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.