Skip to main content

bias_rust_mcp_macros/
lib.rs

1extern crate proc_macro;
2
3mod common;
4mod elicit;
5mod resource;
6mod tool;
7mod utils;
8
9use crate::elicit::generator::{generate_form_schema, generate_from_impl};
10use crate::elicit::parser::{ElicitArgs, ElicitMode};
11use crate::resource::generator::{
12    generate_resource_template_tokens, generate_resource_tokens, ResourceTemplateTokens,
13    ResourceTokens,
14};
15use crate::resource::parser::{McpResourceMacroAttributes, McpResourceTemplateMacroAttributes};
16use crate::tool::generator::{generate_tool_tokens, ToolTokens};
17use crate::tool::parser::McpToolMacroAttributes;
18use proc_macro::TokenStream;
19use quote::quote;
20use syn::{parse_macro_input, Data, DeriveInput, Fields};
21use utils::{base_crate, is_option, is_vec_string, renamed_field, type_to_json_schema};
22
23/// A procedural macro attribute to generate rust_mcp_schema::Tool related utility methods for a struct.
24///
25/// The `mcp_tool` macro generates an implementation for the annotated struct that includes:
26/// - A `tool_name()` method returning the tool's name as a string.
27/// - A `tool()` method returning a `rust_mcp_schema::Tool` instance with the tool's name,
28///   description, input schema, meta, and title derived from the struct's fields and attributes.
29///
30/// # Attributes
31/// * `name` - The name of the tool (required, non-empty string).
32/// * `description` - A description of the tool (required, non-empty string).
33/// * `meta` - Optional JSON object as a string literal for metadata.
34/// * `title` - Optional string for the tool's title.
35///
36/// # Panics
37/// Panics if the macro is applied to anything other than a struct.
38///
39/// # Example
40/// ```rust,ignore
41/// # #[cfg(not(feature = "sdk"))]
42/// # {
43/// #[rust_mcp_macros::mcp_tool(
44///     name = "example_tool",
45///     description = "An example tool",
46///     meta = "{\"version\": \"1.0\"}",
47///     title = "Example Tool"
48/// )]
49/// #[derive(rust_mcp_macros::JsonSchema)]
50/// struct ExampleTool {
51///     field1: String,
52///     field2: i32,
53/// }
54///
55/// assert_eq!(ExampleTool::tool_name(), "example_tool");
56/// let tool: rust_mcp_schema::Tool = ExampleTool::tool();
57/// assert_eq!(tool.name, "example_tool");
58/// assert_eq!(tool.description.unwrap(), "An example tool");
59/// assert_eq!(tool.meta.as_ref().unwrap().get("version").unwrap(), "1.0");
60/// assert_eq!(tool.title.unwrap(), "Example Tool");
61///
62/// let schema_properties = tool.input_schema.properties.unwrap();
63/// assert_eq!(schema_properties.len(), 2);
64/// assert!(schema_properties.contains_key("field1"));
65/// assert!(schema_properties.contains_key("field2"));
66/// }
67/// ```
68#[proc_macro_attribute]
69pub fn mcp_tool(attributes: TokenStream, input: TokenStream) -> TokenStream {
70    let input_item: syn::Item = parse_macro_input!(input as syn::Item);
71
72    let (ident, original_item) = match input_item {
73        syn::Item::Struct(struct_item) => {
74            let ident = struct_item.ident.clone();
75            (ident, syn::Item::Struct(struct_item))
76        }
77        syn::Item::Type(type_item) => {
78            // Only support simple non-generic type aliases like `type Foo = Bar;`
79            // (no paths with ::, no generics)
80            let aliased_ty = type_item.ty.clone();
81            if let syn::Type::Path(type_path) = *aliased_ty {
82                if type_path.path.leading_colon.is_none() && type_path.path.segments.len() == 1 {
83                    let segment = type_path.path.segments.first().unwrap();
84                    if matches!(segment.arguments, syn::PathArguments::None) {
85                        let ident = type_item.ident.clone();
86                        (ident, syn::Item::Type(type_item))
87                    } else {
88                        return quote! {
89                                compile_error!("mcp_tool does not support type aliases with generic arguments");
90                            }
91                            .into();
92                    }
93                } else {
94                    return quote! {
95                            compile_error!("mcp_tool only supports simple type aliases to a single identifier (e.g. `type Foo = Bar;`)");
96                        }
97                        .into();
98                }
99            } else {
100                return quote! {
101                    compile_error!("mcp_tool only supports type aliases to path types");
102                }
103                .into();
104            }
105        }
106        _ => {
107            return quote! {
108                compile_error!("#[mcp_tool] can only be applied to structs or type aliases");
109            }
110            .into();
111        }
112    };
113
114    let input_ident = &ident;
115    let macro_attributes = parse_macro_input!(attributes as McpToolMacroAttributes);
116
117    let ToolTokens {
118        base_crate,
119        tool_name,
120        tool_description,
121        meta,
122        title,
123        output_schema,
124        annotations,
125        execution,
126        icons,
127    } = generate_tool_tokens(macro_attributes);
128
129    // TODO: add support for schema version to ToolInputSchema :
130    // it defaults to JSON Schema 2020-12 when no explicit $schema is provided.
131    let tool_token = quote! {
132        #base_crate::Tool {
133            name: #tool_name.to_string(),
134            description: Some(#tool_description.to_string()),
135            #output_schema
136            #title
137            #meta
138            #annotations
139            #execution
140            #icons
141            input_schema: #base_crate::ToolInputSchema::new(required, properties, None)
142        }
143    };
144
145    let output = quote! {
146        impl #input_ident {
147            /// Returns the name of the tool as a String.
148            pub fn tool_name() -> String {
149                #tool_name.to_string()
150            }
151
152            /// Returns a `CallToolRequestParams` initialized with the current tool's name.
153            ///
154            /// You can further customize the request by adding arguments or other attributes
155            /// using the builder pattern. For example:
156            ///
157            /// ```ignore
158            /// # use my_crate::{MyTool};
159            /// let args = serde_json::Map::new();
160            /// let task_meta = TaskMetadata{ttl: Some(200)}
161            ///
162            /// let params: CallToolRequestParams = MyTool::request_params()
163            ///     .with_arguments(args)
164            ///     .with_task(task_meta);
165            /// ```
166            ///
167            /// # Returns
168            /// A `CallToolRequestParams` with the tool name set.
169            pub fn request_params() -> #base_crate::CallToolRequestParams {
170               #base_crate::CallToolRequestParams::new(#tool_name.to_string())
171            }
172
173            /// Constructs and returns a `rust_mcp_schema::Tool` instance.
174            ///
175            /// The tool includes the name, description, input schema, meta, and title derived from
176            /// the struct's attributes.
177            pub fn tool() -> #base_crate::Tool {
178                let json_schema = &#input_ident::json_schema();
179
180                let required: Vec<_> = match json_schema.get("required").and_then(|r| r.as_array()) {
181                    Some(arr) => arr
182                        .iter()
183                        .filter_map(|item| item.as_str().map(String::from))
184                        .collect(),
185                    None => Vec::new(),
186                };
187
188                let properties: Option<
189                    std::collections::HashMap<String, serde_json::Map<String, serde_json::Value>>,
190                > = json_schema
191                    .get("properties")
192                    .and_then(|v| v.as_object()) // Safely extract "properties" as an object.
193                    .map(|properties| {
194                        properties
195                            .iter()
196                            .filter_map(|(key, value)| {
197                                serde_json::to_value(value)
198                                    .ok() // If serialization fails, return None.
199                                    .and_then(|v| {
200                                        if let serde_json::Value::Object(obj) = v {
201                                            Some(obj)
202                                        } else {
203                                            None
204                                        }
205                                    })
206                                    .map(|obj| (key.to_string(), obj)) // Return the (key, value) tuple
207                            })
208                            .collect()
209                    });
210
211                #tool_token
212            }
213        }
214        // Retain the original item (struct definition)
215        #original_item
216    };
217
218    TokenStream::from(output)
219}
220
221#[proc_macro_attribute]
222pub fn mcp_elicit(args: TokenStream, input: TokenStream) -> TokenStream {
223    let input = parse_macro_input!(input as DeriveInput);
224
225    let fields = match &input.data {
226        Data::Struct(s) => match &s.fields {
227            Fields::Named(n) => &n.named,
228            _ => panic!("mcp_elicit only supports structs with named fields"),
229        },
230        _ => panic!("mcp_elicit only supports structs"),
231    };
232
233    let struct_name = &input.ident;
234    let elicit_args = parse_macro_input!(args as ElicitArgs);
235
236    let base_crate = base_crate();
237
238    let message = &elicit_args.message;
239
240    let impl_block = match elicit_args.mode {
241        ElicitMode::Form => {
242            let (from_content, init) = generate_from_impl(fields, &base_crate);
243            let schema = generate_form_schema(struct_name, &base_crate);
244
245            quote! {
246                impl #struct_name {
247                    pub fn message() -> &'static str{
248                        #message
249                    }
250
251                    pub fn requested_schema() -> #base_crate::ElicitFormSchema {
252                        #schema
253                    }
254
255                    pub fn elicit_mode()->&'static str{
256                        "form"
257                    }
258
259                    pub fn elicit_form_params() -> #base_crate::ElicitRequestFormParams {
260                            #base_crate::ElicitRequestFormParams::new(
261                                Self::message().to_string(),
262                                Self::requested_schema(),
263                                None,
264                                None,
265                            )
266                    }
267
268                    pub fn elicit_request_params() -> #base_crate::ElicitRequestParams {
269                        Self::elicit_form_params().into()
270                    }
271
272                    pub fn from_elicit_result_content(
273                        mut content: Option<std::collections::HashMap<String, #base_crate::ElicitResultContent>>,
274                    ) -> Result<Self, #base_crate::RpcError> {
275                        use #base_crate::{ElicitResultContent as V, RpcError};
276                        let mut map = content.take().unwrap_or_default();
277                            #from_content
278                            Ok(#init)
279                    }
280
281                }
282            }
283        }
284        ElicitMode::Url { url } => {
285            let (from_content, init) = generate_from_impl(fields, &base_crate);
286
287            quote! {
288                impl #struct_name {
289                    pub fn message() -> &'static str {
290                        #message
291                    }
292
293                    pub fn url() -> &'static str {
294                        #url
295                    }
296
297                    pub fn elicit_mode()->&'static str {
298                        "url"
299                    }
300
301                    pub fn elicit_url_params(elicitation_id:String) -> #base_crate::ElicitRequestUrlParams {
302                            #base_crate::ElicitRequestUrlParams::new(
303                                elicitation_id,
304                                Self::message().to_string(),
305                                Self::url().to_string(),
306                                None,
307                                None,
308                            )
309                    }
310
311                    pub fn elicit_request_params(elicitation_id:String) -> #base_crate::ElicitRequestParams {
312                        Self::elicit_url_params(elicitation_id).into()
313                    }
314
315                    pub fn from_elicit_result_content(
316                        mut content: Option<std::collections::HashMap<String, #base_crate::ElicitResultContent>>,
317                    ) -> Result<Self, RpcError> {
318                        use #base_crate::{ElicitResultContent as V, RpcError};
319                        let mut map = content.take().unwrap_or_default();
320                            #from_content
321                            Ok(#init)
322                    }
323                }
324            }
325        }
326    };
327
328    let expanded = quote! {
329        #input
330        #impl_block
331    };
332
333    TokenStream::from(expanded)
334}
335
336/// Derives a JSON Schema representation for a struct.
337///
338/// This procedural macro generates a `json_schema()` method for the annotated struct, returning a
339/// `serde_json::Map<String, serde_json::Value>` that represents the struct as a JSON Schema object.
340/// The schema includes the struct's fields as properties, with support for basic types, `Option<T>`,
341/// `Vec<T>`, and nested structs that also derive `JsonSchema`.
342///
343/// # Features
344/// - **Basic Types:** Maps `String` to `"string"`, `i32` to `"integer"`, `bool` to `"boolean"`, etc.
345/// - **`Option<T>`:** Adds `"nullable": true` to the schema of the inner type, indicating the field is optional.
346/// - **`Vec<T>`:** Generates an `"array"` schema with an `"items"` field describing the inner type.
347/// - **Nested Structs:** Recursively includes the schema of nested structs (assumed to derive `JsonSchema`),
348///   embedding their `"properties"` and `"required"` fields.
349/// - **Required Fields:** Adds a top-level `"required"` array listing field names not wrapped in `Option`.
350///
351/// # Notes
352/// It’s designed as a straightforward solution to meet the basic needs of this package, supporting
353/// common types and simple nested structures. For more advanced features or robust JSON Schema generation,
354/// consider exploring established crates like
355/// [`schemars`](https://crates.io/crates/schemars) on crates.io
356///
357/// # Limitations
358/// - Supports only structs with named fields (e.g., `struct S { field: Type }`).
359/// - Nested structs must also derive `JsonSchema`, or compilation will fail.
360/// - Unknown types are mapped to `{"type": "unknown"}`.
361/// - Type paths must be in scope (e.g., fully qualified paths like `my_mod::InnerStruct` work if imported).
362///
363/// # Panics
364/// - If the input is not a struct with named fields (e.g., tuple structs or enums).
365///
366/// # Dependencies
367/// Relies on `serde_json` for `Map` and `Value` types.
368///
369#[proc_macro_derive(JsonSchema, attributes(json_schema))]
370pub fn derive_json_schema(input: TokenStream) -> TokenStream {
371    let input = syn::parse_macro_input!(input as DeriveInput);
372    let name = &input.ident;
373
374    let schema_body = match &input.data {
375        Data::Struct(data) => match &data.fields {
376            Fields::Named(fields) => {
377                let field_entries = fields.named.iter().map(|field| {
378                    let field_attrs = &field.attrs;
379                    let renamed_field = renamed_field(field_attrs);
380                    let field_name =
381                        renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
382                    let field_type = &field.ty;
383
384                    let schema = type_to_json_schema(field_type, field_attrs);
385                    quote! {
386                        properties.insert(
387                            #field_name.to_string(),
388                            serde_json::Value::Object(#schema)
389                        );
390                    }
391                });
392
393                let required_fields = fields.named.iter().filter_map(|field| {
394                    let renamed_field = renamed_field(&field.attrs);
395                    let field_name =
396                        renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
397
398                    let field_type = &field.ty;
399                    if !is_option(field_type) {
400                        Some(quote! {
401                            required.push(#field_name.to_string());
402                        })
403                    } else {
404                        None
405                    }
406                });
407
408                quote! {
409                    let mut schema = serde_json::Map::new();
410                    let mut properties = serde_json::Map::new();
411                    let mut required = Vec::new();
412
413                    #(#field_entries)*
414
415                    #(#required_fields)*
416
417                    schema.insert("type".to_string(), serde_json::Value::String("object".to_string()));
418                    schema.insert("properties".to_string(), serde_json::Value::Object(properties));
419                    if !required.is_empty() {
420                        schema.insert("required".to_string(), serde_json::Value::Array(
421                            required.into_iter().map(serde_json::Value::String).collect()
422                        ));
423                    }
424
425                    schema
426                }
427            }
428            _ => panic!("JsonSchema derive macro only supports named fields for structs"),
429        },
430        Data::Enum(data) => {
431            let variant_schemas = data.variants.iter().map(|variant| {
432                let variant_attrs = &variant.attrs;
433                let variant_name = variant.ident.to_string();
434                let renamed_variant = renamed_field(variant_attrs).unwrap_or(variant_name.clone());
435
436                // Parse variant-level json_schema attributes
437                let mut title: Option<String> = None;
438                let mut description: Option<String> = None;
439                for attr in variant_attrs {
440                    if attr.path().is_ident("json_schema") {
441                        let _ = attr.parse_nested_meta(|meta| {
442                            if meta.path.is_ident("title") {
443                                title = Some(meta.value()?.parse::<syn::LitStr>()?.value());
444                            } else if meta.path.is_ident("description") {
445                                description = Some(meta.value()?.parse::<syn::LitStr>()?.value());
446                            }
447                            Ok(())
448                        });
449                    }
450                }
451
452                let title_quote = title.as_ref().map(|t| {
453                    quote! { map.insert("title".to_string(), serde_json::Value::String(#t.to_string())); }
454                });
455                let description_quote = description.as_ref().map(|desc| {
456                    quote! { map.insert("description".to_string(), serde_json::Value::String(#desc.to_string())); }
457                });
458
459                match &variant.fields {
460                    Fields::Unit => {
461                        // Unit variant: use "enum" with the variant name
462                        quote! {
463                            {
464                                let mut map = serde_json::Map::new();
465                                map.insert("enum".to_string(), serde_json::Value::Array(vec![
466                                    serde_json::Value::String(#renamed_variant.to_string())
467                                ]));
468                                #title_quote
469                                #description_quote
470                                serde_json::Value::Object(map)
471                            }
472                        }
473                    }
474                    Fields::Unnamed(fields) => {
475                        // Newtype or tuple variant
476                        if fields.unnamed.len() == 1 {
477                            // Newtype variant: use the inner type's schema
478                            let field = &fields.unnamed[0];
479                            let field_type = &field.ty;
480                            let field_attrs = &field.attrs;
481                            let schema = type_to_json_schema(field_type, field_attrs);
482                            quote! {
483                                {
484                                    let mut map = #schema;
485                                    #title_quote
486                                    #description_quote
487                                    serde_json::Value::Object(map)
488                                }
489                            }
490                        } else {
491                            // Tuple variant: array with items
492                            let field_schemas = fields.unnamed.iter().map(|field| {
493                                let field_type = &field.ty;
494                                let field_attrs = &field.attrs;
495                                let schema = type_to_json_schema(field_type, field_attrs);
496                                quote! { serde_json::Value::Object(#schema) }
497                            });
498                            quote! {
499                                {
500                                    let mut map = serde_json::Map::new();
501                                    map.insert("type".to_string(), serde_json::Value::String("array".to_string()));
502                                    map.insert("items".to_string(), serde_json::Value::Array(vec![#(#field_schemas),*]));
503                                    map.insert("additionalItems".to_string(), serde_json::Value::Bool(false));
504                                    #title_quote
505                                    #description_quote
506                                    serde_json::Value::Object(map)
507                                }
508                            }
509                        }
510                    }
511                    Fields::Named(fields) => {
512                        // Struct variant: object with properties and required fields
513                        let field_entries = fields.named.iter().map(|field| {
514                            let field_attrs = &field.attrs;
515                            let renamed_field = renamed_field(field_attrs);
516                            let field_name = renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
517                            let field_type = &field.ty;
518
519                            let schema = type_to_json_schema(field_type, field_attrs);
520                            quote! {
521                                properties.insert(
522                                    #field_name.to_string(),
523                                    serde_json::Value::Object(#schema)
524                                );
525                            }
526                        });
527
528                        let required_fields = fields.named.iter().filter_map(|field| {
529                            let renamed_field = renamed_field(&field.attrs);
530                            let field_name = renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
531
532                            let field_type = &field.ty;
533                            if !is_option(field_type) {
534                                Some(quote! {
535                                    required.push(#field_name.to_string());
536                                })
537                            } else {
538                                None
539                            }
540                        });
541
542                        quote! {
543                            {
544                                let mut map = serde_json::Map::new();
545                                let mut properties = serde_json::Map::new();
546                                let mut required = Vec::new();
547
548                                #(#field_entries)*
549
550                                #(#required_fields)*
551
552                                map.insert("type".to_string(), serde_json::Value::String("object".to_string()));
553                                map.insert("properties".to_string(), serde_json::Value::Object(properties));
554                                if !required.is_empty() {
555                                    map.insert("required".to_string(), serde_json::Value::Array(
556                                        required.into_iter().map(serde_json::Value::String).collect()
557                                    ));
558                                }
559                                #title_quote
560                                #description_quote
561                                serde_json::Value::Object(map)
562                            }
563                        }
564                    }
565                }
566            });
567
568            quote! {
569                let mut schema = serde_json::Map::new();
570                schema.insert("oneOf".to_string(), serde_json::Value::Array(vec![
571                    #(#variant_schemas),*
572                ]));
573                schema
574            }
575        }
576        _ => panic!("JsonSchema derive macro only supports structs and enums"),
577    };
578
579    let expanded = quote! {
580        impl #name {
581            pub fn json_schema() -> serde_json::Map<String, serde_json::Value> {
582                #schema_body
583            }
584        }
585    };
586    TokenStream::from(expanded)
587}
588
589#[proc_macro_attribute]
590/// A procedural macro attribute to generate `rust_mcp_schema::Resource` related utility methods for a struct.
591///
592/// The `mcp_resource` macro adds static methods to the annotated struct that provide access to
593/// resource metadata and construct a fully populated `rust_mcp_schema::Resource` instance.
594///
595/// Generated methods:
596/// - `resource_name()` → returns the resource name as `&'static str`
597/// - `resource_uri()` → returns the resource URI as `&'static str`
598/// - `resource()` → constructs and returns a complete `rust_mcp_schema::Resource` value
599///
600/// # Attributes
601///
602/// All attributes are optional except `name` and `uri`, which are **required** and must be non-empty.
603///
604/// | Attribute     | Type                                 | Required | Description |
605/// |---------------|--------------------------------------|----------|-------------|
606/// | `name`        | string literal or `concat!(...)`     | Yes      | Unique name of the resource. |
607/// | `description` | string literal or `concat!(...)`     | Yes      | Human-readable description of the resource. |
608/// | `title`       | string literal or `concat!(...)`     | No       | Display title for the resource. |
609/// | `meta`        | JSON object as string literal        | No       | Arbitrary metadata as a valid JSON object. Must parse as a JSON object (not array, null, etc.). |
610/// | `mime_type`   | string literal                       | No       | MIME type of the resource (e.g., `"image/png"`, `"application/pdf"`). |
611/// | `size`        | integer literal (`i64`)              | No       | Size of the resource in bytes. |
612/// | `uri`         | string literal                       | No       | URI where the resource can be accessed. |
613/// | `audience`    | array of string literals             | No       | List of intended audiences (e.g., `["user", "system"]`). |
614/// | `icons`       | array of icon objects                | No       | List of icons in the same format as web app manifests (supports `src`, `sizes`, `type`). |
615///
616/// String fields (`name`, `description`, `title`) support `concat!(...)` with string literals.
617///
618/// # Panics
619///
620/// The macro will cause a compile-time error (not a runtime panic) if:
621/// - Applied to anything other than a struct.
622/// - Required attributes (`name` or `uri`) are missing or empty.
623/// - `meta` is provided but is not a valid JSON object.
624/// - Invalid types are used for any attribute (e.g., non-integer for `size`).
625///
626/// # Example
627///
628/// ```rust
629/// use rust_mcp_macros::mcp_resource;
630/// #[mcp_resource(
631///     name = "company-logo",
632///     description = "The official company logo in high resolution",
633///     title = "Company Logo",
634///     mime_type = "image/png",
635///     size = 102400,
636///     uri = "https://example.com/assets/logo.png",
637///     audience = ["user", "assistant"],
638///     meta = "{\"license\": \"proprietary\", \"author\": \"Ali Hashemi\"}",
639///     icons = [
640///     ( src = "logo-192.png", sizes = ["192x192"], mime_type = "image/png" ),
641///     ( src = "logo-512.png", sizes = ["512x512"], mime_type = "image/png" )
642///     ]
643/// )]
644/// struct CompanyLogo{};
645///
646/// // Usage
647/// assert_eq!(CompanyLogo::resource_name(), "company-logo");
648/// assert_eq!(CompanyLogo::resource_uri(), "https://example.com/assets/logo.png");
649///
650/// let resource = CompanyLogo::resource();
651/// assert_eq!(resource.name, "company-logo");
652/// assert_eq!(resource.mime_type.unwrap(), "image/png");
653/// assert_eq!(resource.size.unwrap(), 102400);
654/// assert!(resource.icons.len() == 2);
655/// ```
656pub fn mcp_resource(attributes: TokenStream, input: TokenStream) -> TokenStream {
657    let input = parse_macro_input!(input as DeriveInput);
658    let input_ident = &input.ident;
659    let macro_attributes = parse_macro_input!(attributes as McpResourceMacroAttributes);
660
661    let ResourceTokens {
662        base_crate,
663        name,
664        description,
665        meta,
666        title,
667        icons,
668        annotations,
669        mime_type,
670        size,
671        uri,
672    } = generate_resource_tokens(macro_attributes);
673
674    quote! {
675         impl #input_ident {
676
677            /// returns the Resource uri
678            pub fn resource_uri()->&'static str{
679                #uri
680            }
681
682            /// returns the Resource name
683            pub fn resource_name()->&'static str{
684                #name
685            }
686
687            /// Constructs and returns a `rust_mcp_schema::Resource` instance.
688            pub fn resource()->#base_crate::Resource{
689                #base_crate::Resource{
690                    annotations: #annotations,
691                    description: #description,
692                    icons: #icons,
693                    meta: #meta,
694                    mime_type: #mime_type,
695                    name: #name,
696                    size: #size,
697                    title: #title,
698                    uri: #uri
699                }
700            }
701         }
702         #input
703    }
704    .into()
705}
706
707#[proc_macro_attribute]
708/// A procedural macro attribute to generate `rust_mcp_schema::Resource` related utility methods for a struct.
709///
710/// The `mcp_resource` macro adds static methods to the annotated struct that provide access to
711/// resource metadata and construct a fully populated `rust_mcp_schema::Resource` instance.
712///
713/// Generated methods:
714/// - `resource_name()` → returns the resource name as `&'static str`
715/// - `resource_uri_template()` → returns the resource template URI as `&'static str`
716/// - `resource()` → constructs and returns a complete `rust_mcp_schema::Resource` value
717///
718/// # Attributes
719///
720/// All attributes are optional except `name` and `uri`, which are **required** and must be non-empty.
721///
722/// | Attribute     | Type                                 | Required | Description |
723/// |---------------|--------------------------------------|----------|-------------|
724/// | `name`        | string literal or `concat!(...)`     | Yes      | Unique name of the resource. |
725/// | `description` | string literal or `concat!(...)`     | Yes      | Human-readable description of the resource. |
726/// | `title`       | string literal or `concat!(...)`     | No       | Display title for the resource. |
727/// | `meta`        | JSON object as string literal        | No       | Arbitrary metadata as a valid JSON object. Must parse as a JSON object (not array, null, etc.). |
728/// | `mime_type`   | string literal                       | No       | MIME type of the resource (e.g., `"image/png"`, `"application/pdf"`). |
729/// | `uri_template`         | string literal                       | No       | URI template where the resource can be accessed. |
730/// | `audience`    | array of string literals             | No       | List of intended audiences (e.g., `["user", "system"]`). |
731/// | `icons`       | array of icon objects                | No       | List of icons in the same format as web app manifests (supports `src`, `sizes`, `type`). |
732///
733/// String fields (`name`, `description`, `title`) support `concat!(...)` with string literals.
734///
735/// # Panics
736///
737/// The macro will cause a compile-time error (not a runtime panic) if:
738/// - Applied to anything other than a struct.
739/// - Required attributes (`name` or `uri_template`) are missing or empty.
740/// - `meta` is provided but is not a valid JSON object.
741/// - Invalid types are used for any attribute (e.g., non-integer for `size`).
742///
743/// # Example
744///
745/// ```rust
746/// use rust_mcp_macros::mcp_resource_template;
747/// #[mcp_resource_template(
748///     name = "company-logos",
749///     description = "The official company logos in different resolutions",
750///     title = "Company Logos",
751///     mime_type = "image/png",
752///     uri_template = "https://example.com/assets/{file_path}",
753///     audience = ["user", "assistant"],
754///     meta = "{\"license\": \"proprietary\", \"author\": \"Ali Hashemi\"}",
755///     icons = [
756///     ( src = "logo-192.png", sizes = ["192x192"], mime_type = "image/png" ),
757///     ( src = "logo-512.png", sizes = ["512x512"], mime_type = "image/png" )
758///     ]
759/// )]
760/// struct CompanyLogo {};
761///
762/// // Usage
763/// assert_eq!(CompanyLogo::resource_template_name(), "company-logos");
764/// assert_eq!(
765///     CompanyLogo::resource_template_uri(),
766///     "https://example.com/assets/{file_path}"
767/// );
768///
769/// let resource_template = CompanyLogo::resource_template();
770/// assert_eq!(resource_template.name, "company-logos");
771/// assert_eq!(resource_template.mime_type.unwrap(), "image/png");
772/// assert!(resource_template.icons.len() == 2);
773/// ```
774pub fn mcp_resource_template(attributes: TokenStream, input: TokenStream) -> TokenStream {
775    let input = parse_macro_input!(input as DeriveInput);
776    let input_ident = &input.ident;
777    let macro_attributes = parse_macro_input!(attributes as McpResourceTemplateMacroAttributes);
778
779    let ResourceTemplateTokens {
780        base_crate,
781        name,
782        description,
783        meta,
784        title,
785        icons,
786        annotations,
787        mime_type,
788        uri_template,
789    } = generate_resource_template_tokens(macro_attributes);
790
791    quote! {
792         impl #input_ident {
793
794            /// returns the Resource Template uri
795            pub fn resource_template_uri()->&'static str{
796                #uri_template
797            }
798
799            /// returns the Resource Template name
800            pub fn resource_template_name()->&'static str{
801                #name
802            }
803
804            /// Constructs and returns a `rust_mcp_schema::Resource` instance.
805            pub fn resource_template()->#base_crate::ResourceTemplate{
806                #base_crate::ResourceTemplate{
807                    annotations: #annotations,
808                    description: #description,
809                    icons: #icons,
810                    meta: #meta,
811                    mime_type: #mime_type,
812                    name: #name,
813                    title: #title,
814                    uri_template: #uri_template
815                }
816            }
817         }
818         #input
819    }
820    .into()
821}