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
13struct McpToolMacroAttributes {
30 name: Option<String>,
31 description: Option<String>,
32 #[cfg(feature = "2025_06_18")]
33 meta: Option<String>, #[cfg(feature = "2025_06_18")]
35 title: Option<String>,
36 #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
37 destructive_hint: Option<bool>,
38 #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
39 idempotent_hint: Option<bool>,
40 #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
41 open_world_hint: Option<bool>,
42 #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
43 read_only_hint: Option<bool>,
44}
45
46use syn::parse::ParseStream;
47
48struct ExprList {
49 exprs: Punctuated<Expr, Token![,]>,
50}
51
52impl Parse for ExprList {
53 fn parse(input: ParseStream) -> syn::Result<Self> {
54 Ok(ExprList {
55 exprs: Punctuated::parse_terminated(input)?,
56 })
57 }
58}
59
60impl Parse for McpToolMacroAttributes {
61 fn parse(attributes: syn::parse::ParseStream) -> syn::Result<Self> {
74 let mut instance = Self {
75 name: None,
76 description: None,
77 #[cfg(feature = "2025_06_18")]
78 meta: None,
79 #[cfg(feature = "2025_06_18")]
80 title: None,
81 #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
82 destructive_hint: None,
83 #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
84 idempotent_hint: None,
85 #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
86 open_world_hint: None,
87 #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
88 read_only_hint: None,
89 };
90
91 let meta_list: Punctuated<Meta, Token![,]> = Punctuated::parse_terminated(attributes)?;
92 for meta in meta_list {
93 if let Meta::NameValue(meta_name_value) = meta {
94 let ident = meta_name_value.path.get_ident().unwrap();
95 let ident_str = ident.to_string();
96
97 match ident_str.as_str() {
98 "name" | "description" => {
99 let value = match &meta_name_value.value {
100 Expr::Lit(ExprLit {
101 lit: Lit::Str(lit_str),
102 ..
103 }) => lit_str.value(),
104 Expr::Macro(expr_macro) => {
105 let mac = &expr_macro.mac;
106 if mac.path.is_ident("concat") {
107 let args: ExprList = syn::parse2(mac.tokens.clone())?;
108 let mut result = String::new();
109 for expr in args.exprs {
110 if let Expr::Lit(ExprLit {
111 lit: Lit::Str(lit_str),
112 ..
113 }) = expr
114 {
115 result.push_str(&lit_str.value());
116 } else {
117 return Err(Error::new_spanned(
118 expr,
119 "Only string literals are allowed inside concat!()",
120 ));
121 }
122 }
123 result
124 } else {
125 return Err(Error::new_spanned(
126 expr_macro,
127 "Only concat!(...) is supported here",
128 ));
129 }
130 }
131 _ => {
132 return Err(Error::new_spanned(
133 &meta_name_value.value,
134 "Expected a string literal or concat!(...)",
135 ));
136 }
137 };
138 match ident_str.as_str() {
139 "name" => instance.name = Some(value),
140 "description" => instance.description = Some(value),
141 _ => {}
142 }
143 }
144 #[cfg(feature = "2025_06_18")]
145 "meta" => {
146 let value = match &meta_name_value.value {
147 Expr::Lit(ExprLit {
148 lit: Lit::Str(lit_str),
149 ..
150 }) => lit_str.value(),
151 _ => {
152 return Err(Error::new_spanned(
153 &meta_name_value.value,
154 "Expected a JSON object as a string literal",
155 ));
156 }
157 };
158 let parsed: serde_json::Value =
160 serde_json::from_str(&value).map_err(|e| {
161 Error::new_spanned(
162 &meta_name_value.value,
163 format!("Expected a valid JSON object: {e}"),
164 )
165 })?;
166 if !parsed.is_object() {
167 return Err(Error::new_spanned(
168 &meta_name_value.value,
169 "Expected a JSON object",
170 ));
171 }
172 instance.meta = Some(value);
173 }
174 #[cfg(feature = "2025_06_18")]
175 "title" => {
176 let value = match &meta_name_value.value {
177 Expr::Lit(ExprLit {
178 lit: Lit::Str(lit_str),
179 ..
180 }) => lit_str.value(),
181 _ => {
182 return Err(Error::new_spanned(
183 &meta_name_value.value,
184 "Expected a string literal",
185 ));
186 }
187 };
188 instance.title = Some(value);
189 }
190 "destructive_hint" | "idempotent_hint" | "open_world_hint"
191 | "read_only_hint" => {
192 #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
193 {
194 let value = match &meta_name_value.value {
195 Expr::Lit(ExprLit {
196 lit: Lit::Bool(lit_bool),
197 ..
198 }) => lit_bool.value,
199 _ => {
200 return Err(Error::new_spanned(
201 &meta_name_value.value,
202 "Expected a boolean literal",
203 ));
204 }
205 };
206
207 match ident_str.as_str() {
208 "destructive_hint" => instance.destructive_hint = Some(value),
209 "idempotent_hint" => instance.idempotent_hint = Some(value),
210 "open_world_hint" => instance.open_world_hint = Some(value),
211 "read_only_hint" => instance.read_only_hint = Some(value),
212 _ => {}
213 }
214 }
215 }
216 _ => {}
217 }
218 }
219 }
220
221 if instance
223 .name
224 .as_ref()
225 .map(|s| s.trim().is_empty())
226 .unwrap_or(true)
227 {
228 return Err(Error::new(
229 attributes.span(),
230 "The 'name' attribute is required and must not be empty.",
231 ));
232 }
233 if instance
234 .description
235 .as_ref()
236 .map(|s| s.trim().is_empty())
237 .unwrap_or(true)
238 {
239 return Err(Error::new(
240 attributes.span(),
241 "The 'description' attribute is required and must not be empty.",
242 ));
243 }
244
245 Ok(instance)
246 }
247}
248
249#[proc_macro_attribute]
292pub fn mcp_tool(attributes: TokenStream, input: TokenStream) -> TokenStream {
293 let input = parse_macro_input!(input as DeriveInput);
294 let input_ident = &input.ident;
295
296 let base_crate = if cfg!(feature = "sdk") {
298 quote! { rust_mcp_sdk::schema }
299 } else {
300 quote! { rust_mcp_schema }
301 };
302
303 let macro_attributes = parse_macro_input!(attributes as McpToolMacroAttributes);
304
305 let tool_name = macro_attributes.name.unwrap_or_default();
306 let tool_description = macro_attributes.description.unwrap_or_default();
307
308 #[cfg(not(feature = "2025_06_18"))]
309 let meta = quote! {};
310 #[cfg(feature = "2025_06_18")]
311 let meta = macro_attributes.meta.map_or(quote! { meta: None, }, |m| {
312 quote! { meta: Some(serde_json::from_str(#m).expect("Failed to parse meta JSON")), }
313 });
314
315 #[cfg(not(feature = "2025_06_18"))]
316 let title = quote! {};
317 #[cfg(feature = "2025_06_18")]
318 let title = macro_attributes.title.map_or(
319 quote! { title: None, },
320 |t| quote! { title: Some(#t.to_string()), },
321 );
322
323 #[cfg(not(feature = "2025_06_18"))]
324 let output_schema = quote! {};
325 #[cfg(feature = "2025_06_18")]
326 let output_schema = quote! { output_schema: None,};
327
328 #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
329 let some_annotations = macro_attributes.destructive_hint.is_some()
330 || macro_attributes.idempotent_hint.is_some()
331 || macro_attributes.open_world_hint.is_some()
332 || macro_attributes.read_only_hint.is_some();
333
334 #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
335 let annotations = if some_annotations {
336 let destructive_hint = macro_attributes
337 .destructive_hint
338 .map_or(quote! {None}, |v| quote! {Some(#v)});
339
340 let idempotent_hint = macro_attributes
341 .idempotent_hint
342 .map_or(quote! {None}, |v| quote! {Some(#v)});
343 let open_world_hint = macro_attributes
344 .open_world_hint
345 .map_or(quote! {None}, |v| quote! {Some(#v)});
346 let read_only_hint = macro_attributes
347 .read_only_hint
348 .map_or(quote! {None}, |v| quote! {Some(#v)});
349 quote! {
350 Some(#base_crate::ToolAnnotations {
351 destructive_hint: #destructive_hint,
352 idempotent_hint: #idempotent_hint,
353 open_world_hint: #open_world_hint,
354 read_only_hint: #read_only_hint,
355 title: None,
356 })
357 }
358 } else {
359 quote! { None }
360 };
361
362 let annotations_token = {
363 #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
364 {
365 quote! { annotations: #annotations, }
366 }
367 #[cfg(not(any(feature = "2025_03_26", feature = "2025_06_18")))]
368 {
369 quote! {}
370 }
371 };
372
373 let tool_token = quote! {
374 #base_crate::Tool {
375 name: #tool_name.to_string(),
376 description: Some(#tool_description.to_string()),
377 #output_schema
378 #title
379 #meta
380 #annotations_token
381 input_schema: #base_crate::ToolInputSchema::new(required, properties)
382 }
383 };
384
385 let output = quote! {
386 impl #input_ident {
387 pub fn tool_name() -> String {
389 #tool_name.to_string()
390 }
391
392 pub fn tool() -> #base_crate::Tool {
397 let json_schema = &#input_ident::json_schema();
398
399 let required: Vec<_> = match json_schema.get("required").and_then(|r| r.as_array()) {
400 Some(arr) => arr
401 .iter()
402 .filter_map(|item| item.as_str().map(String::from))
403 .collect(),
404 None => Vec::new(), };
406
407 let properties: Option<
408 std::collections::HashMap<String, serde_json::Map<String, serde_json::Value>>,
409 > = json_schema
410 .get("properties")
411 .and_then(|v| v.as_object()) .map(|properties| {
413 properties
414 .iter()
415 .filter_map(|(key, value)| {
416 serde_json::to_value(value)
417 .ok() .and_then(|v| {
419 if let serde_json::Value::Object(obj) = v {
420 Some(obj)
421 } else {
422 None
423 }
424 })
425 .map(|obj| (key.to_string(), obj)) })
427 .collect()
428 });
429
430 #tool_token
431 }
432 }
433 #input
435 };
436
437 TokenStream::from(output)
438}
439
440#[proc_macro_derive(JsonSchema)]
474pub fn derive_json_schema(input: TokenStream) -> TokenStream {
475 let input = parse_macro_input!(input as DeriveInput);
476 let name = &input.ident;
477
478 let fields = match &input.data {
479 Data::Struct(data) => match &data.fields {
480 Fields::Named(fields) => &fields.named,
481 _ => panic!("JsonSchema derive macro only supports named fields"),
482 },
483 _ => panic!("JsonSchema derive macro only supports structs"),
484 };
485
486 let field_entries = fields.iter().map(|field| {
487 let field_attrs = &field.attrs;
488 let renamed_field = renamed_field(field_attrs);
489 let field_name = renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
490 let field_type = &field.ty;
491
492 let schema = type_to_json_schema(field_type, field_attrs);
493 quote! {
494 properties.insert(
495 #field_name.to_string(),
496 serde_json::Value::Object(#schema)
497 );
498 }
499 });
500
501 let required_fields = fields.iter().filter_map(|field| {
502 let renamed_field = renamed_field(&field.attrs);
503 let field_name = renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
504
505 let field_type = &field.ty;
506 if !is_option(field_type) {
507 Some(quote! {
508 required.push(#field_name.to_string());
509 })
510 } else {
511 None
512 }
513 });
514
515 let expanded = quote! {
516 impl #name {
517 pub fn json_schema() -> serde_json::Map<String, serde_json::Value> {
518 let mut schema = serde_json::Map::new();
519 let mut properties = serde_json::Map::new();
520 let mut required = Vec::new();
521
522 #(#field_entries)*
523
524 #(#required_fields)*
525
526 schema.insert("type".to_string(), serde_json::Value::String("object".to_string()));
527 schema.insert("properties".to_string(), serde_json::Value::Object(properties));
528 if !required.is_empty() {
529 schema.insert("required".to_string(), serde_json::Value::Array(
530 required.into_iter().map(serde_json::Value::String).collect()
531 ));
532 }
533
534 schema
535 }
536 }
537 };
538 TokenStream::from(expanded)
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544 use syn::parse_str;
545 #[test]
546 fn test_valid_macro_attributes() {
547 let input = r#"name = "test_tool", description = "A test tool.", meta = "{\"version\": \"1.0\"}", title = "Test Tool""#;
548 let parsed: McpToolMacroAttributes = parse_str(input).unwrap();
549
550 assert_eq!(parsed.name.unwrap(), "test_tool");
551 assert_eq!(parsed.description.unwrap(), "A test tool.");
552 assert_eq!(parsed.meta.unwrap(), "{\"version\": \"1.0\"}");
553 assert_eq!(parsed.title.unwrap(), "Test Tool");
554 }
555
556 #[test]
557 fn test_missing_name() {
558 let input = r#"description = "Only description""#;
559 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
560 assert!(result.is_err());
561 assert_eq!(
562 result.err().unwrap().to_string(),
563 "The 'name' attribute is required and must not be empty."
564 );
565 }
566
567 #[test]
568 fn test_missing_description() {
569 let input = r#"name = "OnlyName""#;
570 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
571 assert!(result.is_err());
572 assert_eq!(
573 result.err().unwrap().to_string(),
574 "The 'description' attribute is required and must not be empty."
575 );
576 }
577
578 #[test]
579 fn test_empty_name_field() {
580 let input = r#"name = "", description = "something""#;
581 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
582 assert!(result.is_err());
583 assert_eq!(
584 result.err().unwrap().to_string(),
585 "The 'name' attribute is required and must not be empty."
586 );
587 }
588
589 #[test]
590 fn test_empty_description_field() {
591 let input = r#"name = "my-tool", description = """#;
592 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
593 assert!(result.is_err());
594 assert_eq!(
595 result.err().unwrap().to_string(),
596 "The 'description' attribute is required and must not be empty."
597 );
598 }
599
600 #[test]
601 fn test_invalid_meta() {
602 let input =
603 r#"name = "test_tool", description = "A test tool.", meta = "not_a_json_object""#;
604 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
605 assert!(result.is_err());
606 assert!(result
607 .err()
608 .unwrap()
609 .to_string()
610 .contains("Expected a valid JSON object"));
611 }
612
613 #[test]
614 fn test_non_object_meta() {
615 let input = r#"name = "test_tool", description = "A test tool.", meta = "[1, 2, 3]""#;
616 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
617 assert!(result.is_err());
618 assert_eq!(result.err().unwrap().to_string(), "Expected a JSON object");
619 }
620}