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}