1use proc_macro::TokenStream;
2use quote::{format_ident, quote};
3use syn::parse::{Parse, ParseStream};
4use syn::punctuated::Punctuated;
5use syn::token::Comma;
6use syn::{ItemFn, Lit, Meta, parse_macro_input};
7
8struct MacroArgs {
10 name: Option<String>,
12 description: Option<String>,
14 categories: Vec<String>,
16 streaming: Option<bool>,
18 version: Option<String>,
20 author: Option<String>,
22 docs: Option<String>,
24}
25
26impl Parse for MacroArgs {
27 fn parse(input: ParseStream) -> syn::Result<Self> {
28 let args = Punctuated::<Meta, Comma>::parse_terminated(input)?;
29 let mut name = None;
30 let mut description = None;
31 let mut categories = Vec::new();
32 let mut streaming = None;
33 let mut version = None;
34 let mut author = None;
35 let mut docs = None;
36
37 for arg in args {
38 if let Meta::NameValue(nv) = arg {
39 let key = nv.path.get_ident().unwrap().to_string();
40 if let Lit::Str(lit) = nv.lit {
41 let value = lit.value();
42 match key.as_str() {
43 "name" => name = Some(value),
44 "description" => description = Some(value),
45 "version" => version = Some(value),
46 "author" => author = Some(value),
47 "docs" => docs = Some(value),
48 "categories" => {
49 for cat in value.split(',') {
50 categories.push(cat.trim().to_string());
51 }
52 }
53 _ => {}
54 }
55 } else if let Lit::Bool(b) = nv.lit {
56 if key == "streaming" {
57 streaming = Some(b.value);
58 }
59 }
60 }
61 }
62
63 Ok(MacroArgs {
64 name,
65 description,
66 categories,
67 streaming,
68 version,
69 author,
70 docs,
71 })
72 }
73}
74
75#[proc_macro_attribute]
100pub fn mcp_tool(attr: TokenStream, item: TokenStream) -> TokenStream {
101 let input_fn = parse_macro_input!(item as ItemFn);
103 let fn_name = &input_fn.sig.ident;
104
105 let args = syn::parse_macro_input!(attr as MacroArgs);
107
108 let tool_name = args.name.unwrap_or_else(|| fn_name.to_string());
110
111 let tool_description = args
113 .description
114 .unwrap_or_else(|| format!("MCP tool: {tool_name}"));
115
116 let struct_name = format_ident!("{}Tool", fn_name);
118
119 let categories = &args.categories;
121 let categories_expr = if categories.is_empty() {
122 quote! { vec![] }
123 } else {
124 let category_strings = categories.iter().map(|c| c.as_str());
125 quote! { vec![#(#category_strings.to_string()),*] }
126 };
127
128 let streaming = args.streaming.unwrap_or(false);
130
131 let version_expr = if let Some(v) = args.version {
133 quote! { Some(#v.to_string()) }
134 } else {
135 quote! { None }
136 };
137
138 let author_expr = if let Some(a) = args.author {
139 quote! { Some(#a.to_string()) }
140 } else {
141 quote! { None }
142 };
143
144 let docs_expr = if let Some(d) = args.docs {
145 quote! { Some(#d.to_string()) }
146 } else {
147 quote! { None }
148 };
149
150 let output = quote! {
152 #input_fn
154
155 #[derive(Debug)]
157 struct #struct_name;
158
159 #[async_trait::async_trait]
160 impl ::fastmcp::Tool for #struct_name {
161 fn name(&self) -> &str {
162 #tool_name
163 }
164
165 fn description(&self) -> &str {
166 #tool_description
167 }
168
169 fn parameters(&self) -> serde_json::Value {
170 serde_json::json!({
173 "type": "object",
174 "properties": {}
175 })
176 }
177
178 fn streaming(&self) -> bool {
179 #streaming
180 }
181
182 fn categories(&self) -> Vec<String> {
183 #categories_expr
184 }
185
186 fn version(&self) -> Option<String> {
187 #version_expr
188 }
189
190 fn author(&self) -> Option<String> {
191 #author_expr
192 }
193
194 fn documentation_url(&self) -> Option<String> {
195 #docs_expr
196 }
197
198 async fn execute(&self, params: serde_json::Value, context: std::sync::Arc<::fastmcp::ToolContext>) -> ::fastmcp::Result<serde_json::Value> {
199 let result = #fn_name().await;
203
204 Ok(serde_json::json!({
207 "result": format!("{:?}", result)
208 }))
209 }
210 }
211 };
212
213 output.into()
214}