Skip to content

Commit f8bb8f2

Browse files
authored
feat: concat support for mcp_tool macro (#23)
1 parent 49f80b8 commit f8bb8f2

File tree

1 file changed

+83
-43
lines changed
  • crates/rust-mcp-macros/src

1 file changed

+83
-43
lines changed

crates/rust-mcp-macros/src/lib.rs

+83-43
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,20 @@ struct McpToolMacroAttributes {
2424
description: Option<String>,
2525
}
2626

27+
use syn::parse::ParseStream;
28+
29+
struct ExprList {
30+
exprs: Punctuated<Expr, Token![,]>,
31+
}
32+
33+
impl Parse for ExprList {
34+
fn parse(input: ParseStream) -> syn::Result<Self> {
35+
Ok(ExprList {
36+
exprs: Punctuated::parse_terminated(input)?,
37+
})
38+
}
39+
}
40+
2741
impl Parse for McpToolMacroAttributes {
2842
/// Parses the macro attributes from a `ParseStream`.
2943
///
@@ -41,51 +55,77 @@ impl Parse for McpToolMacroAttributes {
4155
for meta in meta_list {
4256
if let Meta::NameValue(meta_name_value) = meta {
4357
let ident = meta_name_value.path.get_ident().unwrap();
44-
if let Expr::Lit(ExprLit {
45-
lit: Lit::Str(lit_str),
46-
..
47-
}) = meta_name_value.value
48-
{
49-
match ident.to_string().as_str() {
50-
"name" => name = Some(lit_str.value()),
51-
"description" => description = Some(lit_str.value()),
52-
_ => {}
58+
let ident_str = ident.to_string();
59+
60+
let value = match &meta_name_value.value {
61+
Expr::Lit(ExprLit {
62+
lit: Lit::Str(lit_str),
63+
..
64+
}) => lit_str.value(),
65+
66+
Expr::Macro(expr_macro) => {
67+
let mac = &expr_macro.mac;
68+
if mac.path.is_ident("concat") {
69+
let args: ExprList = syn::parse2(mac.tokens.clone())?;
70+
let mut result = String::new();
71+
72+
for expr in args.exprs {
73+
if let Expr::Lit(ExprLit {
74+
lit: Lit::Str(lit_str),
75+
..
76+
}) = expr
77+
{
78+
result.push_str(&lit_str.value());
79+
} else {
80+
return Err(Error::new_spanned(
81+
expr,
82+
"Only string literals are allowed inside concat!()",
83+
));
84+
}
85+
}
86+
87+
result
88+
} else {
89+
return Err(Error::new_spanned(
90+
expr_macro,
91+
"Only concat!(...) is supported here",
92+
));
93+
}
94+
}
95+
96+
_ => {
97+
return Err(Error::new_spanned(
98+
&meta_name_value.value,
99+
"Expected a string literal or concat!(...)",
100+
));
53101
}
102+
};
103+
104+
match ident_str.as_str() {
105+
"name" => name = Some(value),
106+
"description" => description = Some(value),
107+
_ => {}
54108
}
55109
}
56110
}
57-
match &name {
58-
Some(tool_name) => {
59-
if tool_name.trim().is_empty() {
60-
return Err(Error::new(
61-
attributes.span(),
62-
"The 'name' attribute should not be an empty string.",
63-
));
64-
}
65-
}
66-
None => {
67-
return Err(Error::new(
68-
attributes.span(),
69-
"The 'name' attribute is required.",
70-
));
71-
}
111+
112+
// Validate presence and non-emptiness
113+
if name.as_ref().map(|s| s.trim().is_empty()).unwrap_or(true) {
114+
return Err(Error::new(
115+
attributes.span(),
116+
"The 'name' attribute is required and must not be empty.",
117+
));
72118
}
73119

74-
match &description {
75-
Some(description) => {
76-
if description.trim().is_empty() {
77-
return Err(Error::new(
78-
attributes.span(),
79-
"The 'description' attribute should not be an empty string.",
80-
));
81-
}
82-
}
83-
None => {
84-
return Err(Error::new(
85-
attributes.span(),
86-
"The 'description' attribute is required.",
87-
));
88-
}
120+
if description
121+
.as_ref()
122+
.map(|s| s.trim().is_empty())
123+
.unwrap_or(true)
124+
{
125+
return Err(Error::new(
126+
attributes.span(),
127+
"The 'description' attribute is required and must not be empty.",
128+
));
89129
}
90130

91131
Ok(Self { name, description })
@@ -360,7 +400,7 @@ mod tests {
360400
assert!(result.is_err());
361401
assert_eq!(
362402
result.err().unwrap().to_string(),
363-
"The 'name' attribute is required."
403+
"The 'name' attribute is required and must not be empty."
364404
)
365405
}
366406

@@ -371,7 +411,7 @@ mod tests {
371411
assert!(result.is_err());
372412
assert_eq!(
373413
result.err().unwrap().to_string(),
374-
"The 'description' attribute is required."
414+
"The 'description' attribute is required and must not be empty."
375415
)
376416
}
377417

@@ -382,7 +422,7 @@ mod tests {
382422
assert!(result.is_err());
383423
assert_eq!(
384424
result.err().unwrap().to_string(),
385-
"The 'name' attribute should not be an empty string."
425+
"The 'name' attribute is required and must not be empty."
386426
);
387427
}
388428
#[test]
@@ -392,7 +432,7 @@ mod tests {
392432
assert!(result.is_err());
393433
assert_eq!(
394434
result.err().unwrap().to_string(),
395-
"The 'description' attribute should not be an empty string."
435+
"The 'description' attribute is required and must not be empty."
396436
);
397437
}
398438
}

0 commit comments

Comments
 (0)