extern crate proc_macro;
mod common;
mod elicit;
mod resource;
mod tool;
mod utils;
use crate::elicit::generator::{generate_form_schema, generate_from_impl};
use crate::elicit::parser::{ElicitArgs, ElicitMode};
use crate::resource::generator::{
generate_resource_template_tokens, generate_resource_tokens, ResourceTemplateTokens,
ResourceTokens,
};
use crate::resource::parser::{McpResourceMacroAttributes, McpResourceTemplateMacroAttributes};
use crate::tool::generator::{generate_tool_tokens, ToolTokens};
use crate::tool::parser::McpToolMacroAttributes;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields};
use utils::{base_crate, is_option, is_vec_string, renamed_field, type_to_json_schema};
#[proc_macro_attribute]
pub fn mcp_tool(attributes: TokenStream, input: TokenStream) -> TokenStream {
let input_item: syn::Item = parse_macro_input!(input as syn::Item);
let (ident, original_item) = match input_item {
syn::Item::Struct(struct_item) => {
let ident = struct_item.ident.clone();
(ident, syn::Item::Struct(struct_item))
}
syn::Item::Type(type_item) => {
let aliased_ty = type_item.ty.clone();
if let syn::Type::Path(type_path) = *aliased_ty {
if type_path.path.leading_colon.is_none() && type_path.path.segments.len() == 1 {
let segment = type_path.path.segments.first().unwrap();
if matches!(segment.arguments, syn::PathArguments::None) {
let ident = type_item.ident.clone();
(ident, syn::Item::Type(type_item))
} else {
return quote! {
compile_error!("mcp_tool does not support type aliases with generic arguments");
}
.into();
}
} else {
return quote! {
compile_error!("mcp_tool only supports simple type aliases to a single identifier (e.g. `type Foo = Bar;`)");
}
.into();
}
} else {
return quote! {
compile_error!("mcp_tool only supports type aliases to path types");
}
.into();
}
}
_ => {
return quote! {
compile_error!("#[mcp_tool] can only be applied to structs or type aliases");
}
.into();
}
};
let input_ident = &ident;
let macro_attributes = parse_macro_input!(attributes as McpToolMacroAttributes);
let ToolTokens {
base_crate,
tool_name,
tool_description,
meta,
title,
output_schema,
annotations,
execution,
icons,
} = generate_tool_tokens(macro_attributes);
let tool_token = quote! {
#base_crate::Tool {
name: #tool_name.to_string(),
description: Some(#tool_description.to_string()),
#output_schema
#title
#meta
#annotations
#execution
#icons
input_schema: #base_crate::ToolInputSchema::new(required, properties, None)
}
};
let output = quote! {
impl #input_ident {
pub fn tool_name() -> String {
#tool_name.to_string()
}
pub fn request_params() -> #base_crate::CallToolRequestParams {
#base_crate::CallToolRequestParams::new(#tool_name.to_string())
}
pub fn tool() -> #base_crate::Tool {
let json_schema = &#input_ident::json_schema();
let required: Vec<_> = match json_schema.get("required").and_then(|r| r.as_array()) {
Some(arr) => arr
.iter()
.filter_map(|item| item.as_str().map(String::from))
.collect(),
None => Vec::new(),
};
let properties: Option<
std::collections::BTreeMap<String, serde_json::Map<String, serde_json::Value>>,
> = json_schema
.get("properties")
.and_then(|v| v.as_object()) .map(|properties| {
properties
.iter()
.filter_map(|(key, value)| {
serde_json::to_value(value)
.ok() .and_then(|v| {
if let serde_json::Value::Object(obj) = v {
Some(obj)
} else {
None
}
})
.map(|obj| (key.to_string(), obj)) })
.collect()
});
#tool_token
}
}
#original_item
};
TokenStream::from(output)
}
#[proc_macro_attribute]
pub fn mcp_elicit(args: TokenStream, input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let fields = match &input.data {
Data::Struct(s) => match &s.fields {
Fields::Named(n) => &n.named,
_ => panic!("mcp_elicit only supports structs with named fields"),
},
_ => panic!("mcp_elicit only supports structs"),
};
let struct_name = &input.ident;
let elicit_args = parse_macro_input!(args as ElicitArgs);
let base_crate = base_crate();
let message = &elicit_args.message;
let impl_block = match elicit_args.mode {
ElicitMode::Form => {
let (from_content, init) = generate_from_impl(fields, &base_crate);
let schema = generate_form_schema(struct_name, &base_crate);
quote! {
impl #struct_name {
pub fn message() -> &'static str{
#message
}
pub fn requested_schema() -> #base_crate::ElicitFormSchema {
#schema
}
pub fn elicit_mode()->&'static str{
"form"
}
pub fn elicit_form_params() -> #base_crate::ElicitRequestFormParams {
#base_crate::ElicitRequestFormParams::new(
Self::message().to_string(),
Self::requested_schema(),
None,
None,
)
}
pub fn elicit_request_params() -> #base_crate::ElicitRequestParams {
Self::elicit_form_params().into()
}
pub fn from_elicit_result_content(
mut content: Option<std::collections::BTreeMap<String, #base_crate::ElicitResultContent>>,
) -> Result<Self, #base_crate::RpcError> {
use #base_crate::{ElicitResultContent as V, RpcError};
let mut map = content.take().unwrap_or_default();
#from_content
Ok(#init)
}
}
}
}
ElicitMode::Url { url } => {
let (from_content, init) = generate_from_impl(fields, &base_crate);
quote! {
impl #struct_name {
pub fn message() -> &'static str {
#message
}
pub fn url() -> &'static str {
#url
}
pub fn elicit_mode()->&'static str {
"url"
}
pub fn elicit_url_params(elicitation_id:String) -> #base_crate::ElicitRequestUrlParams {
#base_crate::ElicitRequestUrlParams::new(
elicitation_id,
Self::message().to_string(),
Self::url().to_string(),
None,
None,
)
}
pub fn elicit_request_params(elicitation_id:String) -> #base_crate::ElicitRequestParams {
Self::elicit_url_params(elicitation_id).into()
}
pub fn from_elicit_result_content(
mut content: Option<std::collections::BTreeMap<String, #base_crate::ElicitResultContent>>,
) -> Result<Self, RpcError> {
use #base_crate::{ElicitResultContent as V, RpcError};
let mut map = content.take().unwrap_or_default();
#from_content
Ok(#init)
}
}
}
}
};
let expanded = quote! {
#input
#impl_block
};
TokenStream::from(expanded)
}
#[proc_macro_derive(JsonSchema, attributes(json_schema))]
pub fn derive_json_schema(input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let schema_body = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => {
let field_entries = fields.named.iter().map(|field| {
let field_attrs = &field.attrs;
let renamed_field = renamed_field(field_attrs);
let field_name =
renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
let field_type = &field.ty;
let schema = type_to_json_schema(field_type, field_attrs);
quote! {
properties.insert(
#field_name.to_string(),
serde_json::Value::Object(#schema)
);
}
});
let required_fields = fields.named.iter().filter_map(|field| {
let renamed_field = renamed_field(&field.attrs);
let field_name =
renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
let field_type = &field.ty;
if !is_option(field_type) {
Some(quote! {
required.push(#field_name.to_string());
})
} else {
None
}
});
quote! {
let mut schema = serde_json::Map::new();
let mut properties = serde_json::Map::new();
let mut required = Vec::new();
#(#field_entries)*
#(#required_fields)*
schema.insert("type".to_string(), serde_json::Value::String("object".to_string()));
schema.insert("properties".to_string(), serde_json::Value::Object(properties));
if !required.is_empty() {
schema.insert("required".to_string(), serde_json::Value::Array(
required.into_iter().map(serde_json::Value::String).collect()
));
}
schema
}
}
_ => panic!("JsonSchema derive macro only supports named fields for structs"),
},
Data::Enum(data) => {
let variant_schemas = data.variants.iter().map(|variant| {
let variant_attrs = &variant.attrs;
let variant_name = variant.ident.to_string();
let renamed_variant = renamed_field(variant_attrs).unwrap_or(variant_name.clone());
let mut title: Option<String> = None;
let mut description: Option<String> = None;
for attr in variant_attrs {
if attr.path().is_ident("json_schema") {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("title") {
title = Some(meta.value()?.parse::<syn::LitStr>()?.value());
} else if meta.path.is_ident("description") {
description = Some(meta.value()?.parse::<syn::LitStr>()?.value());
}
Ok(())
});
}
}
let title_quote = title.as_ref().map(|t| {
quote! { map.insert("title".to_string(), serde_json::Value::String(#t.to_string())); }
});
let description_quote = description.as_ref().map(|desc| {
quote! { map.insert("description".to_string(), serde_json::Value::String(#desc.to_string())); }
});
match &variant.fields {
Fields::Unit => {
quote! {
{
let mut map = serde_json::Map::new();
map.insert("enum".to_string(), serde_json::Value::Array(vec![
serde_json::Value::String(#renamed_variant.to_string())
]));
#title_quote
#description_quote
serde_json::Value::Object(map)
}
}
}
Fields::Unnamed(fields) => {
if fields.unnamed.len() == 1 {
let field = &fields.unnamed[0];
let field_type = &field.ty;
let field_attrs = &field.attrs;
let schema = type_to_json_schema(field_type, field_attrs);
quote! {
{
let mut map = #schema;
#title_quote
#description_quote
serde_json::Value::Object(map)
}
}
} else {
let field_schemas = fields.unnamed.iter().map(|field| {
let field_type = &field.ty;
let field_attrs = &field.attrs;
let schema = type_to_json_schema(field_type, field_attrs);
quote! { serde_json::Value::Object(#schema) }
});
quote! {
{
let mut map = serde_json::Map::new();
map.insert("type".to_string(), serde_json::Value::String("array".to_string()));
map.insert("items".to_string(), serde_json::Value::Array(vec![#(#field_schemas),*]));
map.insert("additionalItems".to_string(), serde_json::Value::Bool(false));
#title_quote
#description_quote
serde_json::Value::Object(map)
}
}
}
}
Fields::Named(fields) => {
let field_entries = fields.named.iter().map(|field| {
let field_attrs = &field.attrs;
let renamed_field = renamed_field(field_attrs);
let field_name = renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
let field_type = &field.ty;
let schema = type_to_json_schema(field_type, field_attrs);
quote! {
properties.insert(
#field_name.to_string(),
serde_json::Value::Object(#schema)
);
}
});
let required_fields = fields.named.iter().filter_map(|field| {
let renamed_field = renamed_field(&field.attrs);
let field_name = renamed_field.unwrap_or(field.ident.as_ref().unwrap().to_string());
let field_type = &field.ty;
if !is_option(field_type) {
Some(quote! {
required.push(#field_name.to_string());
})
} else {
None
}
});
quote! {
{
let mut map = serde_json::Map::new();
let mut properties = serde_json::Map::new();
let mut required = Vec::new();
#(#field_entries)*
#(#required_fields)*
map.insert("type".to_string(), serde_json::Value::String("object".to_string()));
map.insert("properties".to_string(), serde_json::Value::Object(properties));
if !required.is_empty() {
map.insert("required".to_string(), serde_json::Value::Array(
required.into_iter().map(serde_json::Value::String).collect()
));
}
#title_quote
#description_quote
serde_json::Value::Object(map)
}
}
}
}
});
quote! {
let mut schema = serde_json::Map::new();
schema.insert("oneOf".to_string(), serde_json::Value::Array(vec![
#(#variant_schemas),*
]));
schema
}
}
_ => panic!("JsonSchema derive macro only supports structs and enums"),
};
let expanded = quote! {
impl #name {
pub fn json_schema() -> serde_json::Map<String, serde_json::Value> {
#schema_body
}
}
};
TokenStream::from(expanded)
}
#[proc_macro_attribute]
pub fn mcp_resource(attributes: TokenStream, input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let input_ident = &input.ident;
let macro_attributes = parse_macro_input!(attributes as McpResourceMacroAttributes);
let ResourceTokens {
base_crate,
name,
description,
meta,
title,
icons,
annotations,
mime_type,
size,
uri,
} = generate_resource_tokens(macro_attributes);
quote! {
impl #input_ident {
pub fn resource_uri()->&'static str{
#uri
}
pub fn resource_name()->&'static str{
#name
}
pub fn resource()->#base_crate::Resource{
#base_crate::Resource{
annotations: #annotations,
description: #description,
icons: #icons,
meta: #meta,
mime_type: #mime_type,
name: #name,
size: #size,
title: #title,
uri: #uri
}
}
}
#input
}
.into()
}
#[proc_macro_attribute]
pub fn mcp_resource_template(attributes: TokenStream, input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let input_ident = &input.ident;
let macro_attributes = parse_macro_input!(attributes as McpResourceTemplateMacroAttributes);
let ResourceTemplateTokens {
base_crate,
name,
description,
meta,
title,
icons,
annotations,
mime_type,
uri_template,
} = generate_resource_template_tokens(macro_attributes);
quote! {
impl #input_ident {
pub fn resource_template_uri()->&'static str{
#uri_template
}
pub fn resource_template_name()->&'static str{
#name
}
pub fn resource_template()->#base_crate::ResourceTemplate{
#base_crate::ResourceTemplate{
annotations: #annotations,
description: #description,
icons: #icons,
meta: #meta,
mime_type: #mime_type,
name: #name,
title: #title,
uri_template: #uri_template
}
}
}
#input
}
.into()
}