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 base_crate = if cfg!(feature = "sdk") {
259 quote! { rust_mcp_sdk::schema }
260 } else {
261 quote! { rust_mcp_schema }
262 };
263
264 let macro_attributes = parse_macro_input!(attributes as McpToolMacroAttributes);
265
266 let tool_name = macro_attributes.name.unwrap_or_default();
267 let tool_description = macro_attributes.description.unwrap_or_default();
268
269 #[cfg(feature = "2025_03_26")]
270 let some_annotations = macro_attributes.destructive_hint.is_some()
271 || macro_attributes.idempotent_hint.is_some()
272 || macro_attributes.open_world_hint.is_some()
273 || macro_attributes.read_only_hint.is_some()
274 || macro_attributes.title.is_some();
275
276 #[cfg(feature = "2025_03_26")]
277 let annotations = if some_annotations {
278 let destructive_hint = macro_attributes
279 .destructive_hint
280 .map_or(quote! {None}, |v| quote! {Some(#v)});
281
282 let idempotent_hint = macro_attributes
283 .idempotent_hint
284 .map_or(quote! {None}, |v| quote! {Some(#v)});
285 let open_world_hint = macro_attributes
286 .open_world_hint
287 .map_or(quote! {None}, |v| quote! {Some(#v)});
288 let read_only_hint = macro_attributes
289 .read_only_hint
290 .map_or(quote! {None}, |v| quote! {Some(#v)});
291 let title = macro_attributes
292 .title
293 .map_or(quote! {None}, |v| quote! {Some(#v)});
294 quote! {
295 Some(#base_crate::ToolAnnotations {
296 destructive_hint: #destructive_hint,
297 idempotent_hint: #idempotent_hint,
298 open_world_hint: #open_world_hint,
299 read_only_hint: #read_only_hint,
300 title: #title,
301 }),
302 }
303 } else {
304 quote! {None}
305 };
306
307 #[cfg(feature = "2025_03_26")]
308 let tool_token = quote! {
309 #base_crate::Tool {
310 name: #tool_name.to_string(),
311 description: Some(#tool_description.to_string()),
312 input_schema: #base_crate::ToolInputSchema::new(required, properties),
313 annotations: #annotations
314 }
315 };
316 #[cfg(feature = "2024_11_05")]
317 let tool_token = quote! {
318 #base_crate::Tool {
319 name: #tool_name.to_string(),
320 description: Some(#tool_description.to_string()),
321 input_schema: #base_crate::ToolInputSchema::new(required, properties),
322 }
323 };
324
325 let output = quote! {
326 impl #input_ident {
327 pub fn tool_name()->String{
329 #tool_name.to_string()
330 }
331
332 pub fn tool()-> #base_crate::Tool
337 {
338 let json_schema = &#input_ident::json_schema();
339
340 let required: Vec<_> = match json_schema.get("required").and_then(|r| r.as_array()) {
341 Some(arr) => arr
342 .iter()
343 .filter_map(|item| item.as_str().map(String::from))
344 .collect(),
345 None => Vec::new(), };
347
348 let properties: Option<
349 std::collections::HashMap<String, serde_json::Map<String, serde_json::Value>>,
350 > = json_schema
351 .get("properties")
352 .and_then(|v| v.as_object()) .map(|properties| {
354 properties
355 .iter()
356 .filter_map(|(key, value)| {
357 serde_json::to_value(value)
358 .ok() .and_then(|v| {
360 if let serde_json::Value::Object(obj) = v {
361 Some(obj)
362 } else {
363 None
364 }
365 })
366 .map(|obj| (key.to_string(), obj)) })
368 .collect()
369 });
370
371 #tool_token
372 }
373 }
374 #input
376 };
377
378 TokenStream::from(output)
379}
380
381#[proc_macro_derive(JsonSchema)]
415pub fn derive_json_schema(input: TokenStream) -> TokenStream {
416 let input = parse_macro_input!(input as DeriveInput);
417 let name = &input.ident;
418
419 let fields = match &input.data {
420 Data::Struct(data) => match &data.fields {
421 Fields::Named(fields) => &fields.named,
422 _ => panic!("JsonSchema derive macro only supports named fields"),
423 },
424 _ => panic!("JsonSchema derive macro only supports structs"),
425 };
426
427 let field_entries = fields.iter().map(|field| {
428 let field_attrs = &field.attrs;
429 let renamed_field = renamed_field(field_attrs);
430 let field_name = renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
431 let field_type = &field.ty;
432
433 let schema = type_to_json_schema(field_type, field_attrs);
434 quote! {
435 properties.insert(
436 #field_name.to_string(),
437 serde_json::Value::Object(#schema)
438 );
439 }
440 });
441
442 let required_fields = fields.iter().filter_map(|field| {
443 let renamed_field = renamed_field(&field.attrs);
444 let field_name = renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
445
446 let field_type = &field.ty;
447 if !is_option(field_type) {
448 Some(quote! {
449 required.push(#field_name.to_string());
450 })
451 } else {
452 None
453 }
454 });
455
456 let expanded = quote! {
457 impl #name {
458 pub fn json_schema() -> serde_json::Map<String, serde_json::Value> {
459 let mut schema = serde_json::Map::new();
460 let mut properties = serde_json::Map::new();
461 let mut required = Vec::new();
462
463 #(#field_entries)*
464
465 #(#required_fields)*
466
467 schema.insert("type".to_string(), serde_json::Value::String("object".to_string()));
468 schema.insert("properties".to_string(), serde_json::Value::Object(properties));
469 if !required.is_empty() {
470 schema.insert("required".to_string(), serde_json::Value::Array(
471 required.into_iter().map(serde_json::Value::String).collect()
472 ));
473 }
474
475 schema
476 }
477 }
478 };
479 TokenStream::from(expanded)
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485 use syn::parse_str;
486 #[test]
487 fn test_valid_macro_attributes() {
488 let input = r#"name = "test_tool", description = "A test tool.""#;
489 let parsed: McpToolMacroAttributes = parse_str(input).unwrap();
490
491 assert_eq!(parsed.name.unwrap(), "test_tool");
492 assert_eq!(parsed.description.unwrap(), "A test tool.");
493 }
494
495 #[test]
496 fn test_missing_name() {
497 let input = r#"description = "Only description""#;
498 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
499 assert!(result.is_err());
500 assert_eq!(
501 result.err().unwrap().to_string(),
502 "The 'name' attribute is required and must not be empty."
503 )
504 }
505
506 #[test]
507 fn test_missing_description() {
508 let input = r#"name = "OnlyName""#;
509 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
510 assert!(result.is_err());
511 assert_eq!(
512 result.err().unwrap().to_string(),
513 "The 'description' attribute is required and must not be empty."
514 )
515 }
516
517 #[test]
518 fn test_empty_name_field() {
519 let input = r#"name = "", description = "something""#;
520 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
521 assert!(result.is_err());
522 assert_eq!(
523 result.err().unwrap().to_string(),
524 "The 'name' attribute is required and must not be empty."
525 );
526 }
527 #[test]
528 fn test_empty_description_field() {
529 let input = r#"name = "my-tool", description = """#;
530 let result: Result<McpToolMacroAttributes, Error> = parse_str(input);
531 assert!(result.is_err());
532 assert_eq!(
533 result.err().unwrap().to_string(),
534 "The 'description' attribute is required and must not be empty."
535 );
536 }
537}