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]
295pub fn mcp_tool(attributes: TokenStream, input: TokenStream) -> TokenStream {
296 let input = parse_macro_input!(input as DeriveInput);
297 let input_ident = &input.ident;
298
299 let base_crate = if cfg!(feature = "sdk") {
301 quote! { rust_mcp_sdk::schema }
302 } else {
303 quote! { rust_mcp_schema }
304 };
305
306 let macro_attributes = parse_macro_input!(attributes as McpToolMacroAttributes);
307
308 let tool_name = macro_attributes.name.unwrap_or_default();
309 let tool_description = macro_attributes.description.unwrap_or_default();
310
311 #[cfg(not(feature = "2025_06_18"))]
312 let meta = quote! {};
313 #[cfg(feature = "2025_06_18")]
314 let meta = macro_attributes.meta.map_or(quote! { meta: None, }, |m| {
315 quote! { meta: Some(serde_json::from_str(#m).expect("Failed to parse meta JSON")), }
316 });
317
318 #[cfg(not(feature = "2025_06_18"))]
319 let title = quote! {};
320 #[cfg(feature = "2025_06_18")]
321 let title = macro_attributes.title.map_or(
322 quote! { title: None, },
323 |t| quote! { title: Some(#t.to_string()), },
324 );
325
326 #[cfg(not(feature = "2025_06_18"))]
327 let output_schema = quote! {};
328 #[cfg(feature = "2025_06_18")]
329 let output_schema = quote! { output_schema: None,};
330
331 #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
332 let some_annotations = macro_attributes.destructive_hint.is_some()
333 || macro_attributes.idempotent_hint.is_some()
334 || macro_attributes.open_world_hint.is_some()
335 || macro_attributes.read_only_hint.is_some();
336
337 #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
338 let annotations = if some_annotations {
339 let destructive_hint = macro_attributes
340 .destructive_hint
341 .map_or(quote! {None}, |v| quote! {Some(#v)});
342
343 let idempotent_hint = macro_attributes
344 .idempotent_hint
345 .map_or(quote! {None}, |v| quote! {Some(#v)});
346 let open_world_hint = macro_attributes
347 .open_world_hint
348 .map_or(quote! {None}, |v| quote! {Some(#v)});
349 let read_only_hint = macro_attributes
350 .read_only_hint
351 .map_or(quote! {None}, |v| quote! {Some(#v)});
352 quote! {
353 Some(#base_crate::ToolAnnotations {
354 destructive_hint: #destructive_hint,
355 idempotent_hint: #idempotent_hint,
356 open_world_hint: #open_world_hint,
357 read_only_hint: #read_only_hint,
358 title: None,
359 })
360 }
361 } else {
362 quote! { None }
363 };
364
365 let annotations_token = {
366 #[cfg(any(feature = "2025_03_26", feature = "2025_06_18"))]
367 {
368 quote! { annotations: #annotations, }
369 }
370 #[cfg(not(any(feature = "2025_03_26", feature = "2025_06_18")))]
371 {
372 quote! {}
373 }
374 };
375
376 let tool_token = quote! {
377 #base_crate::Tool {
378 name: #tool_name.to_string(),
379 description: Some(#tool_description.to_string()),
380 #output_schema
381 #title
382 #meta
383 #annotations_token
384 input_schema: #base_crate::ToolInputSchema::new(required, properties)
385 }
386 };
387
388 let output = quote! {
389 impl #input_ident {
390 pub fn tool_name() -> String {
392 #tool_name.to_string()
393 }
394
395 pub fn tool() -> #base_crate::Tool {
400 let json_schema = &#input_ident::json_schema();
401
402 let required: Vec<_> = match json_schema.get("required").and_then(|r| r.as_array()) {
403 Some(arr) => arr
404 .iter()
405 .filter_map(|item| item.as_str().map(String::from))
406 .collect(),
407 None => Vec::new(), };
409
410 let properties: Option<
411 std::collections::HashMap<String, serde_json::Map<String, serde_json::Value>>,
412 > = json_schema
413 .get("properties")
414 .and_then(|v| v.as_object()) .map(|properties| {
416 properties
417 .iter()
418 .filter_map(|(key, value)| {
419 serde_json::to_value(value)
420 .ok() .and_then(|v| {
422 if let serde_json::Value::Object(obj) = v {
423 Some(obj)
424 } else {
425 None
426 }
427 })
428 .map(|obj| (key.to_string(), obj)) })
430 .collect()
431 });
432
433 #tool_token
434 }
435 }
436 #input
438 };
439
440 TokenStream::from(output)
441}
442
443#[proc_macro_derive(JsonSchema)]
477pub fn derive_json_schema(input: TokenStream) -> TokenStream {
478 let input = parse_macro_input!(input as DeriveInput);
479 let name = &input.ident;
480
481 let fields = match &input.data {
482 Data::Struct(data) => match &data.fields {
483 Fields::Named(fields) => &fields.named,
484 _ => panic!("JsonSchema derive macro only supports named fields"),
485 },
486 _ => panic!("JsonSchema derive macro only supports structs"),
487 };
488
489 let field_entries = fields.iter().map(|field| {
490 let field_attrs = &field.attrs;
491 let renamed_field = renamed_field(field_attrs);
492 let field_name = renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
493 let field_type = &field.ty;
494
495 let schema = type_to_json_schema(field_type, field_attrs);
496 quote! {
497 properties.insert(
498 #field_name.to_string(),
499 serde_json::Value::Object(#schema)
500 );
501 }
502 });
503
504 let required_fields = fields.iter().filter_map(|field| {
505 let renamed_field = renamed_field(&field.attrs);
506 let field_name = renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
507
508 let field_type = &field.ty;
509 if !is_option(field_type) {
510 Some(quote! {
511 required.push(#field_name.to_string());
512 })
513 } else {
514 None
515 }
516 });
517
518 let expanded = quote! {
519 impl #name {
520 pub fn json_schema() -> serde_json::Map<String, serde_json::Value> {
521 let mut schema = serde_json::Map::new();
522 let mut properties = serde_json::Map::new();
523 let mut required = Vec::new();
524
525 #(#field_entries)*
526
527 #(#required_fields)*
528
529 schema.insert("type".to_string(), serde_json::Value::String("object".to_string()));
530 schema.insert("properties".to_string(), serde_json::Value::Object(properties));
531 if !required.is_empty() {
532 schema.insert("required".to_string(), serde_json::Value::Array(
533 required.into_iter().map(serde_json::Value::String).collect()
534 ));
535 }
536
537 schema
538 }
539 }
540 };
541 TokenStream::from(expanded)
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547 use syn::parse_str;
548 #[test]
549 fn test_valid_macro_attributes() {
550 let input = r#"name = "test_tool", description = "A test tool.", meta = "{\"version\": \"1.0\"}", title = "Test Tool""#;
551 let parsed: McpToolMacroAttributes = parse_str(input).unwrap();
552
553 assert_eq!(parsed.name.unwrap(), "test_tool");
554 assert_eq!(parsed.description.unwrap(), "A test tool.");
555 assert_eq!(parsed.meta.unwrap(), "{\"version\": \"1.0\"}");
556 assert_eq!(parsed.title.unwrap(), "Test Tool");
557 }
558
559 #[test]
560 fn test_missing_name() {
561 let input = r#"description = "Only description""#;
562 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
563 assert!(result.is_err());
564 assert_eq!(
565 result.err().unwrap().to_string(),
566 "The 'name' attribute is required and must not be empty."
567 );
568 }
569
570 #[test]
571 fn test_missing_description() {
572 let input = r#"name = "OnlyName""#;
573 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
574 assert!(result.is_err());
575 assert_eq!(
576 result.err().unwrap().to_string(),
577 "The 'description' attribute is required and must not be empty."
578 );
579 }
580
581 #[test]
582 fn test_empty_name_field() {
583 let input = r#"name = "", description = "something""#;
584 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
585 assert!(result.is_err());
586 assert_eq!(
587 result.err().unwrap().to_string(),
588 "The 'name' attribute is required and must not be empty."
589 );
590 }
591
592 #[test]
593 fn test_empty_description_field() {
594 let input = r#"name = "my-tool", description = """#;
595 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
596 assert!(result.is_err());
597 assert_eq!(
598 result.err().unwrap().to_string(),
599 "The 'description' attribute is required and must not be empty."
600 );
601 }
602
603 #[test]
604 fn test_invalid_meta() {
605 let input =
606 r#"name = "test_tool", description = "A test tool.", meta = "not_a_json_object""#;
607 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
608 assert!(result.is_err());
609 assert!(result
610 .err()
611 .unwrap()
612 .to_string()
613 .contains("Expected a valid JSON object"));
614 }
615
616 #[test]
617 fn test_non_object_meta() {
618 let input = r#"name = "test_tool", description = "A test tool.", meta = "[1, 2, 3]""#;
619 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
620 assert!(result.is_err());
621 assert_eq!(result.err().unwrap().to_string(), "Expected a JSON object");
622 }
623}