From 48a4b214d969c680ed69b59242deb0c0a8924469 Mon Sep 17 00:00:00 2001 From: Sandipsinh Rathod Date: Thu, 17 Apr 2025 14:11:31 -0400 Subject: [PATCH 01/11] feat: generate description from docs --- crates/rmcp-macros/src/tool.rs | 79 +++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/crates/rmcp-macros/src/tool.rs b/crates/rmcp-macros/src/tool.rs index ab5a74da6..eef6d8a10 100644 --- a/crates/rmcp-macros/src/tool.rs +++ b/crates/rmcp-macros/src/tool.rs @@ -405,10 +405,36 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu // generate get tool attr function let tool_attr_fn = { let description = if let Some(expr) = tool_macro_attrs.fn_item.description { + // Use explicitly provided description if available expr } else { - parse_quote! { - "" + // Try to extract documentation comments + let mut doc_content = String::new(); + + for attr in &input_fn.attrs { + if attr.path().is_ident("doc") { + if let Ok(lit) = attr.parse_args::() { + let doc_line = lit.value(); + if !doc_content.is_empty() { + doc_content.push_str("\n"); + } + doc_content.push_str(doc_line.trim()); + } + } + } + + if !doc_content.is_empty() { + // Use documentation comments if available + let doc_str = doc_content.trim().to_string(); + // Convert the string to a string literal expression + parse_quote! { + #doc_str + } + } else { + // Fall back to empty string if no description is found + parse_quote! { + "" + } } }; let schema = match &tool_macro_attrs.params { @@ -657,4 +683,53 @@ mod test { println!("input: {:#}", input); Ok(()) } + #[test] + fn test_doc_comment_description() -> syn::Result<()> { + let attr = quote! {}; // No explicit description + let input = quote! { + /// This is a test description from doc comments + /// with multiple lines + fn test_function(&self) -> Result<(), Error> { + Ok(()) + } + }; + let result = tool(attr, input)?; + + // The output should contain the description from doc comments + let result_str = result.to_string(); + assert!(result_str.contains("This is a test description from doc comments")); + assert!(result_str.contains("with multiple lines")); + + Ok(()) + } + #[test] + fn test_explicit_description_priority() -> syn::Result<()> { + let attr = quote! { + description = "Explicit description has priority" + }; + let input = quote! { + /// Doc comment description that should be ignored + fn test_function(&self) -> Result<(), Error> { + Ok(()) + } + }; + let result = tool(attr, input)?; + + // The output should contain the explicit description + let result_str = result.to_string(); + assert!(result_str.contains("Explicit description has priority")); + + // Check that in the description = ... part we don't have the doc comment text + let tool_attr_fn_part = result_str + .split("fn test_function_tool_attr") + .nth(1) + .unwrap_or(""); + let description_part = tool_attr_fn_part + .split("description: Some") + .nth(1) + .unwrap_or(""); + assert!(!description_part.contains("Doc comment description that should be ignored")); + + Ok(()) + } } From 438cdae2fbea167f0a168096136aedbc0b56eb8c Mon Sep 17 00:00:00 2001 From: Sandipsinh Rathod Date: Thu, 17 Apr 2025 15:27:39 -0400 Subject: [PATCH 02/11] fix logic to extract doc --- Cargo.toml | 2 +- crates/rmcp-macros/src/tool.rs | 28 +++++++++++----------------- crates/rmcp/Cargo.toml | 2 +- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 317e479bc..1f0848794 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ resolver = "2" [workspace.dependencies] rmcp = { version = "0.1.5", path = "./crates/rmcp" } -rmcp-macros = { version = "0.1.5", path = "./crates/rmcp-macros" } +rmcp-macros = { path = "./crates/rmcp-macros" } [workspace.package] edition = "2024" diff --git a/crates/rmcp-macros/src/tool.rs b/crates/rmcp-macros/src/tool.rs index eef6d8a10..e7014fba5 100644 --- a/crates/rmcp-macros/src/tool.rs +++ b/crates/rmcp-macros/src/tool.rs @@ -413,28 +413,22 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu for attr in &input_fn.attrs { if attr.path().is_ident("doc") { - if let Ok(lit) = attr.parse_args::() { - let doc_line = lit.value(); - if !doc_content.is_empty() { - doc_content.push_str("\n"); + if let syn::Meta::NameValue(name_value) = &attr.meta { + if let syn::Expr::Lit(expr_lit) = &name_value.value { + if let syn::Lit::Str(lit_str) = &expr_lit.lit { + let doc_line = lit_str.value(); + if !doc_content.is_empty() { + doc_content.push_str("\n"); + } + doc_content.push_str(doc_line.trim()); + } } - doc_content.push_str(doc_line.trim()); } } } - if !doc_content.is_empty() { - // Use documentation comments if available - let doc_str = doc_content.trim().to_string(); - // Convert the string to a string literal expression - parse_quote! { - #doc_str - } - } else { - // Fall back to empty string if no description is found - parse_quote! { - "" - } + parse_quote! { + #doc_content.trim().to_string() } }; let schema = match &tool_macro_attrs.params { diff --git a/crates/rmcp/Cargo.toml b/crates/rmcp/Cargo.toml index 738f24462..56242b3ad 100644 --- a/crates/rmcp/Cargo.toml +++ b/crates/rmcp/Cargo.toml @@ -51,7 +51,7 @@ rand = { version = "0.9", optional = true } tokio-stream = { version = "0.1", optional = true } # macro -rmcp-macros = { version = "0.1", workspace = true, optional = true } +rmcp-macros = { workspace = true, optional = true } [features] default = ["base64", "macros", "server"] From 10abe804c02c448e5c523f94078d96164a7c2c42 Mon Sep 17 00:00:00 2001 From: Sandipsinh Rathod Date: Thu, 17 Apr 2025 15:30:40 -0400 Subject: [PATCH 03/11] fmt --- crates/rmcp-macros/src/tool.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rmcp-macros/src/tool.rs b/crates/rmcp-macros/src/tool.rs index e7014fba5..388516953 100644 --- a/crates/rmcp-macros/src/tool.rs +++ b/crates/rmcp-macros/src/tool.rs @@ -418,7 +418,7 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu if let syn::Lit::Str(lit_str) = &expr_lit.lit { let doc_line = lit_str.value(); if !doc_content.is_empty() { - doc_content.push_str("\n"); + doc_content.push('\n'); } doc_content.push_str(doc_line.trim()); } From 7f8f49a54ce77aaecea17cdb517870f1e448ed02 Mon Sep 17 00:00:00 2001 From: Sandipsinh Rathod Date: Thu, 17 Apr 2025 15:36:55 -0400 Subject: [PATCH 04/11] chore: undo unnecessary changes in `Cargo.toml` --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1f0848794..317e479bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ resolver = "2" [workspace.dependencies] rmcp = { version = "0.1.5", path = "./crates/rmcp" } -rmcp-macros = { path = "./crates/rmcp-macros" } +rmcp-macros = { version = "0.1.5", path = "./crates/rmcp-macros" } [workspace.package] edition = "2024" From 3caa575ece338cbba9096d4c7e3780a76d13a2b8 Mon Sep 17 00:00:00 2001 From: Sandipsinh Rathod Date: Thu, 17 Apr 2025 15:37:35 -0400 Subject: [PATCH 05/11] chore: undo unnecessary changes in `Cargo.toml` --- crates/rmcp/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rmcp/Cargo.toml b/crates/rmcp/Cargo.toml index 56242b3ad..738f24462 100644 --- a/crates/rmcp/Cargo.toml +++ b/crates/rmcp/Cargo.toml @@ -51,7 +51,7 @@ rand = { version = "0.9", optional = true } tokio-stream = { version = "0.1", optional = true } # macro -rmcp-macros = { workspace = true, optional = true } +rmcp-macros = { version = "0.1", workspace = true, optional = true } [features] default = ["base64", "macros", "server"] From a0deaa66114181eedf4b3e16000bf1b4dc6d69c6 Mon Sep 17 00:00:00 2001 From: Sandipsinh Rathod Date: Thu, 17 Apr 2025 15:38:57 -0400 Subject: [PATCH 06/11] chore: remove unnecessary code in tests --- crates/rmcp-macros/src/tool.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/crates/rmcp-macros/src/tool.rs b/crates/rmcp-macros/src/tool.rs index 388516953..44d6fe372 100644 --- a/crates/rmcp-macros/src/tool.rs +++ b/crates/rmcp-macros/src/tool.rs @@ -712,18 +712,6 @@ mod test { // The output should contain the explicit description let result_str = result.to_string(); assert!(result_str.contains("Explicit description has priority")); - - // Check that in the description = ... part we don't have the doc comment text - let tool_attr_fn_part = result_str - .split("fn test_function_tool_attr") - .nth(1) - .unwrap_or(""); - let description_part = tool_attr_fn_part - .split("description: Some") - .nth(1) - .unwrap_or(""); - assert!(!description_part.contains("Doc comment description that should be ignored")); - Ok(()) } } From 78265a12d576542cb7a360320976fa882240c48d Mon Sep 17 00:00:00 2001 From: Sandipsinh Rathod Date: Thu, 17 Apr 2025 21:27:03 -0400 Subject: [PATCH 07/11] create `extract_doc_line` --- crates/rmcp-macros/src/tool.rs | 35 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/crates/rmcp-macros/src/tool.rs b/crates/rmcp-macros/src/tool.rs index 44d6fe372..47b4c6472 100644 --- a/crates/rmcp-macros/src/tool.rs +++ b/crates/rmcp-macros/src/tool.rs @@ -296,6 +296,19 @@ pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Res }) } +fn extract_doc_line(attr: &syn::Attribute) -> Option { + if attr.path().is_ident("doc") { + if let syn::Meta::NameValue(name_value) = &attr.meta { + if let syn::Expr::Lit(expr_lit) = &name_value.value { + if let syn::Lit::Str(lit_str) = &expr_lit.lit { + return Some(lit_str.value().trim().to_string()); + } + } + } + } + None +} + pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Result { let mut tool_macro_attrs = ToolAttrs::default(); let args: ToolFnItemAttrs = syn::parse2(attr)?; @@ -409,23 +422,11 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu expr } else { // Try to extract documentation comments - let mut doc_content = String::new(); - - for attr in &input_fn.attrs { - if attr.path().is_ident("doc") { - if let syn::Meta::NameValue(name_value) = &attr.meta { - if let syn::Expr::Lit(expr_lit) = &name_value.value { - if let syn::Lit::Str(lit_str) = &expr_lit.lit { - let doc_line = lit_str.value(); - if !doc_content.is_empty() { - doc_content.push('\n'); - } - doc_content.push_str(doc_line.trim()); - } - } - } - } - } + let doc_content = input_fn.attrs + .iter() + .filter_map(extract_doc_line) + .collect::>() + .join("\n"); parse_quote! { #doc_content.trim().to_string() From c278da104c7f5010c65e491237b851ac57273155 Mon Sep 17 00:00:00 2001 From: Sandipsinh Rathod Date: Thu, 17 Apr 2025 21:28:43 -0400 Subject: [PATCH 08/11] fmt --- crates/rmcp-macros/src/tool.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/rmcp-macros/src/tool.rs b/crates/rmcp-macros/src/tool.rs index 47b4c6472..21d2d947f 100644 --- a/crates/rmcp-macros/src/tool.rs +++ b/crates/rmcp-macros/src/tool.rs @@ -422,7 +422,8 @@ pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Resu expr } else { // Try to extract documentation comments - let doc_content = input_fn.attrs + let doc_content = input_fn + .attrs .iter() .filter_map(extract_doc_line) .collect::>() From 587e1f953891204d3cd8bd677257cfc7fed67642 Mon Sep 17 00:00:00 2001 From: Sandipsinh Rathod Date: Thu, 17 Apr 2025 21:36:52 -0400 Subject: [PATCH 09/11] make sure the string is not empty --- crates/rmcp-macros/src/tool.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/rmcp-macros/src/tool.rs b/crates/rmcp-macros/src/tool.rs index 21d2d947f..6c8afbe0a 100644 --- a/crates/rmcp-macros/src/tool.rs +++ b/crates/rmcp-macros/src/tool.rs @@ -301,7 +301,11 @@ fn extract_doc_line(attr: &syn::Attribute) -> Option { if let syn::Meta::NameValue(name_value) = &attr.meta { if let syn::Expr::Lit(expr_lit) = &name_value.value { if let syn::Lit::Str(lit_str) = &expr_lit.lit { - return Some(lit_str.value().trim().to_string()); + let str = lit_str.value().trim().to_string(); + if str.is_empty() { + return None; + } + return Some(str); } } } From b196f1e90969b0f46f58497085fa743f093abb45 Mon Sep 17 00:00:00 2001 From: Sandipsinh Rathod Date: Thu, 17 Apr 2025 21:59:30 -0400 Subject: [PATCH 10/11] avoid multilayer nesting --- crates/rmcp-macros/src/tool.rs | 38 ++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/crates/rmcp-macros/src/tool.rs b/crates/rmcp-macros/src/tool.rs index 6c8afbe0a..379016b93 100644 --- a/crates/rmcp-macros/src/tool.rs +++ b/crates/rmcp-macros/src/tool.rs @@ -297,20 +297,32 @@ pub(crate) fn tool_impl_item(attr: TokenStream, mut input: ItemImpl) -> syn::Res } fn extract_doc_line(attr: &syn::Attribute) -> Option { - if attr.path().is_ident("doc") { - if let syn::Meta::NameValue(name_value) = &attr.meta { - if let syn::Expr::Lit(expr_lit) = &name_value.value { - if let syn::Lit::Str(lit_str) = &expr_lit.lit { - let str = lit_str.value().trim().to_string(); - if str.is_empty() { - return None; - } - return Some(str); - } - } - } + if !attr.path().is_ident("doc") { + return None; + } + + let name_value = match &attr.meta { + syn::Meta::NameValue(nv) => nv, + _ => return None, + }; + + let expr_lit = match &name_value.value { + syn::Expr::Lit(lit) => lit, + _ => return None, + }; + + let lit_str = match &expr_lit.lit { + syn::Lit::Str(s) => s, + _ => return None, + }; + + let content = lit_str.value().trim().to_string(); + + if content.is_empty() { + None + } else { + Some(content) } - None } pub(crate) fn tool_fn_item(attr: TokenStream, mut input_fn: ItemFn) -> syn::Result { From c99d4173c1b1683c91306fdd6066ac98cc7b50a2 Mon Sep 17 00:00:00 2001 From: Sandipsinh Rathod Date: Thu, 17 Apr 2025 21:59:56 -0400 Subject: [PATCH 11/11] fmt --- crates/rmcp-macros/src/tool.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/rmcp-macros/src/tool.rs b/crates/rmcp-macros/src/tool.rs index 379016b93..40b0aecae 100644 --- a/crates/rmcp-macros/src/tool.rs +++ b/crates/rmcp-macros/src/tool.rs @@ -300,24 +300,24 @@ fn extract_doc_line(attr: &syn::Attribute) -> Option { if !attr.path().is_ident("doc") { return None; } - + let name_value = match &attr.meta { syn::Meta::NameValue(nv) => nv, _ => return None, }; - + let expr_lit = match &name_value.value { syn::Expr::Lit(lit) => lit, _ => return None, }; - + let lit_str = match &expr_lit.lit { syn::Lit::Str(s) => s, _ => return None, }; - + let content = lit_str.value().trim().to_string(); - + if content.is_empty() { None } else {