1extern crate proc_macro;
2
3mod utils;
4
5use proc_macro::TokenStream;
6use quote::quote;
7use syn::{
8 parse::Parse, parse_macro_input, punctuated::Punctuated, Data, DeriveInput, Error, Expr,
9 ExprLit, Fields, Lit, Meta, Token,
10};
11use utils::{is_option, renamed_field, type_to_json_schema};
12
13#[cfg(feature = "2024_11_05")]
23struct McpToolMacroAttributes {
24 name: Option<String>,
25 description: Option<String>,
26}
27
28#[cfg(feature = "2025_03_26")]
43struct McpToolMacroAttributes {
44 name: Option<String>,
45 description: Option<String>,
46 destructive_hint: Option<bool>,
47 idempotent_hint: Option<bool>,
48 open_world_hint: Option<bool>,
49 read_only_hint: Option<bool>,
50 title: Option<String>,
51}
52
53use syn::parse::ParseStream;
54
55struct ExprList {
56 exprs: Punctuated<Expr, Token![,]>,
57}
58
59impl Parse for ExprList {
60 fn parse(input: ParseStream) -> syn::Result<Self> {
61 Ok(ExprList {
62 exprs: Punctuated::parse_terminated(input)?,
63 })
64 }
65}
66
67impl Parse for McpToolMacroAttributes {
68 fn parse(attributes: syn::parse::ParseStream) -> syn::Result<Self> {
78 let mut name = None;
79 let mut description = None;
80 let mut destructive_hint = None;
81 let mut idempotent_hint = None;
82 let mut open_world_hint = None;
83 let mut read_only_hint = None;
84 let mut title = None;
85
86 let meta_list: Punctuated<Meta, Token![,]> = Punctuated::parse_terminated(attributes)?;
87 for meta in meta_list {
88 if let Meta::NameValue(meta_name_value) = meta {
89 let ident = meta_name_value.path.get_ident().unwrap();
90 let ident_str = ident.to_string();
91
92 match ident_str.as_str() {
93 "name" | "description" => {
94 let value = match &meta_name_value.value {
95 Expr::Lit(ExprLit {
96 lit: Lit::Str(lit_str),
97 ..
98 }) => lit_str.value(),
99 Expr::Macro(expr_macro) => {
100 let mac = &expr_macro.mac;
101 if mac.path.is_ident("concat") {
102 let args: ExprList = syn::parse2(mac.tokens.clone())?;
103 let mut result = String::new();
104 for expr in args.exprs {
105 if let Expr::Lit(ExprLit {
106 lit: Lit::Str(lit_str),
107 ..
108 }) = expr
109 {
110 result.push_str(&lit_str.value());
111 } else {
112 return Err(Error::new_spanned(
113 expr,
114 "Only string literals are allowed inside concat!()",
115 ));
116 }
117 }
118 result
119 } else {
120 return Err(Error::new_spanned(
121 expr_macro,
122 "Only concat!(...) is supported here",
123 ));
124 }
125 }
126 _ => {
127 return Err(Error::new_spanned(
128 &meta_name_value.value,
129 "Expected a string literal or concat!(...)",
130 ));
131 }
132 };
133 match ident_str.as_str() {
134 "name" => name = Some(value),
135 "description" => description = Some(value),
136 _ => {}
137 }
138 }
139 "destructive_hint" | "idempotent_hint" | "open_world_hint"
140 | "read_only_hint" => {
141 let value = match &meta_name_value.value {
142 Expr::Lit(ExprLit {
143 lit: Lit::Bool(lit_bool),
144 ..
145 }) => lit_bool.value,
146 _ => {
147 return Err(Error::new_spanned(
148 &meta_name_value.value,
149 "Expected a boolean literal",
150 ));
151 }
152 };
153 match ident_str.as_str() {
154 "destructive_hint" => destructive_hint = Some(value),
155 "idempotent_hint" => idempotent_hint = Some(value),
156 "open_world_hint" => open_world_hint = Some(value),
157 "read_only_hint" => read_only_hint = Some(value),
158 _ => {}
159 }
160 }
161 "title" => {
162 let value = match &meta_name_value.value {
163 Expr::Lit(ExprLit {
164 lit: Lit::Str(lit_str),
165 ..
166 }) => lit_str.value(),
167 _ => {
168 return Err(Error::new_spanned(
169 &meta_name_value.value,
170 "Expected a string literal",
171 ));
172 }
173 };
174 title = Some(value);
175 }
176 _ => {}
177 }
178 }
179 }
180
181 if name.as_ref().map(|s| s.trim().is_empty()).unwrap_or(true) {
183 return Err(Error::new(
184 attributes.span(),
185 "The 'name' attribute is required and must not be empty.",
186 ));
187 }
188 if description
189 .as_ref()
190 .map(|s| s.trim().is_empty())
191 .unwrap_or(true)
192 {
193 return Err(Error::new(
194 attributes.span(),
195 "The 'description' attribute is required and must not be empty.",
196 ));
197 }
198
199 #[cfg(feature = "2024_11_05")]
200 let instance = Self { name, description };
201
202 #[cfg(feature = "2025_03_26")]
203 let instance = Self {
204 name,
205 description,
206 destructive_hint,
207 idempotent_hint,
208 open_world_hint,
209 read_only_hint,
210 title,
211 };
212
213 Ok(instance)
214 }
215}
216
217#[proc_macro_attribute]
253pub fn mcp_tool(attributes: TokenStream, input: TokenStream) -> TokenStream {
254 let input = parse_macro_input!(input as DeriveInput); let input_ident = &input.ident;
256
257 let macro_attributes = parse_macro_input!(attributes as McpToolMacroAttributes);
258
259 let tool_name = macro_attributes.name.unwrap_or_default();
260 let tool_description = macro_attributes.description.unwrap_or_default();
261
262 #[cfg(feature = "2025_03_26")]
263 let some_annotations = macro_attributes.destructive_hint.is_some()
264 || macro_attributes.idempotent_hint.is_some()
265 || macro_attributes.open_world_hint.is_some()
266 || macro_attributes.read_only_hint.is_some()
267 || macro_attributes.title.is_some();
268
269 #[cfg(feature = "2025_03_26")]
270 let annotations = if some_annotations {
271 let destructive_hint = macro_attributes
272 .destructive_hint
273 .map_or(quote! {None}, |v| quote! {Some(#v)});
274
275 let idempotent_hint = macro_attributes
276 .idempotent_hint
277 .map_or(quote! {None}, |v| quote! {Some(#v)});
278 let open_world_hint = macro_attributes
279 .open_world_hint
280 .map_or(quote! {None}, |v| quote! {Some(#v)});
281 let read_only_hint = macro_attributes
282 .read_only_hint
283 .map_or(quote! {None}, |v| quote! {Some(#v)});
284 let title = macro_attributes
285 .title
286 .map_or(quote! {None}, |v| quote! {Some(#v)});
287 quote! {
288 Some(rust_mcp_schema::ToolAnnotations {
289 destructive_hint: #destructive_hint,
290 idempotent_hint: #idempotent_hint,
291 open_world_hint: #open_world_hint,
292 read_only_hint: #read_only_hint,
293 title: #title,
294 }),
295 }
296 } else {
297 quote! {None}
298 };
299
300 #[cfg(feature = "2025_03_26")]
301 let tool_token = quote! {
302 rust_mcp_schema::Tool {
303 name: #tool_name.to_string(),
304 description: Some(#tool_description.to_string()),
305 input_schema: rust_mcp_schema::ToolInputSchema::new(required, properties),
306 annotations: #annotations
307 }
308 };
309 #[cfg(feature = "2024_11_05")]
310 let tool_token = quote! {
311 rust_mcp_schema::Tool {
312 name: #tool_name.to_string(),
313 description: Some(#tool_description.to_string()),
314 input_schema: rust_mcp_schema::ToolInputSchema::new(required, properties),
315 }
316 };
317
318 let output = quote! {
319 impl #input_ident {
320 pub fn tool_name()->String{
322 #tool_name.to_string()
323 }
324
325 pub fn tool()-> rust_mcp_schema::Tool
330 {
331 let json_schema = &#input_ident::json_schema();
332
333 let required: Vec<_> = match json_schema.get("required").and_then(|r| r.as_array()) {
334 Some(arr) => arr
335 .iter()
336 .filter_map(|item| item.as_str().map(String::from))
337 .collect(),
338 None => Vec::new(), };
340
341 let properties: Option<
342 std::collections::HashMap<String, serde_json::Map<String, serde_json::Value>>,
343 > = json_schema
344 .get("properties")
345 .and_then(|v| v.as_object()) .map(|properties| {
347 properties
348 .iter()
349 .filter_map(|(key, value)| {
350 serde_json::to_value(value)
351 .ok() .and_then(|v| {
353 if let serde_json::Value::Object(obj) = v {
354 Some(obj)
355 } else {
356 None
357 }
358 })
359 .map(|obj| (key.to_string(), obj)) })
361 .collect()
362 });
363
364 #tool_token
365 }
366 }
367 #input
369 };
370
371 TokenStream::from(output)
372}
373
374#[proc_macro_derive(JsonSchema)]
408pub fn derive_json_schema(input: TokenStream) -> TokenStream {
409 let input = parse_macro_input!(input as DeriveInput);
410 let name = &input.ident;
411
412 let fields = match &input.data {
413 Data::Struct(data) => match &data.fields {
414 Fields::Named(fields) => &fields.named,
415 _ => panic!("JsonSchema derive macro only supports named fields"),
416 },
417 _ => panic!("JsonSchema derive macro only supports structs"),
418 };
419
420 let field_entries = fields.iter().map(|field| {
421 let field_attrs = &field.attrs;
422 let renamed_field = renamed_field(field_attrs);
423 let field_name = renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
424 let field_type = &field.ty;
425
426 let schema = type_to_json_schema(field_type, field_attrs);
427 quote! {
428 properties.insert(
429 #field_name.to_string(),
430 serde_json::Value::Object(#schema)
431 );
432 }
433 });
434
435 let required_fields = fields.iter().filter_map(|field| {
436 let renamed_field = renamed_field(&field.attrs);
437 let field_name = renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
438
439 let field_type = &field.ty;
440 if !is_option(field_type) {
441 Some(quote! {
442 required.push(#field_name.to_string());
443 })
444 } else {
445 None
446 }
447 });
448
449 let expanded = quote! {
450 impl #name {
451 pub fn json_schema() -> serde_json::Map<String, serde_json::Value> {
452 let mut schema = serde_json::Map::new();
453 let mut properties = serde_json::Map::new();
454 let mut required = Vec::new();
455
456 #(#field_entries)*
457
458 #(#required_fields)*
459
460 schema.insert("type".to_string(), serde_json::Value::String("object".to_string()));
461 schema.insert("properties".to_string(), serde_json::Value::Object(properties));
462 if !required.is_empty() {
463 schema.insert("required".to_string(), serde_json::Value::Array(
464 required.into_iter().map(serde_json::Value::String).collect()
465 ));
466 }
467
468 schema
469 }
470 }
471 };
472 TokenStream::from(expanded)
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use syn::parse_str;
479 #[test]
480 fn test_valid_macro_attributes() {
481 let input = r#"name = "test_tool", description = "A test tool.""#;
482 let parsed: McpToolMacroAttributes = parse_str(input).unwrap();
483
484 assert_eq!(parsed.name.unwrap(), "test_tool");
485 assert_eq!(parsed.description.unwrap(), "A test tool.");
486 }
487
488 #[test]
489 fn test_missing_name() {
490 let input = r#"description = "Only description""#;
491 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
492 assert!(result.is_err());
493 assert_eq!(
494 result.err().unwrap().to_string(),
495 "The 'name' attribute is required and must not be empty."
496 )
497 }
498
499 #[test]
500 fn test_missing_description() {
501 let input = r#"name = "OnlyName""#;
502 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
503 assert!(result.is_err());
504 assert_eq!(
505 result.err().unwrap().to_string(),
506 "The 'description' attribute is required and must not be empty."
507 )
508 }
509
510 #[test]
511 fn test_empty_name_field() {
512 let input = r#"name = "", description = "something""#;
513 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
514 assert!(result.is_err());
515 assert_eq!(
516 result.err().unwrap().to_string(),
517 "The 'name' attribute is required and must not be empty."
518 );
519 }
520 #[test]
521 fn test_empty_description_field() {
522 let input = r#"name = "my-tool", description = """#;
523 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
524 assert!(result.is_err());
525 assert_eq!(
526 result.err().unwrap().to_string(),
527 "The 'description' attribute is required and must not be empty."
528 );
529 }
530}